diff --git a/docs/features/commands.rst b/docs/features/commands.rst index 31da73c65..8abfb18a9 100644 --- a/docs/features/commands.rst +++ b/docs/features/commands.rst @@ -1,5 +1,5 @@ -Commands -======== +|stub| Commands +=============== The `Discord.Net.Commands`_ package DiscordBotClient extends DiscordClient with support for commands. @@ -11,10 +11,10 @@ Example (Simple) .. literalinclude:: /samples/command.cs :language: csharp6 :tab-width: 2 - + Example (Groups) ---------------- .. literalinclude:: /samples/command_group.cs :language: csharp6 - :tab-width: 2 \ No newline at end of file + :tab-width: 2 diff --git a/docs/features/events.rst b/docs/features/events.rst index 862d28dd3..2cfe27f54 100644 --- a/docs/features/events.rst +++ b/docs/features/events.rst @@ -1,75 +1,78 @@ -|stub| Events -============= +Events +====== Usage ----- -To take advantage of Events in Discord.Net, you need to hook into them. - -There are two ways of hooking into events. See the example for examples on using these events. - -Usable Events -------------- -+--------------------+--------------------+------------------------------------------+ -| Event Name | EventArgs | Description | -+====================+====================+==========================================+ -| UserBanned | BanEventArgs | Called when a user is banned. | -+--------------------+--------------------+------------------------------------------+ -| UserUnbanned | BanEventArgs | Called when a user is unbanned. | -+--------------------+--------------------+------------------------------------------+ -| ChannelCreated | ChannelEventArgs | Called when a channel is created. | -+--------------------+--------------------+------------------------------------------+ -| ChannelDestroyed | ChannelEventArgs | Called when a channel is destroyed. | -+--------------------+--------------------+------------------------------------------+ -| ChannelUpdated | ChannelEventArgs | Called when a channel is updated. | -+--------------------+--------------------+------------------------------------------+ -| MessageReceived | MessageEventArgs | Called when a message is received. | -+--------------------+--------------------+------------------------------------------+ -| MessageSent | MessageEventArgs | Called when a message is sent. | -+--------------------+--------------------+------------------------------------------+ -| MessageDeleted | MessageEventArgs | Called when a message is deleted. | -+--------------------+--------------------+------------------------------------------+ -| MessageUpdated | MessageEventArgs | Called when a message is updated\\edited.| -+--------------------+--------------------+------------------------------------------+ -| MessageReadRemotely| MessageEventArgs | Called when a message is read. | -+--------------------+--------------------+------------------------------------------+ -| RoleCreated | RoleEventArgs | Called when a role is created. | -+--------------------+--------------------+------------------------------------------+ -| RoleUpdated | RoleEventArgs | Called when a role is updated. | -+--------------------+--------------------+------------------------------------------+ -| RoleDeleted | RoleEventArgs | Called when a role is deleted. | -+--------------------+--------------------+------------------------------------------+ -| JoinedServer | ServerEventArgs | Called when a member joins a server. | -+--------------------+--------------------+------------------------------------------+ -| LeftServer | ServerEventArgs | Called when a member leaves a server. | -+--------------------+--------------------+------------------------------------------+ -| ServerUpdated | ServerEventArgs | Called when a server is updated. | -+--------------------+--------------------+------------------------------------------+ -| ServerUnavailable | ServerEventArgs | Called when a Discord server goes down. | -+--------------------+--------------------+------------------------------------------+ -| ServerAvailable | ServerEventArgs |Called when a Discord server goes back up.| -+--------------------+--------------------+------------------------------------------+ -| UserJoined | UserEventArgs | Called when a user joins a Channel. | -+--------------------+--------------------+------------------------------------------+ -| UserLeft | UserEventArgs | Called when a user leaves a Channel. | -+--------------------+--------------------+------------------------------------------+ -| UserUpdated | UserEventArgs | --- | -+--------------------+--------------------+------------------------------------------+ -| UserPresenceUpdated| UserEventArgs | Called when a user's presence changes. | -| | | (Here\\Away) | -+--------------------+--------------------+------------------------------------------+ -| UserVoiceState | UserEventArgs | Called when a user's voice state changes.| -| Updated | | (Muted\\Unmuted) | -+--------------------+--------------------+------------------------------------------+ -|UserIsTypingUpdated | UserEventArgs | Called when a user starts\\stops typing. | -+--------------------+--------------------+------------------------------------------+ -| UserIsSpeaking | UserEventArgs | Called when a user's voice state changes.| -| Updated | | (Speaking\\Not Speaking) | -+--------------------+--------------------+------------------------------------------+ -| ProfileUpdated | N/A | Called when a user's profile changes. | -+--------------------+--------------------+------------------------------------------+ -Example -------- - -.. literalinclude:: /samples/events.cs - :language: csharp6 - :tab-width: 2 \ No newline at end of file +Messages from the Discord server are exposed via events on the DiscordClient class and follow the standard EventHandler C# pattern. + +.. warning:: + Note that all synchronous code in an event handler will run on the gateway socket's thread and should be handled as quickly as possible. + Using the async-await pattern to let the thread continue immediately is recommended and is demonstrated in the examples below. + +Connection State +---------------- + +Connection Events will be raised when the Connection State of your client changes. + +.. warning:: + You should not use DiscordClient.Connected to run code when your client first connects to Discord. + If you lose connection and automatically reconnect, this code will be ran again, which may lead to unexpected behavior. + +Messages +-------- + +- MessageReceived, MessageUpdated and MessageDeleted are raised when a new message arrives, an existing one has been updated (by the user, or by Discord itself), or deleted. +- MessageAcknowledged is only triggered in client mode, and occurs when a message is read on another device logged-in with your account. + +Example of MessageReceived: + +.. code-block:: c# + + // (Preface: Echo Bots are discouraged, make sure your bot is not running in a public server if you use them) + + // Hook into the MessageReceived event using a Lambda + _client.MessageReceived += async (s, e) => { + // Check to make sure that the bot is not the author + if (!e.Message.IsAuthor) + // Echo the message back to the channel + await e.Channel.SendMessage(e.Message); + }; + +Users +----- + +There are several user events: + +- UserBanned: A user has been banned from a server. +- UserUnbanned: A user was unbanned. +- UserJoined: A user joins a server. +- UserLeft: A user left (or was kicked from) a server. +- UserIsTyping: A user in a channel starts typing. +- UserUpdated: A user object was updated (presence update, role/permission change, or a voice state update). + +.. note:: + UserUpdated Events include a ``User`` object for Before and After the change. + When accessing the User, you should only use ``e.Before`` if comparing changes, otherwise use ``e.After`` + +Examples: + +.. code-block:: c# + + // Register a Hook into the UserBanned event using a Lambda + _client.UserBanned += async (s, e) => { + // Create a Channel object by searching for a channel named '#logs' on the server the ban occurred in. + var logChannel = e.Server.FindChannels("logs").FirstOrDefault(); + // Send a message to the server's log channel, stating that a user was banned. + await logChannel.SendMessage($"User Banned: {e.User.Name}"); + }; + + // Register a Hook into the UserUpdated event using a Lambda + _client.UserUpdated += async (s, e) => { + // Check that the user is in a Voice channel + if (e.After.VoiceChannel == null) return; + + // See if they changed Voice channels + if (e.Before.VoiceChannel == e.After.VoiceChannel) return; + + await logChannel.SendMessage($"User {e.After.Name} changed voice channels!"); + }; diff --git a/docs/features/logging.rst b/docs/features/logging.rst index b16c9bfd1..4b9f254a5 100644 --- a/docs/features/logging.rst +++ b/docs/features/logging.rst @@ -1,11 +1,35 @@ -|stub| Logging -============== +Logging +======= -|stub-desc| +Discord.Net will log all of its events/exceptions using a built-in LogManager. +This LogManager can be accessed through DiscordClient.Log + +Usage +----- + +To handle Log Messages through Discord.Net's Logger, you must hook into the Log.Message Event. + +The LogManager does not provide a string-based result for the message, you must put your own message format together using the data provided through LogMessageEventArgs +See the Example for a snippet of logging. + +Logging Your Own Data +--------------------- + +The LogManager included in Discord.Net can also be used to log your own messages. + +You can use DiscordClient.Log.Log(LogSeverity, Source, Message, Exception), or one of the shortcut helpers, to log data. + +Example: +.. code-block:: c# + + _client.MessageReceived += async (s, e) { + // Log a new Message with Severity Info, Sourced from 'MessageReceived', with the Message Contents. + _client.Log.Info("MessageReceived", e.Message.Text, null); + }; Example ------- .. literalinclude:: /samples/logging.cs - :language: csharp6 - :tab-width: 2 \ No newline at end of file + :language: c# + :tab-width: 2 diff --git a/docs/features/modes.rst b/docs/features/modes.rst new file mode 100644 index 000000000..63a5922c3 --- /dev/null +++ b/docs/features/modes.rst @@ -0,0 +1,23 @@ +Modes +====== + +Usage +----- +Using this library requires you to state the intention of the program using it. +By default, the library assumes your application is a bot or otherwise automated program, and locks access to certain client-only features. +As we approach the official API, Discord will be creating a divide between bots and clients, so it's important to use the mode appropriate for your program to minimize breaking changes! + +.. warning:: + This is not a complete list, new features will be added in the future. + +Client-Only Features +-------------------- + +- Message Acknowledgement (Message.Acknowledge(), DiscordClient.MessageAcknowledged) +- Message Importing/Exporting +- Message Read States + +Bot-Only Features +----------------- + +- Currently, None diff --git a/docs/features/permissions.rst b/docs/features/permissions.rst index b4922cb53..058fe07cf 100644 --- a/docs/features/permissions.rst +++ b/docs/features/permissions.rst @@ -1,11 +1,11 @@ Permissions -================== +=========== There are two types of permissions: *Channel Permissions* and *Server Permissions*. Channel Permissions ------------------- -Channel Permissions have a set of bools behind them: +Channel Permissions are controlled using a set of flags: ======================= ======= ============== Flag Type Description @@ -49,7 +49,7 @@ Otherwise, you can use a single DualChannelPermissions. Server Permissions ------------------ -Server permisisons are read-only, you cannot change them. You may still access them, however, using User.GetServerPermissions(); +Server Permissions can be accessed by ``Server.GetPermissions(User)``, and updated with ``Server.UpdatePermissions(User, ServerPermissions)`` A user's server permissions also contain the default values for it's channel permissions, so the channel permissions listed above are also valid flags for Server Permissions. There are also a few extra Server Permissions: @@ -57,7 +57,7 @@ A user's server permissions also contain the default values for it's channel per Flag Type Description ======================= ======= ============== BanMembers Server Ban users from the server. -KickMembers Server Kick users from the server. They can stil rejoin. +KickMembers Server Kick users from the server. They can still rejoin. ManageRoles Server Manage roles on the server, and their permissions. ManageChannels Server Manage channels that exist on the server (add, remove them) ManageServer Server Manage the server settings. @@ -69,7 +69,7 @@ Managing permissions for roles is much easier than for users in channels. For ro Example ------- - + .. literalinclude:: /samples/permissions.cs :language: csharp6 - :tab-width: 2 \ No newline at end of file + :tab-width: 2 diff --git a/docs/features/profile.rst b/docs/features/profile.rst deleted file mode 100644 index 4abb4e99b..000000000 --- a/docs/features/profile.rst +++ /dev/null @@ -1,4 +0,0 @@ -|stub| Profile -=================== - -|stub-desc| \ No newline at end of file diff --git a/docs/getting_started.rst b/docs/getting_started.rst index 4ebc4f841..1e8ee492e 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -4,7 +4,7 @@ Getting Started Requirements ------------ -Discord.Net currently requires logging in with a claimed account - anonymous logins are not supported. You can `register for a Discord account here`. +Discord.Net currently requires logging in with a claimed account - anonymous logins are not supported. You can `register for a Discord account here`_. New accounts are also useless when not connected to a server, so you should create an invite code for whatever server you intend to test on using the official Discord client. @@ -17,12 +17,16 @@ You can get Discord.Net from NuGet: * `Discord.Net`_ * `Discord.Net.Commands`_ +* `Discord.Net.Modules`_ + +If you have trouble installing from NuGet, try installing dependencies manually. You can also pull the latest source from `GitHub`_ -.. _Discord.Net: https://discordapp.com/register -.. _Discord.Net.Commands: https://discordapp.com/register -.. _GitHub: https://github.com/RogueException/Discord.Net/> +.. _Discord.Net: https://www.nuget.org/packages/Discord.Net/0.8.1-beta2 +.. _Discord.Net.Commands: https://www.nuget.org/packages/Discord.Net.Commands/0.8.1-beta2 +.. _Discord.Net.Modules: https://www.nuget.org/packages/Discord.Net.Modules/0.8.1-beta2 +.. _GitHub: https://github.com/RogueException/Discord.Net/ Async ----- @@ -39,4 +43,4 @@ Example .. literalinclude:: samples/getting_started.cs :language: csharp6 - :tab-width: 2 \ No newline at end of file + :tab-width: 2 diff --git a/docs/index.rst b/docs/index.rst index c90a2936b..6a9e5889d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,7 +1,7 @@ Discord.Net =========== -Discord.Net is an unofficial C# wrapper around the `Discord chat service`. +Discord.Net is an unofficial C# wrapper around the `Discord Chat Service`. It offers several methods to create automated operations, bots, or even custom clients. Feel free to join us in the `Discord API chat`_. @@ -9,28 +9,29 @@ Feel free to join us in the `Discord API chat`_. .. _Discord chat service: https://discordapp.com .. _Discord API chat: https://discord.gg/0SBTUU1wZTVjAMPx -Warning -------- +.. warn:: -This is an alpha! +This is a beta! -This library has been built thanks to a community effort reverse engineering the Discord client. -As Discord is still in alpha, it may change at any time without notice, breaking this library as well. -Discord.Net itself is also in early development and you will often encounter breaking changes until the official Discord API is released. +This library has been built thanks to a community effort reverse engineering the Discord client. +As the API is still unofficial, it may change at any time without notice, breaking this library as well. +Discord.Net itself is still in development (and is currently undergoing a rewrite) and you may encounter breaking changes throughout development until the official Discord API is released. It is highly recommended that you always use the latest version and please report any bugs you find to our `Discord chat`_. .. _Discord chat: https://discord.gg/0SBTUU1wZTVjAMPx +This Documentation is **currently undergoing a rewrite**. Some pages (marked with a wrench) are not updated, or are not completed yet. + .. toctree:: :caption: Documentation :maxdepth: 2 - - getting_started - features/logging + + getting_started + features/modes + features/logging features/management features/permissions - features/profile features/commands features/voice - features/events \ No newline at end of file + features/events diff --git a/docs/samples/command.cs b/docs/samples/command.cs index 2dfb8e549..d32e216c3 100644 --- a/docs/samples/command.cs +++ b/docs/samples/command.cs @@ -1,16 +1,10 @@ -public enum Permissions -{ - User, - Moderator, - Admin -} - -//Usage: say [text] -client.CreateCommand("say") - .ArgsEqual(1) - .MinPermissions((int)Permissions.User) - .Do(async e => - { - string msg = Format.Normal(e.CommandText); - await _client.SendMessage(e.Channel, msg); - }); \ No newline at end of file +//Since we have setup our CommandChar to be '~', we will run this command by typing ~greet +commands.CreateCommand("greet") //create command greet + .Alias(new string[] { "gr", "hi" }) //add 2 aliases, so it can be run with ~gr and ~hi + .Description("Greets a person.") //add description, it will be shown when ~help is used + .Parameter("GreetedPerson", ParameterType.Required) //as an argument, we have a person we want to greet + .Do(async e => + { + await client.SendMessage(e.Channel, e.User.Name + " greets " + e.GetArg("GreetedPerson")); + //sends a message to channel with the given text + }); \ No newline at end of file diff --git a/docs/samples/command_group.cs b/docs/samples/command_group.cs index b49b87d94..5610723be 100644 --- a/docs/samples/command_group.cs +++ b/docs/samples/command_group.cs @@ -1,20 +1,21 @@ -client.CreateCommandGroup("invites", invites => -{ - invites.DefaultMinPermissions((int)Permissions.Admin); - - //Usage: invites accept [inviteCode] - invites.CreateCommand("accept") - .ArgsEqual(1) - .Do(async e => - { - try - { - await _client.AcceptInvite(e.Args[0]); - await _client.SendMessage(e.Channel, "Invite \"" + e.Args[0] + "\" accepted."); - } - catch (HttpException ex) - { - await _client.SendMessage(e.Channel, "Error: " + ex.Message); - } - }); -}); \ No newline at end of file +//we would run our commands with ~do greet X and ~do bye X +commands.CreateGroup("do", cgb => + { + cgb.CreateCommand("greet") + .Alias(new string[] { "gr", "hi" }) + .Description("Greets a person.") + .Parameter("GreetedPerson", ParameterType.Required) + .Do(async e => + { + await client.SendMessage(e.Channel, e.User.Name + " greets " + e.GetArg("GreetedPerson")); + }); + + cgb.CreateCommand("bye") + .Alias(new string[] { "bb", "gb" }) + .Description("Greets a person.") + .Parameter("GreetedPerson", ParameterType.Required) + .Do(async e => + { + await client.SendMessage(e.Channel, e.User.Name + " says goodbye to " + e.GetArg("GreetedPerson")); + }); + }); \ No newline at end of file diff --git a/docs/samples/command_service_creation.cs b/docs/samples/command_service_creation.cs new file mode 100644 index 000000000..013619ff5 --- /dev/null +++ b/docs/samples/command_service_creation.cs @@ -0,0 +1,9 @@ +//create command service +var commandService = new CommandService(new CommandServiceConfig +{ + CommandChar = '~', // prefix char for commands + HelpMode = HelpMode.Public +}); + +//add command service +var commands = client.AddService(commandService); \ No newline at end of file diff --git a/docs/samples/getting_started.cs b/docs/samples/getting_started.cs index 228ef234e..55f7923a4 100644 --- a/docs/samples/getting_started.cs +++ b/docs/samples/getting_started.cs @@ -11,17 +11,17 @@ class Program client.MessageReceived += async (s, e) => { if (!e.Message.IsAuthor) - await client.SendMessage(e.Channel, e.Message.Text); + await e.Channel.SendMessage(e.Message.Text); }; //Convert our sync method to an async one and block the Main function until the bot disconnects - client.Run(async () => + client.ExecuteAndWait(async () => { //Connect to the Discord server using our email and password await client.Connect("discordtest@email.com", "Password123"); //If we are not a member of any server, use our invite code (made beforehand in the official Discord Client) - if (!client.AllServers.Any()) + if (!client.Servers.Any()) await client.AcceptInvite(client.GetInvite("aaabbbcccdddeee")); }); } diff --git a/docs/samples/logging.cs b/docs/samples/logging.cs index ae5f9f03b..c68b8aded 100644 --- a/docs/samples/logging.cs +++ b/docs/samples/logging.cs @@ -3,13 +3,14 @@ class Program private static DiscordBotClient _client; static void Main(string[] args) { - var client = new DiscordClient(new DiscordClientConfig { - //Warning: Debug mode should only be used for identifying problems. It _will_ slow your application down. - LogLevel = LogMessageSeverity.Debug - }); - client.LogMessage += (s, e) => Console.WriteLine($"[{e.Severity}] {e.Source}: {e.Message}"); - - client.Run(async () => + var client = new DiscordClient(x => + { + LogLevel = LogSeverity.Info + }); + + _client.Log.Message += (s, e) => Console.WriteLine($"[{e.Severity}] {e.Source}: {e.Message}"); + + client.ExecuteAndWait(async () => { await client.Connect("discordtest@email.com", "Password123"); if (!client.Servers.Any()) diff --git a/docs/samples/permissions.cs b/docs/samples/permissions.cs index c09a151d6..419026714 100644 --- a/docs/samples/permissions.cs +++ b/docs/samples/permissions.cs @@ -1,28 +1,14 @@ -// Finding User Permissions +// Find a User's Channel Permissions +var userChannelPermissions = user.GetPermissions(channel); -void FindPermissions(User u, Channel c) -{ - ChannelPermissions cperms = u.GetPermissions(c); - ServerPermissions sperms = u.GetServerPermissions(); -} - -void SetPermissionsChannelPerms(User u, Channel c) -{ - ChannelPermissions allow = new ChannelPermissions(); - ChannelPermissions deny = new ChannelPermissions(); +// Find a User's Server Permissions +var userServerPermissions = user.ServerPermissions(); +var userServerPermissions = server.GetPermissions(user); - allow.Connect = true; - deny.AttachFiles = true; +// Set a User's Channel Permissions (using DualChannelPermissions) - client.SetChannelPermissions(c, u, allow, deny) +var userPerms = user.GetPermissions(channel); +userPerms.ReadMessageHistory = false; +userPerms.AttachFiles = null; +channel.AddPermissionsRule(user, userPerms); } - -void SetPermissionsDualPerms(User u, Channel c) -{ - DualChannelPermissions dual = new DualChannelPermissions(); - dual.ReadMessageHistory = false; - dual.Connect = true; - dual.AttachFiles = null; - - client.SetChannelPermissions(c, u, dual); -} \ No newline at end of file diff --git a/src/Discord.Net.Audio.Net45/Discord.Net.Audio.csproj b/src/Discord.Net.Audio.Net45/Discord.Net.Audio.csproj index 280dc619e..99cfdcc0b 100644 --- a/src/Discord.Net.Audio.Net45/Discord.Net.Audio.csproj +++ b/src/Discord.Net.Audio.Net45/Discord.Net.Audio.csproj @@ -62,8 +62,8 @@ InternalIsSpeakingEventArgs.cs - - Net\VoiceWebSocket.cs + + Net\VoiceSocket.cs Opus\OpusConverter.cs @@ -74,15 +74,15 @@ Opus\OpusEncoder.cs - - SimpleAudioClient.cs - Sodium\SecretBox.cs UserIsTalkingEventArgs.cs + + VirtualClient.cs + VoiceBuffer.cs diff --git a/src/Discord.Net.Audio/AudioClient.cs b/src/Discord.Net.Audio/AudioClient.cs index 9657b89a8..c7d9d182d 100644 --- a/src/Discord.Net.Audio/AudioClient.cs +++ b/src/Discord.Net.Audio/AudioClient.cs @@ -1,9 +1,12 @@ using Discord.API.Client.GatewaySocket; +using Discord.API.Client.Rest; using Discord.Logging; +using Discord.Net.Rest; using Discord.Net.WebSockets; using Newtonsoft.Json; using Nito.AsyncEx; using System; +using System.Diagnostics; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -41,124 +44,175 @@ namespace Discord.Audio } } + private readonly DiscordConfig _config; private readonly AsyncLock _connectionLock; - private readonly JsonSerializer _serializer; - private CancellationTokenSource _cancelTokenSource; + private readonly TaskManager _taskManager; + private ConnectionState _gatewayState; - internal AudioService Service { get; } internal Logger Logger { get; } + public int Id { get; } + public AudioService Service { get; } + public AudioServiceConfig Config { get; } + public RestClient ClientAPI { get; } public GatewaySocket GatewaySocket { get; } - public VoiceWebSocket VoiceSocket { get; } + public VoiceSocket VoiceSocket { get; } + public JsonSerializer Serializer { get; } public Stream OutputStream { get; } - + + public CancellationToken CancelToken { get; private set; } + public string SessionId { get; private set; } + public ConnectionState State => VoiceSocket.State; public Server Server => VoiceSocket.Server; public Channel Channel => VoiceSocket.Channel; - public AudioClient(AudioService service, int clientId, Server server, GatewaySocket gatewaySocket, Logger logger) + public AudioClient(DiscordClient client, Server server, int id) { - Service = service; - _serializer = service.Client.Serializer; - Id = clientId; - GatewaySocket = gatewaySocket; - Logger = logger; - OutputStream = new OutStream(this); + Id = id; + _config = client.Config; + Service = client.Audio(); + Config = Service.Config; + Serializer = client.Serializer; + _gatewayState = (int)ConnectionState.Disconnected; - _connectionLock = new AsyncLock(); + //Logging + Logger = client.Log.CreateLogger($"AudioClient #{id}"); - GatewaySocket.ReceivedDispatch += OnReceivedDispatch; + //Async + _taskManager = new TaskManager(Cleanup, false); + _connectionLock = new AsyncLock(); + CancelToken = new CancellationToken(true); - VoiceSocket = new VoiceWebSocket(service.Client, this, logger); + //Networking + if (Config.EnableMultiserver) + { + ClientAPI = new JsonRestClient(_config, DiscordConfig.ClientAPIUrl, client.Log.CreateLogger($"ClientAPI #{id}")); + GatewaySocket = new GatewaySocket(_config, client.Serializer, client.Log.CreateLogger($"Gateway #{id}")); + GatewaySocket.Connected += (s, e) => + { + if (_gatewayState == ConnectionState.Connecting) + EndGatewayConnect(); + }; + } + else + GatewaySocket = client.GatewaySocket; + GatewaySocket.ReceivedDispatch += (s, e) => OnReceivedEvent(e); + VoiceSocket = new VoiceSocket(_config, Config, client.Serializer, client.Log.CreateLogger($"Voice #{id}")); VoiceSocket.Server = server; - - /*_voiceSocket.Connected += (s, e) => RaiseVoiceConnected(); - _voiceSocket.Disconnected += async (s, e) => - { - _voiceSocket.CurrentServerId; - if (voiceServerId != null) - _gatewaySocket.SendLeaveVoice(voiceServerId.Value); - await _voiceSocket.Disconnect().ConfigureAwait(false); - RaiseVoiceDisconnected(socket.CurrentServerId.Value, e); - if (e.WasUnexpected) - await socket.Reconnect().ConfigureAwait(false); - };*/ - - /*_voiceSocket.IsSpeaking += (s, e) => - { - if (_voiceSocket.State == WebSocketState.Connected) - { - var user = _users[e.UserId, socket.CurrentServerId]; - bool value = e.IsSpeaking; - if (user.IsSpeaking != value) - { - user.IsSpeaking = value; - var channel = _channels[_voiceSocket.CurrentChannelId]; - RaiseUserIsSpeaking(user, channel, value); - if (Config.TrackActivity) - user.UpdateActivity(); - } - } - };*/ - - /*this.Connected += (s, e) => - { - _voiceSocket.ParentCancelToken = _cancelToken; - };*/ + OutputStream = new OutStream(this); } - public async Task Join(Channel channel) - { - if (channel == null) throw new ArgumentNullException(nameof(channel)); - if (channel.Type != ChannelType.Voice) - throw new ArgumentException("Channel must be a voice channel.", nameof(channel)); - if (channel.Server != VoiceSocket.Server) - throw new ArgumentException("This is channel is not part of the current server.", nameof(channel)); - if (channel == VoiceSocket.Channel) return; - if (VoiceSocket.Server == null) - throw new InvalidOperationException("This client has been closed."); - - using (await _connectionLock.LockAsync().ConfigureAwait(false)) + public async Task Connect() + { + if (Config.EnableMultiserver) + await BeginGatewayConnect().ConfigureAwait(false); + else { - VoiceSocket.Channel = channel; - - await Task.Run(() => + var cancelSource = new CancellationTokenSource(); + CancelToken = cancelSource.Token; + await _taskManager.Start(new Task[0], cancelSource).ConfigureAwait(false); + } + } + private async Task BeginGatewayConnect() + { + try + { + using (await _connectionLock.LockAsync().ConfigureAwait(false)) { - SendVoiceUpdate(); - VoiceSocket.WaitForConnection(_cancelTokenSource.Token); - }); + await Disconnect().ConfigureAwait(false); + _taskManager.ClearException(); + + ClientAPI.Token = Service.Client.ClientAPI.Token; + + Stopwatch stopwatch = null; + if (_config.LogLevel >= LogSeverity.Verbose) + stopwatch = Stopwatch.StartNew(); + _gatewayState = ConnectionState.Connecting; + + var cancelSource = new CancellationTokenSource(); + CancelToken = cancelSource.Token; + ClientAPI.CancelToken = CancelToken; + + await GatewaySocket.Connect(ClientAPI, CancelToken).ConfigureAwait(false); + + await _taskManager.Start(new Task[0], cancelSource).ConfigureAwait(false); + GatewaySocket.WaitForConnection(CancelToken); + + if (_config.LogLevel >= LogSeverity.Verbose) + { + stopwatch.Stop(); + double seconds = Math.Round(stopwatch.ElapsedTicks / (double)TimeSpan.TicksPerSecond, 2); + Logger.Verbose($"Connection took {seconds} sec"); + } + } + } + catch (Exception ex) + { + await _taskManager.SignalError(ex).ConfigureAwait(false); + throw; } } + private void EndGatewayConnect() + { + _gatewayState = ConnectionState.Connected; + } - public async Task Connect(bool connectGateway) + public async Task Disconnect() { - using (await _connectionLock.LockAsync().ConfigureAwait(false)) - { - _cancelTokenSource = new CancellationTokenSource(); - var cancelToken = _cancelTokenSource.Token; - VoiceSocket.ParentCancelToken = cancelToken; + await _taskManager.Stop(true).ConfigureAwait(false); + if (Config.EnableMultiserver) + ClientAPI.Token = null; + } + private async Task Cleanup() + { + var oldState = _gatewayState; + _gatewayState = ConnectionState.Disconnecting; - if (connectGateway) + if (Config.EnableMultiserver) + { + if (oldState == ConnectionState.Connected) { - GatewaySocket.ParentCancelToken = cancelToken; - await GatewaySocket.Connect().ConfigureAwait(false); - GatewaySocket.WaitForConnection(cancelToken); + try { await ClientAPI.Send(new LogoutRequest()).ConfigureAwait(false); } + catch (OperationCanceledException) { } } + + await GatewaySocket.Disconnect().ConfigureAwait(false); + ClientAPI.Token = null; } + + var server = VoiceSocket.Server; + VoiceSocket.Server = null; + VoiceSocket.Channel = null; + if (Config.EnableMultiserver) + await Service.RemoveClient(server, this).ConfigureAwait(false); + SendVoiceUpdate(server.Id, null); + + await VoiceSocket.Disconnect().ConfigureAwait(false); + if (Config.EnableMultiserver) + await GatewaySocket.Disconnect().ConfigureAwait(false); + + _gatewayState = (int)ConnectionState.Disconnected; } - public async Task Disconnect() + public async Task Join(Channel channel) { + if (channel == null) throw new ArgumentNullException(nameof(channel)); + if (channel.Type != ChannelType.Voice) + throw new ArgumentException("Channel must be a voice channel.", nameof(channel)); + if (channel == VoiceSocket.Channel) return; + var server = channel.Server; + if (server != VoiceSocket.Server) + throw new ArgumentException("This is channel is not part of the current server.", nameof(channel)); + if (VoiceSocket.Server == null) + throw new InvalidOperationException("This client has been closed."); + + SendVoiceUpdate(channel.Server.Id, channel.Id); using (await _connectionLock.LockAsync().ConfigureAwait(false)) - { - await Service.RemoveClient(VoiceSocket.Server, this).ConfigureAwait(false); - VoiceSocket.Channel = null; - SendVoiceUpdate(); - await VoiceSocket.Disconnect(); - } + await Task.Run(() => VoiceSocket.WaitForConnection(CancelToken)); } - private async void OnReceivedDispatch(object sender, WebSocketEventEventArgs e) + private async void OnReceivedEvent(WebSocketEventEventArgs e) { try { @@ -166,11 +220,11 @@ namespace Discord.Audio { case "VOICE_STATE_UPDATE": { - var data = e.Payload.ToObject(_serializer); + var data = e.Payload.ToObject(Serializer); if (data.GuildId == VoiceSocket.Server?.Id && data.UserId == Service.Client.CurrentUser?.Id) { if (data.ChannelId == null) - await Disconnect(); + await Disconnect().ConfigureAwait(false); else { var channel = Service.Client.GetChannel(data.ChannelId.Value); @@ -179,7 +233,7 @@ namespace Discord.Audio else { Logger.Warning("VOICE_STATE_UPDATE referenced an unknown channel, disconnecting."); - await Disconnect(); + await Disconnect().ConfigureAwait(false); } } } @@ -187,13 +241,16 @@ namespace Discord.Audio break; case "VOICE_SERVER_UPDATE": { - var data = e.Payload.ToObject(_serializer); + var data = e.Payload.ToObject(Serializer); if (data.GuildId == VoiceSocket.Server?.Id) { var client = Service.Client; - VoiceSocket.Token = data.Token; - VoiceSocket.Host = "wss://" + e.Payload.Value("endpoint").Split(':')[0]; - await VoiceSocket.Connect().ConfigureAwait(false); + var id = client.CurrentUser?.Id; + if (id != null) + { + var host = "wss://" + e.Payload.Value("endpoint").Split(':')[0]; + await VoiceSocket.Connect(host, data.Token, id.Value, GatewaySocket.SessionId, CancelToken).ConfigureAwait(false); + } } } break; @@ -205,9 +262,6 @@ namespace Discord.Audio } } - /// Sends a PCM frame to the voice server. Will block until space frees up in the outgoing buffer. - /// PCM frame to send. This must be a single or collection of uncompressed 48Kz monochannel 20ms PCM frames. - /// Number of bytes in this frame. public void Send(byte[] data, int offset, int count) { if (data == null) throw new ArgumentException(nameof(data)); @@ -219,29 +273,22 @@ namespace Discord.Audio VoiceSocket.SendPCMFrames(data, offset, count); } - /// Clears the PCM buffer. public void Clear() { if (VoiceSocket.Server == null) return; //Has been closed VoiceSocket.ClearPCMFrames(); } - - /// Returns a task that completes once the voice output buffer is empty. public void Wait() { if (VoiceSocket.Server == null) return; //Has been closed VoiceSocket.WaitForQueue(); } - private void SendVoiceUpdate() + public void SendVoiceUpdate(ulong? serverId, ulong? channelId) { - var serverId = VoiceSocket.Server?.Id; - if (serverId != null) - { - GatewaySocket.SendUpdateVoice(serverId, VoiceSocket.Channel?.Id, - (Service.Config.Mode | AudioMode.Outgoing) == 0, - (Service.Config.Mode | AudioMode.Incoming) == 0); - } + GatewaySocket.SendUpdateVoice(serverId, channelId, + (Service.Config.Mode | AudioMode.Outgoing) == 0, + (Service.Config.Mode | AudioMode.Incoming) == 0); } } } diff --git a/src/Discord.Net.Audio/AudioClient.cs.old b/src/Discord.Net.Audio/AudioClient.cs.old new file mode 100644 index 000000000..17ee171b0 --- /dev/null +++ b/src/Discord.Net.Audio/AudioClient.cs.old @@ -0,0 +1,242 @@ +using Discord.API.Client.GatewaySocket; +using Discord.Logging; +using Discord.Net.Rest; +using Discord.Net.WebSockets; +using Newtonsoft.Json; +using Nito.AsyncEx; +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Audio +{ + internal class AudioClient : IAudioClient + { + private class OutStream : Stream + { + public override bool CanRead => false; + public override bool CanSeek => false; + public override bool CanWrite => true; + + private readonly AudioClient _client; + + internal OutStream(AudioClient client) + { + _client = client; + } + + public override long Length { get { throw new InvalidOperationException(); } } + public override long Position + { + get { throw new InvalidOperationException(); } + set { throw new InvalidOperationException(); } + } + public override void Flush() { throw new InvalidOperationException(); } + public override long Seek(long offset, SeekOrigin origin) { throw new InvalidOperationException(); } + public override void SetLength(long value) { throw new InvalidOperationException(); } + public override int Read(byte[] buffer, int offset, int count) { throw new InvalidOperationException(); } + public override void Write(byte[] buffer, int offset, int count) + { + _client.Send(buffer, offset, count); + } + } + + private readonly JsonSerializer _serializer; + private readonly bool _ownsGateway; + private TaskManager _taskManager; + private CancellationToken _cancelToken; + + internal AudioService Service { get; } + internal Logger Logger { get; } + public int Id { get; } + public GatewaySocket GatewaySocket { get; } + public VoiceSocket VoiceSocket { get; } + public Stream OutputStream { get; } + + public ConnectionState State => VoiceSocket.State; + public Server Server => VoiceSocket.Server; + public Channel Channel => VoiceSocket.Channel; + + public AudioClient(AudioService service, int clientId, Server server, GatewaySocket gatewaySocket, bool ownsGateway, Logger logger) + { + Service = service; + _serializer = service.Client.Serializer; + Id = clientId; + GatewaySocket = gatewaySocket; + _ownsGateway = ownsGateway; + Logger = logger; + OutputStream = new OutStream(this); + _taskManager = new TaskManager(Cleanup, true); + + GatewaySocket.ReceivedDispatch += OnReceivedDispatch; + + VoiceSocket = new VoiceSocket(service.Client.Config, service.Config, service.Client.Serializer, logger); + VoiceSocket.Server = server; + + /*_voiceSocket.Connected += (s, e) => RaiseVoiceConnected(); + _voiceSocket.Disconnected += async (s, e) => + { + _voiceSocket.CurrentServerId; + if (voiceServerId != null) + _gatewaySocket.SendLeaveVoice(voiceServerId.Value); + await _voiceSocket.Disconnect().ConfigureAwait(false); + RaiseVoiceDisconnected(socket.CurrentServerId.Value, e); + if (e.WasUnexpected) + await socket.Reconnect().ConfigureAwait(false); + };*/ + + /*_voiceSocket.IsSpeaking += (s, e) => + { + if (_voiceSocket.State == WebSocketState.Connected) + { + var user = _users[e.UserId, socket.CurrentServerId]; + bool value = e.IsSpeaking; + if (user.IsSpeaking != value) + { + user.IsSpeaking = value; + var channel = _channels[_voiceSocket.CurrentChannelId]; + RaiseUserIsSpeaking(user, channel, value); + if (Config.TrackActivity) + user.UpdateActivity(); + } + } + };*/ + + /*this.Connected += (s, e) => + { + _voiceSocket.ParentCancelToken = _cancelToken; + };*/ + } + + public async Task Join(Channel channel) + { + if (channel == null) throw new ArgumentNullException(nameof(channel)); + if (channel.Type != ChannelType.Voice) + throw new ArgumentException("Channel must be a voice channel.", nameof(channel)); + if (channel.Server != VoiceSocket.Server) + throw new ArgumentException("This is channel is not part of the current server.", nameof(channel)); + if (channel == VoiceSocket.Channel) return; + if (VoiceSocket.Server == null) + throw new InvalidOperationException("This client has been closed."); + + SendVoiceUpdate(channel.Server.Id, channel.Id); + await Task.Run(() => VoiceSocket.WaitForConnection(_cancelToken)); + } + + public async Task Connect(RestClient rest = null) + { + var cancelSource = new CancellationTokenSource(); + _cancelToken = cancelSource.Token; + + Task[] tasks; + if (rest != null) + tasks = new Task[] { GatewaySocket.Connect(rest, _cancelToken) }; + else + tasks = new Task[0]; + + await _taskManager.Start(tasks, cancelSource); + } + + public Task Disconnect() => _taskManager.Stop(true); + + private async Task Cleanup() + { + var server = VoiceSocket.Server; + VoiceSocket.Server = null; + VoiceSocket.Channel = null; + + await Service.RemoveClient(server, this).ConfigureAwait(false); + SendVoiceUpdate(server.Id, null); + + await VoiceSocket.Disconnect().ConfigureAwait(false); + if (_ownsGateway) + await GatewaySocket.Disconnect().ConfigureAwait(false); + } + + private async void OnReceivedDispatch(object sender, WebSocketEventEventArgs e) + { + try + { + switch (e.Type) + { + case "VOICE_STATE_UPDATE": + { + var data = e.Payload.ToObject(_serializer); + if (data.GuildId == VoiceSocket.Server?.Id && data.UserId == Service.Client.CurrentUser?.Id) + { + if (data.ChannelId == null) + await Disconnect().ConfigureAwait(false); + else + { + var channel = Service.Client.GetChannel(data.ChannelId.Value); + if (channel != null) + VoiceSocket.Channel = channel; + else + { + Logger.Warning("VOICE_STATE_UPDATE referenced an unknown channel, disconnecting."); + await Disconnect().ConfigureAwait(false); + } + } + } + } + break; + case "VOICE_SERVER_UPDATE": + { + var data = e.Payload.ToObject(_serializer); + if (data.GuildId == VoiceSocket.Server?.Id) + { + var client = Service.Client; + var id = client.CurrentUser?.Id; + if (id != null) + { + var host = "wss://" + e.Payload.Value("endpoint").Split(':')[0]; + await VoiceSocket.Connect(host, data.Token, id.Value, GatewaySocket.SessionId, _cancelToken).ConfigureAwait(false); + } + } + } + break; + } + } + catch (Exception ex) + { + Logger.Error($"Error handling {e.Type} event", ex); + } + } + + /// Sends a PCM frame to the voice server. Will block until space frees up in the outgoing buffer. + /// PCM frame to send. This must be a single or collection of uncompressed 48Kz monochannel 20ms PCM frames. + /// Number of bytes in this frame. + public void Send(byte[] data, int offset, int count) + { + if (data == null) throw new ArgumentException(nameof(data)); + if (count < 0) throw new ArgumentOutOfRangeException(nameof(count)); + if (offset < 0) throw new ArgumentOutOfRangeException(nameof(count)); + if (VoiceSocket.Server == null) return; //Has been closed + if (count == 0) return; + + VoiceSocket.SendPCMFrames(data, offset, count); + } + + /// Clears the PCM buffer. + public void Clear() + { + if (VoiceSocket.Server == null) return; //Has been closed + VoiceSocket.ClearPCMFrames(); + } + + /// Returns a task that completes once the voice output buffer is empty. + public void Wait() + { + if (VoiceSocket.Server == null) return; //Has been closed + VoiceSocket.WaitForQueue(); + } + + public void SendVoiceUpdate(ulong? serverId, ulong? channelId) + { + GatewaySocket.SendUpdateVoice(serverId, channelId, + (Service.Config.Mode | AudioMode.Outgoing) == 0, + (Service.Config.Mode | AudioMode.Incoming) == 0); + } + } +} diff --git a/src/Discord.Net.Audio/AudioService.cs b/src/Discord.Net.Audio/AudioService.cs index ec134f884..d5af4ab37 100644 --- a/src/Discord.Net.Audio/AudioService.cs +++ b/src/Discord.Net.Audio/AudioService.cs @@ -1,4 +1,4 @@ -using Discord.Net.WebSockets; +using Nito.AsyncEx; using System; using System.Collections.Concurrent; using System.Linq; @@ -8,8 +8,10 @@ namespace Discord.Audio { public class AudioService : IService { - private AudioClient _defaultClient; - private ConcurrentDictionary _voiceClients; + private readonly AsyncLock _asyncLock; + private AudioClient _defaultClient; //Only used for single server + private VirtualClient _currentClient; //Only used for single server + private ConcurrentDictionary _voiceClients; private ConcurrentDictionary _talkingUsers; private int _nextClientId; @@ -30,22 +32,24 @@ namespace Discord.Audio public AudioService(AudioServiceConfig config) { Config = config; - } + _asyncLock = new AsyncLock(); + + } void IService.Install(DiscordClient client) { Client = client; Config.Lock(); if (Config.EnableMultiserver) - _voiceClients = new ConcurrentDictionary(); + _voiceClients = new ConcurrentDictionary(); else { var logger = Client.Log.CreateLogger("Voice"); - _defaultClient = new SimpleAudioClient(this, 0, logger); + _defaultClient = new AudioClient(Client, null, 0); } _talkingUsers = new ConcurrentDictionary(); - client.Disconnected += async (s, e) => + client.GatewaySocket.Disconnected += async (s, e) => { if (Config.EnableMultiserver) { @@ -75,68 +79,30 @@ namespace Discord.Audio { if (server == null) throw new ArgumentNullException(nameof(server)); - if (!Config.EnableMultiserver) + if (Config.EnableMultiserver) { - if (server == _defaultClient.Server) - return (_defaultClient as SimpleAudioClient).CurrentClient; + AudioClient client; + if (_voiceClients.TryGetValue(server.Id, out client)) + return client; else return null; } else { - IAudioClient client; - if (_voiceClients.TryGetValue(server.Id, out client)) - return client; + if (server == _currentClient.Server) + return _currentClient; else return null; } } - private async Task CreateClient(Server server) - { - var client = _voiceClients.GetOrAdd(server.Id, _ => null); //Placeholder, so we can't have two clients connecting at once - - if (client == null) - { - int id = unchecked(++_nextClientId); - - var gatewayLogger = Client.Log.CreateLogger($"Gateway #{id}"); - var voiceLogger = Client.Log.CreateLogger($"Voice #{id}"); - var gatewaySocket = new GatewaySocket(Client, gatewayLogger); - var voiceClient = new AudioClient(this, id, server, Client.GatewaySocket, voiceLogger); - - await voiceClient.Connect(true).ConfigureAwait(false); - - /*voiceClient.VoiceSocket.FrameReceived += (s, e) => - { - OnFrameReceieved(e); - }; - voiceClient.VoiceSocket.UserIsSpeaking += (s, e) => - { - var user = server.GetUser(e.UserId); - OnUserIsSpeakingUpdated(user, e.IsSpeaking); - };*/ - - //Update the placeholder only it still exists (RemoveClient wasnt called) - if (!_voiceClients.TryUpdate(server.Id, voiceClient, null)) - { - //If it was, cleanup - await voiceClient.Disconnect().ConfigureAwait(false); ; - await gatewaySocket.Disconnect().ConfigureAwait(false); ; - } - } - return client; - } - - //TODO: This isn't threadsafe - internal async Task RemoveClient(Server server, IAudioClient client) + + //Called from AudioClient.Disconnect + internal async Task RemoveClient(Server server, AudioClient client) { - if (Config.EnableMultiserver && server != null) + using (await _asyncLock.LockAsync().ConfigureAwait(false)) { - if (_voiceClients.TryRemove(server.Id, out client)) - { - await client.Disconnect(); - await (client as AudioClient).GatewaySocket.Disconnect(); - } + if (_voiceClients.TryUpdate(server.Id, null, client)) + _voiceClients.TryRemove(server.Id, out client); } } @@ -144,16 +110,48 @@ namespace Discord.Audio { if (channel == null) throw new ArgumentNullException(nameof(channel)); - if (!Config.EnableMultiserver) - { - await (_defaultClient as SimpleAudioClient).Connect(channel, false).ConfigureAwait(false); - return _defaultClient; - } - else + var server = channel.Server; + using (await _asyncLock.LockAsync().ConfigureAwait(false)) { - var client = await CreateClient(channel.Server).ConfigureAwait(false); - await client.Join(channel).ConfigureAwait(false); - return client; + if (Config.EnableMultiserver) + { + AudioClient client; + if (!_voiceClients.TryGetValue(server.Id, out client)) + { + client = new AudioClient(Client, server, unchecked(++_nextClientId)); + _voiceClients[server.Id] = client; + + await client.Connect().ConfigureAwait(false); + + /*voiceClient.VoiceSocket.FrameReceived += (s, e) => + { + OnFrameReceieved(e); + }; + voiceClient.VoiceSocket.UserIsSpeaking += (s, e) => + { + var user = server.GetUser(e.UserId); + OnUserIsSpeakingUpdated(user, e.IsSpeaking); + };*/ + } + + await client.Join(channel).ConfigureAwait(false); + return client; + } + else + { + if (_defaultClient.Server != server) + { + await _defaultClient.Disconnect(); + _defaultClient.VoiceSocket.Server = server; + await _defaultClient.Connect().ConfigureAwait(false); + } + var client = new VirtualClient(_defaultClient, server); + _currentClient = client; + + await client.Join(channel).ConfigureAwait(false); + return client; + } + } } @@ -163,15 +161,18 @@ namespace Discord.Audio if (Config.EnableMultiserver) { - IAudioClient client; + AudioClient client; if (_voiceClients.TryRemove(server.Id, out client)) await client.Disconnect().ConfigureAwait(false); } else { - IAudioClient client = GetClient(server); - if (client != null) - await (_defaultClient as SimpleAudioClient).Leave(client as SimpleAudioClient.VirtualClient).ConfigureAwait(false); + using (await _asyncLock.LockAsync().ConfigureAwait(false)) + { + var client = GetClient(server) as VirtualClient; + if (client != null) + await _defaultClient.Disconnect().ConfigureAwait(false); + } } } } diff --git a/src/Discord.Net.Audio/AudioServiceConfig.cs b/src/Discord.Net.Audio/AudioServiceConfig.cs index b476a6467..a9b4a4b33 100644 --- a/src/Discord.Net.Audio/AudioServiceConfig.cs +++ b/src/Discord.Net.Audio/AudioServiceConfig.cs @@ -12,6 +12,8 @@ namespace Discord.Audio public class AudioServiceConfig { + public const int MaxBitrate = 128; + /// Max time in milliseconds to wait for DiscordAudioClient to connect and initialize. public int ConnectionTimeout { get { return _connectionTimeout; } set { SetValue(ref _connectionTimeout, value); } } private int _connectionTimeout = 30000; @@ -33,8 +35,8 @@ namespace Discord.Audio public int BufferLength { get { return _bufferLength; } set { SetValue(ref _bufferLength, value); } } private int _bufferLength = 1000; - /// Gets or sets the bitrate used (in kbit/s, between 1 and 512 inclusively) for outgoing voice packets. A null value will use default Opus settings. - public int? Bitrate { get { return _bitrate; } set { SetValue(ref _bitrate, value); } } + /// Gets or sets the bitrate used (in kbit/s, between 1 and MaxBitrate inclusively) for outgoing voice packets. A null value will use default Opus settings. + public int? Bitrate { get { return _bitrate; } set { SetValue(ref _bitrate, value); } } private int? _bitrate = null; /// Gets or sets the number of channels (1 or 2) used in both input provided to IAudioClient and output send to Discord. Defaults to 2 (stereo). public int Channels { get { return _channels; } set { SetValue(ref _channels, value); } } diff --git a/src/Discord.Net.Audio/IAudioClient.cs b/src/Discord.Net.Audio/IAudioClient.cs index 838ec945a..71c8783d5 100644 --- a/src/Discord.Net.Audio/IAudioClient.cs +++ b/src/Discord.Net.Audio/IAudioClient.cs @@ -1,16 +1,38 @@ -using System.IO; +using Discord.Net.Rest; +using Discord.Net.WebSockets; +using System.IO; +using System.Threading; using System.Threading.Tasks; namespace Discord.Audio { public interface IAudioClient { + /// Gets the unique identifier for this client. + int Id { get; } + /// Gets the session id for the current connection. + string SessionId { get; } + /// Gets the current state of this client. ConnectionState State { get; } + /// Gets the channel this client is currently a member of. Channel Channel { get; } + /// Gets the server this client is bound to. Server Server { get; } + /// Gets a stream object that wraps the Send() function. Stream OutputStream { get; } + /// Gets a cancellation token that triggers when the client is manually disconnected. + CancellationToken CancelToken { get; } + /// Gets the internal RestClient for the Client API endpoint. + RestClient ClientAPI { get; } + /// Gets the internal WebSocket for the Gateway event stream. + GatewaySocket GatewaySocket { get; } + /// Gets the internal WebSocket for the Voice control stream. + VoiceSocket VoiceSocket { get; } + + /// Moves the client to another channel on the same server. Task Join(Channel channel); + /// Disconnects from the Discord server, canceling any pending requests. Task Disconnect(); /// Sends a PCM frame to the voice server. Will block until space frees up in the outgoing buffer. diff --git a/src/Discord.Net.Audio/Net/VoiceWebSocket.cs b/src/Discord.Net.Audio/Net/VoiceSocket.cs similarity index 90% rename from src/Discord.Net.Audio/Net/VoiceWebSocket.cs rename to src/Discord.Net.Audio/Net/VoiceSocket.cs index c7c4ee0e2..bab928b53 100644 --- a/src/Discord.Net.Audio/Net/VoiceWebSocket.cs +++ b/src/Discord.Net.Audio/Net/VoiceSocket.cs @@ -19,7 +19,7 @@ using System.Threading.Tasks; namespace Discord.Net.WebSockets { - public partial class VoiceWebSocket : WebSocket + public partial class VoiceSocket : WebSocket { private const int MaxOpusSize = 4000; private const string EncryptedMode = "xsalsa20_poly1305"; @@ -27,8 +27,7 @@ namespace Discord.Net.WebSockets private readonly int _targetAudioBufferLength; private readonly ConcurrentDictionary _decoders; - private readonly AudioClient _audioClient; - private readonly AudioServiceConfig _config; + private readonly AudioServiceConfig _audioConfig; private Task _sendTask, _receiveTask; private VoiceBuffer _sendBuffer; private OpusEncoder _encoder; @@ -41,6 +40,8 @@ namespace Discord.Net.WebSockets private ushort _sequence; private string _encryptionMode; private int _ping; + private ulong? _userId; + private string _sessionId; public string Token { get; internal set; } public Server Server { get; internal set; } @@ -57,32 +58,37 @@ namespace Discord.Net.WebSockets internal void OnFrameReceived(ulong userId, ulong channelId, byte[] buffer, int offset, int count) => FrameReceived(this, new InternalFrameEventArgs(userId, channelId, buffer, offset, count)); - internal VoiceWebSocket(DiscordClient client, AudioClient audioClient, Logger logger) - : base(client, logger) + internal VoiceSocket(DiscordConfig config, AudioServiceConfig audioConfig, JsonSerializer serializer, Logger logger) + : base(config, serializer, logger) { - _audioClient = audioClient; - _config = client.Audio().Config; + _audioConfig = audioConfig; _decoders = new ConcurrentDictionary(); - _targetAudioBufferLength = _config.BufferLength / 20; //20 ms frames + _targetAudioBufferLength = _audioConfig.BufferLength / 20; //20 ms frames _encodingBuffer = new byte[MaxOpusSize]; _ssrcMapping = new ConcurrentDictionary(); - _encoder = new OpusEncoder(48000, _config.Channels, 20, _config.Bitrate, OpusApplication.MusicOrMixed); - _sendBuffer = new VoiceBuffer((int)Math.Ceiling(_config.BufferLength / (double)_encoder.FrameLength), _encoder.FrameSize); + _encoder = new OpusEncoder(48000, _audioConfig.Channels, 20, _audioConfig.Bitrate, OpusApplication.MusicOrMixed); + _sendBuffer = new VoiceBuffer((int)Math.Ceiling(_audioConfig.BufferLength / (double)_encoder.FrameLength), _encoder.FrameSize); } - public Task Connect() - => BeginConnect(); + public Task Connect(string host, string token, ulong userId, string sessionId, CancellationToken parentCancelToken) + { + Host = host; + Token = token; + _userId = userId; + _sessionId = sessionId; + return BeginConnect(parentCancelToken); + } private async Task Reconnect() { try { - var cancelToken = ParentCancelToken.Value; - await Task.Delay(_client.Config.ReconnectDelay, cancelToken).ConfigureAwait(false); + var cancelToken = _parentCancelToken; + await Task.Delay(_config.ReconnectDelay, cancelToken).ConfigureAwait(false); while (!cancelToken.IsCancellationRequested) { try { - await Connect().ConfigureAwait(false); + await BeginConnect(_parentCancelToken).ConfigureAwait(false); break; } catch (OperationCanceledException) { throw; } @@ -90,31 +96,35 @@ namespace Discord.Net.WebSockets { Logger.Error("Reconnect failed", ex); //Net is down? We can keep trying to reconnect until the user runs Disconnect() - await Task.Delay(_client.Config.FailedReconnectDelay, cancelToken).ConfigureAwait(false); + await Task.Delay(_config.FailedReconnectDelay, cancelToken).ConfigureAwait(false); } } } catch (OperationCanceledException) { } } - public Task Disconnect() => _taskManager.Stop(true); + public async Task Disconnect() + { + await _taskManager.Stop(true).ConfigureAwait(false); + _userId = null; + } protected override async Task Run() { _udp = new UdpClient(new IPEndPoint(IPAddress.Any, 0)); List tasks = new List(); - if (_config.Mode.HasFlag(AudioMode.Outgoing)) + if (_audioConfig.Mode.HasFlag(AudioMode.Outgoing)) _sendTask = Task.Run(() => SendVoiceAsync(CancelToken)); _receiveTask = Task.Run(() => ReceiveVoiceAsync(CancelToken)); - SendIdentify(); + SendIdentify(_userId.Value, _sessionId); #if !DOTNET5_4 tasks.Add(WatcherAsync()); #endif tasks.AddRange(_engine.GetTasks(CancelToken)); tasks.Add(HeartbeatAsync(CancelToken)); - await _taskManager.Start(tasks, _cancelTokenSource).ConfigureAwait(false); + await _taskManager.Start(tasks, _cancelSource).ConfigureAwait(false); } protected override async Task Cleanup() { @@ -148,7 +158,7 @@ namespace Discord.Net.WebSockets int packetLength, resultOffset, resultLength; IPEndPoint endpoint = new IPEndPoint(IPAddress.Any, 0); - if ((_config.Mode & AudioMode.Incoming) != 0) + if ((_audioConfig.Mode & AudioMode.Incoming) != 0) { decodingBuffer = new byte[MaxOpusSize]; nonce = new byte[24]; @@ -184,7 +194,7 @@ namespace Discord.Net.WebSockets int port = packet[68] | packet[69] << 8; SendSelectProtocol(ip, port); - if ((_config.Mode & AudioMode.Incoming) == 0) + if ((_audioConfig.Mode & AudioMode.Incoming) == 0) return; //We dont need this thread anymore } else @@ -395,7 +405,7 @@ namespace Discord.Net.WebSockets var address = (await Dns.GetHostAddressesAsync(Host.Replace("wss://", "")).ConfigureAwait(false)).FirstOrDefault(); _endpoint = new IPEndPoint(address, payload.Port); - if (_config.EnableEncryption) + if (_audioConfig.EnableEncryption) { if (payload.Modes.Contains(EncryptedMode)) { @@ -467,12 +477,12 @@ namespace Discord.Net.WebSockets public override void SendHeartbeat() => QueueMessage(new HeartbeatCommand()); - public void SendIdentify() + public void SendIdentify(ulong id, string sessionId) => QueueMessage(new IdentifyCommand { GuildId = Server.Id, - UserId = _client.CurrentUser.Id, - SessionId = _client.SessionId, + UserId = id, + SessionId = sessionId, Token = Token }); public void SendSelectProtocol(string externalAddress, int externalPort) diff --git a/src/Discord.Net.Audio/Opus/OpusConverter.cs b/src/Discord.Net.Audio/Opus/OpusConverter.cs index 2a5a4f567..25bda033e 100644 --- a/src/Discord.Net.Audio/Opus/OpusConverter.cs +++ b/src/Discord.Net.Audio/Opus/OpusConverter.cs @@ -4,13 +4,13 @@ using System.Security; namespace Discord.Audio.Opus { - public enum OpusApplication : int + internal enum OpusApplication : int { Voice = 2048, MusicOrMixed = 2049, LowLatency = 2051 } - public enum OpusError : int + internal enum OpusError : int { OK = 0, BadArg = -1, @@ -22,7 +22,7 @@ namespace Discord.Audio.Opus AllocFail = -7 } - public abstract class OpusConverter : IDisposable + internal abstract class OpusConverter : IDisposable { protected enum Ctl : int { diff --git a/src/Discord.Net.Audio/Opus/OpusDecoder.cs b/src/Discord.Net.Audio/Opus/OpusDecoder.cs index 9077ea9cf..d8e6b8087 100644 --- a/src/Discord.Net.Audio/Opus/OpusDecoder.cs +++ b/src/Discord.Net.Audio/Opus/OpusDecoder.cs @@ -2,7 +2,6 @@ namespace Discord.Audio.Opus { - /// Opus codec wrapper. internal class OpusDecoder : OpusConverter { /// Creates a new Opus decoder. diff --git a/src/Discord.Net.Audio/Opus/OpusEncoder.cs b/src/Discord.Net.Audio/Opus/OpusEncoder.cs index 258faf481..be0623c6b 100644 --- a/src/Discord.Net.Audio/Opus/OpusEncoder.cs +++ b/src/Discord.Net.Audio/Opus/OpusEncoder.cs @@ -2,7 +2,6 @@ namespace Discord.Audio.Opus { - /// Opus codec wrapper. internal class OpusEncoder : OpusConverter { /// Gets the bit rate in kbit/s. @@ -19,7 +18,7 @@ namespace Discord.Audio.Opus public OpusEncoder(int samplingRate, int channels, int frameLength, int? bitrate, OpusApplication application) : base(samplingRate, channels, frameLength) { - if (bitrate != null && (bitrate < 1 || bitrate > 512)) + if (bitrate != null && (bitrate < 1 || bitrate > AudioServiceConfig.MaxBitrate)) throw new ArgumentOutOfRangeException(nameof(bitrate)); BitRate = bitrate; diff --git a/src/Discord.Net.Audio/SimpleAudioClient.cs b/src/Discord.Net.Audio/SimpleAudioClient.cs deleted file mode 100644 index b073e2ed3..000000000 --- a/src/Discord.Net.Audio/SimpleAudioClient.cs +++ /dev/null @@ -1,72 +0,0 @@ -using Discord.Logging; -using Nito.AsyncEx; -using System.IO; -using System.Threading.Tasks; - -namespace Discord.Audio -{ - internal class SimpleAudioClient : AudioClient - { - internal class VirtualClient : IAudioClient - { - private readonly SimpleAudioClient _client; - - ConnectionState IAudioClient.State => _client.VoiceSocket.State; - Server IAudioClient.Server => _client.VoiceSocket.Server; - Channel IAudioClient.Channel => _client.VoiceSocket.Channel; - Stream IAudioClient.OutputStream => _client.OutputStream; - - public VirtualClient(SimpleAudioClient client) - { - _client = client; - } - - Task IAudioClient.Disconnect() => _client.Leave(this); - Task IAudioClient.Join(Channel channel) => _client.Join(channel); - - void IAudioClient.Send(byte[] data, int offset, int count) => _client.Send(data, offset, count); - void IAudioClient.Clear() => _client.Clear(); - void IAudioClient.Wait() => _client.Wait(); - } - - private readonly AsyncLock _connectionLock; - - internal VirtualClient CurrentClient { get; private set; } - - public SimpleAudioClient(AudioService service, int id, Logger logger) - : base(service, id, null, service.Client.GatewaySocket, logger) - { - _connectionLock = new AsyncLock(); - } - - //Only disconnects if is current a member of this server - public async Task Leave(VirtualClient client) - { - using (await _connectionLock.LockAsync().ConfigureAwait(false)) - { - if (CurrentClient == client) - { - CurrentClient = null; - await Disconnect().ConfigureAwait(false); - } - } - } - - internal async Task Connect(Channel channel, bool connectGateway) - { - using (await _connectionLock.LockAsync().ConfigureAwait(false)) - { - bool changeServer = channel.Server != VoiceSocket.Server; - if (changeServer || CurrentClient == null) - { - await Disconnect().ConfigureAwait(false); - CurrentClient = new VirtualClient(this); - VoiceSocket.Server = channel.Server; - await Connect(connectGateway).ConfigureAwait(false); - } - await Join(channel).ConfigureAwait(false); - return CurrentClient; - } - } - } -} diff --git a/src/Discord.Net.Audio/VirtualClient.cs b/src/Discord.Net.Audio/VirtualClient.cs new file mode 100644 index 000000000..12d285a0d --- /dev/null +++ b/src/Discord.Net.Audio/VirtualClient.cs @@ -0,0 +1,40 @@ +using Discord.Net.Rest; +using Discord.Net.WebSockets; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Audio +{ + internal class VirtualClient : IAudioClient + { + private readonly AudioClient _client; + + public Server Server { get; } + + public int Id => 0; + public string SessionId => _client.Server == Server ? _client.SessionId : null; + + public ConnectionState State => _client.Server == Server ? _client.State : ConnectionState.Disconnected; + public Channel Channel => _client.Server == Server ? _client.Channel : null; + public Stream OutputStream => _client.Server == Server ? _client.OutputStream : null; + public CancellationToken CancelToken => _client.Server == Server ? _client.CancelToken : CancellationToken.None; + + public RestClient ClientAPI => _client.Server == Server ? _client.ClientAPI : null; + public GatewaySocket GatewaySocket => _client.Server == Server ? _client.GatewaySocket : null; + public VoiceSocket VoiceSocket => _client.Server == Server ? _client.VoiceSocket : null; + + public VirtualClient(AudioClient client, Server server) + { + _client = client; + Server = server; + } + + public Task Disconnect() => _client.Service.Leave(Server); + public Task Join(Channel channel) => _client.Join(channel); + + public void Send(byte[] data, int offset, int count) => _client.Send(data, offset, count); + public void Clear() => _client.Clear(); + public void Wait() => _client.Wait(); + } +} diff --git a/src/Discord.Net.Audio/project.json b/src/Discord.Net.Audio/project.json index be5a82244..c460f0e6c 100644 --- a/src/Discord.Net.Audio/project.json +++ b/src/Discord.Net.Audio/project.json @@ -13,8 +13,8 @@ "contentFiles": [ "libsodium.dll", "opus.dll" ], "compilationOptions": { - "warningsAsErrors": true, - "allowUnsafe": true + "allowUnsafe": true, + "warningsAsErrors": true }, "dependencies": { diff --git a/src/Discord.Net.Commands/Command.cs b/src/Discord.Net.Commands/Command.cs index 8aec0b90f..a8addc1b1 100644 --- a/src/Discord.Net.Commands/Command.cs +++ b/src/Discord.Net.Commands/Command.cs @@ -5,7 +5,8 @@ using System.Threading.Tasks; namespace Discord.Commands { - public sealed class Command + //TODO: Make this more friendly and expose it to be extendable + public class Command { private string[] _aliases; internal CommandParameter[] _parameters; diff --git a/src/Discord.Net.Commands/CommandBuilder.cs b/src/Discord.Net.Commands/CommandBuilder.cs index 6b29cd893..129dc24ab 100644 --- a/src/Discord.Net.Commands/CommandBuilder.cs +++ b/src/Discord.Net.Commands/CommandBuilder.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; namespace Discord.Commands { + //TODO: Make this more friendly and expose it to be extendable public sealed class CommandBuilder { private readonly CommandService _service; @@ -18,17 +19,20 @@ namespace Discord.Commands public CommandService Service => _service; - internal CommandBuilder(CommandService service, Command command, string prefix = "", string category = "", IEnumerable initialChecks = null) + internal CommandBuilder(CommandService service, string text, string prefix = "", string category = "", IEnumerable initialChecks = null) { - _service = service; - _command = command; - _command.Category = category; - _params = new List(); + _service = service; + _prefix = prefix; + + _command = new Command(AppendPrefix(prefix, text)); + _command.Category = category; + if (initialChecks != null) _checks = new List(initialChecks); else _checks = new List(); - _prefix = prefix; + + _params = new List(); _aliases = new List(); _allowRequiredParams = true; @@ -112,7 +116,7 @@ namespace Discord.Commands return prefix; } } - public sealed class CommandGroupBuilder + public class CommandGroupBuilder { private readonly CommandService _service; private readonly string _prefix; @@ -121,10 +125,11 @@ namespace Discord.Commands public CommandService Service => _service; - internal CommandGroupBuilder(CommandService service, string prefix, IEnumerable initialChecks = null) + internal CommandGroupBuilder(CommandService service, string prefix = "", string category = null, IEnumerable initialChecks = null) { _service = service; _prefix = prefix; + _category = category; if (initialChecks != null) _checks = new List(initialChecks); else @@ -145,17 +150,14 @@ namespace Discord.Commands _checks.Add(new GenericPermissionChecker(checkFunc, errorMsg)); } - public CommandGroupBuilder CreateGroup(string cmd, Action config = null) - { - config(new CommandGroupBuilder(_service, CommandBuilder.AppendPrefix(_prefix, cmd), _checks)); + public CommandGroupBuilder CreateGroup(string cmd, Action config) + { + config(new CommandGroupBuilder(_service, CommandBuilder.AppendPrefix(_prefix, cmd), _category, _checks)); return this; } public CommandBuilder CreateCommand() => CreateCommand(""); public CommandBuilder CreateCommand(string cmd) - { - var command = new Command(CommandBuilder.AppendPrefix(_prefix, cmd)); - return new CommandBuilder(_service, command, _prefix, _category, _checks); - } + => new CommandBuilder(_service, cmd, _prefix, _category, _checks); } } diff --git a/src/Discord.Net.Commands/CommandMap.cs b/src/Discord.Net.Commands/CommandMap.cs index 6dd9d2ff1..98decd833 100644 --- a/src/Discord.Net.Commands/CommandMap.cs +++ b/src/Discord.Net.Commands/CommandMap.cs @@ -20,16 +20,20 @@ namespace Discord.Commands public IEnumerable Commands => _commands; public IEnumerable SubGroups => _items.Values; - public CommandMap(CommandMap parent, string name, string fullName) + public CommandMap() + { + _items = new Dictionary(); + _commands = new List(); + _isVisible = false; + _hasNonAliases = false; + _hasSubGroups = false; + } + public CommandMap(CommandMap parent, string name, string fullName) + : this() { _parent = parent; _name = name; _fullName = fullName; - _items = new Dictionary(); - _commands = new List(); - _isVisible = false; - _hasNonAliases = false; - _hasSubGroups = false; } public CommandMap GetItem(string text) diff --git a/src/Discord.Net.Commands/CommandParameter.cs b/src/Discord.Net.Commands/CommandParameter.cs index 413674b78..d7361bef4 100644 --- a/src/Discord.Net.Commands/CommandParameter.cs +++ b/src/Discord.Net.Commands/CommandParameter.cs @@ -11,13 +11,13 @@ /// Catches all remaining text as a single optional parameter. Unparsed } - public sealed class CommandParameter + public class CommandParameter { public string Name { get; } public int Id { get; internal set; } public ParameterType Type { get; } - public CommandParameter(string name, ParameterType type) + internal CommandParameter(string name, ParameterType type) { Name = name; Type = type; diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index d87b61aff..8d5e691bf 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -34,9 +34,9 @@ namespace Discord.Commands Config = config; _allCommands = new List(); - _map = new CommandMap(null, "", ""); + _map = new CommandMap(); _categories = new Dictionary(); - Root = new CommandGroupBuilder(this, "", null); + Root = new CommandGroupBuilder(this); } void IService.Install(DiscordClient client) @@ -309,7 +309,7 @@ namespace Discord.Commands string categoryName = command.Category ?? ""; if (!_categories.TryGetValue(categoryName, out category)) { - category = new CommandMap(null, "", ""); + category = new CommandMap(); _categories.Add(categoryName, category); } diff --git a/src/Discord.Net.Modules/ModuleManager.cs b/src/Discord.Net.Modules/ModuleManager.cs index 091529229..17b2b166b 100644 --- a/src/Discord.Net.Modules/ModuleManager.cs +++ b/src/Discord.Net.Modules/ModuleManager.cs @@ -7,7 +7,7 @@ using System.Linq; namespace Discord.Modules { - public sealed class ModuleManager + public class ModuleManager { public event EventHandler ServerEnabled = delegate { }; public event EventHandler ServerDisabled = delegate { }; @@ -15,31 +15,31 @@ namespace Discord.Modules public event EventHandler ChannelDisabled = delegate { }; public event EventHandler LeftServer = delegate { }; - public event EventHandler ServerUpdated = delegate { }; + public event EventHandler ServerUpdated = delegate { }; public event EventHandler ServerUnavailable = delegate { }; public event EventHandler ServerAvailable = delegate { }; public event EventHandler ChannelCreated = delegate { }; public event EventHandler ChannelDestroyed = delegate { }; - public event EventHandler ChannelUpdated = delegate { }; + public event EventHandler ChannelUpdated = delegate { }; public event EventHandler RoleCreated = delegate { }; - public event EventHandler RoleUpdated = delegate { }; + public event EventHandler RoleUpdated = delegate { }; public event EventHandler RoleDeleted = delegate { }; public event EventHandler UserBanned = delegate { }; public event EventHandler UserJoined = delegate { }; public event EventHandler UserLeft = delegate { }; - public event EventHandler UserUpdated = delegate { }; - public event EventHandler UserPresenceUpdated = delegate { }; - public event EventHandler UserVoiceStateUpdated = delegate { }; + public event EventHandler UserUpdated = delegate { }; + //public event EventHandler UserPresenceUpdated = delegate { }; + //public event EventHandler UserVoiceStateUpdated = delegate { }; public event EventHandler UserUnbanned = delegate { }; - public event EventHandler UserIsTypingUpdated = delegate { }; + public event EventHandler UserIsTyping = delegate { }; public event EventHandler MessageReceived = delegate { }; public event EventHandler MessageSent = delegate { }; public event EventHandler MessageDeleted = delegate { }; - public event EventHandler MessageUpdated = delegate { }; + public event EventHandler MessageUpdated = delegate { }; public event EventHandler MessageReadRemotely = delegate { }; private readonly bool _useServerWhitelist, _useChannelWhitelist, _allowAll, _allowPrivate; @@ -79,11 +79,12 @@ namespace Discord.Modules if (_allowAll || _useServerWhitelist) //Server-only events { client.ChannelCreated += (s, e) => { if (e.Server != null && HasServer(e.Server)) ChannelCreated(s, e); }; - client.UserVoiceStateUpdated += (s, e) => { if (HasServer(e.Server)) UserVoiceStateUpdated(s, e); }; + //TODO: This *is* a channel update if the before/after voice channel is whitelisted + //client.UserVoiceStateUpdated += (s, e) => { if (HasServer(e.Server)) UserVoiceStateUpdated(s, e); }; } client.ChannelDestroyed += (s, e) => { if (HasChannel(e.Channel)) ChannelDestroyed(s, e); }; - client.ChannelUpdated += (s, e) => { if (HasChannel(e.Channel)) ChannelUpdated(s, e); }; + client.ChannelUpdated += (s, e) => { if (HasChannel(e.After)) ChannelUpdated(s, e); }; client.MessageReceived += (s, e) => { if (HasChannel(e.Channel)) MessageReceived(s, e); }; client.MessageSent += (s, e) => { if (HasChannel(e.Channel)) MessageSent(s, e); }; @@ -96,16 +97,16 @@ namespace Discord.Modules client.RoleDeleted += (s, e) => { if (HasIndirectServer(e.Server)) RoleDeleted(s, e); }; client.LeftServer += (s, e) => { if (HasIndirectServer(e.Server)) { DisableServer(e.Server); LeftServer(s, e); } }; - client.ServerUpdated += (s, e) => { if (HasIndirectServer(e.Server)) ServerUpdated(s, e); }; + client.ServerUpdated += (s, e) => { if (HasIndirectServer(e.After)) ServerUpdated(s, e); }; client.ServerUnavailable += (s, e) => { if (HasIndirectServer(e.Server)) ServerUnavailable(s, e); }; client.ServerAvailable += (s, e) => { if (HasIndirectServer(e.Server)) ServerAvailable(s, e); }; client.UserJoined += (s, e) => { if (HasIndirectServer(e.Server)) UserJoined(s, e); }; client.UserLeft += (s, e) => { if (HasIndirectServer(e.Server)) UserLeft(s, e); }; client.UserUpdated += (s, e) => { if (HasIndirectServer(e.Server)) UserUpdated(s, e); }; - client.UserIsTypingUpdated += (s, e) => { if (HasChannel(e.Channel)) UserIsTypingUpdated(s, e); }; + client.UserIsTyping += (s, e) => { if (HasChannel(e.Channel)) UserIsTyping(s, e); }; //TODO: We aren't getting events from UserPresence if AllowPrivate is enabled, but the server we know that user through isn't on the whitelist - client.UserPresenceUpdated += (s, e) => { if (HasIndirectServer(e.Server)) UserPresenceUpdated(s, e); }; + //client.UserPresenceUpdated += (s, e) => { if (HasIndirectServer(e.Server)) UserPresenceUpdated(s, e); }; client.UserBanned += (s, e) => { if (HasIndirectServer(e.Server)) UserBanned(s, e); }; client.UserUnbanned += (s, e) => { if (HasIndirectServer(e.Server)) UserUnbanned(s, e); }; } diff --git a/src/Discord.Net.Net45/Discord.Net.csproj b/src/Discord.Net.Net45/Discord.Net.csproj index 63bb83a09..665efdb95 100644 --- a/src/Discord.Net.Net45/Discord.Net.csproj +++ b/src/Discord.Net.Net45/Discord.Net.csproj @@ -385,18 +385,9 @@ API\Status\Rest\UpcomingMaintenances.cs - - ChannelEventArgs.cs - - - ChannelUserEventArgs.cs - Config.cs - - DisconnectedEventArgs.cs - DiscordClient.cs @@ -406,6 +397,9 @@ DiscordConfig.cs + + DynamicIL.cs + Enums\ChannelType.cs @@ -418,12 +412,66 @@ Enums\PermissionTarget.cs + + Enums\Relative.cs + Enums\StringEnum.cs Enums\UserStatus.cs + + ETF\ETFReader.cs + + + ETF\ETFType.cs + + + ETF\ETFWriter.cs + + + Events\ChannelEventArgs.cs + + + Events\ChannelUpdatedEventArgs.cs + + + Events\ChannelUserEventArgs.cs + + + Events\DisconnectedEventArgs.cs + + + Events\LogMessageEventArgs.cs + + + Events\MessageEventArgs.cs + + + Events\MessageUpdatedEventArgs.cs + + + Events\ProfileUpdatedEventArgs.cs + + + Events\RoleEventArgs.cs + + + Events\RoleUpdatedEventArgs.cs + + + Events\ServerEventArgs.cs + + + Events\ServerUpdatedEventArgs.cs + + + Events\UserEventArgs.cs + + + Events\UserUpdatedEventArgs.cs + Extensions.cs @@ -445,12 +493,6 @@ Logging\LogManager.cs - - LogMessageEventArgs.cs - - - MessageEventArgs.cs - MessageQueue.cs @@ -490,9 +532,15 @@ Net\Rest\CompletedRequestEventArgs.cs + + Net\Rest\ETFRestClient.cs + Net\Rest\IRestEngine.cs + + Net\Rest\JsonRestClient.cs + Net\Rest\RequestEventArgs.cs @@ -532,27 +580,12 @@ Net\WebSockets\WS4NetEngine.cs - - ProfileEventArgs.cs - - - RelativeDirection.cs - - - RoleEventArgs.cs - - - ServerEventArgs.cs - ServiceManager.cs TaskManager.cs - - UserEventArgs.cs - diff --git a/src/Discord.Net/API/Client/Common/Channel.cs b/src/Discord.Net/API/Client/Common/Channel.cs index f54c11e5c..90ed8bf38 100644 --- a/src/Discord.Net/API/Client/Common/Channel.cs +++ b/src/Discord.Net/API/Client/Common/Channel.cs @@ -5,7 +5,7 @@ namespace Discord.API.Client { public class Channel : ChannelReference { - public sealed class PermissionOverwrite + public class PermissionOverwrite { [JsonProperty("type")] public string Type { get; set; } diff --git a/src/Discord.Net/API/Client/Common/ExtendedGuild.cs b/src/Discord.Net/API/Client/Common/ExtendedGuild.cs index 9da0355ca..bfb1971cc 100644 --- a/src/Discord.Net/API/Client/Common/ExtendedGuild.cs +++ b/src/Discord.Net/API/Client/Common/ExtendedGuild.cs @@ -4,7 +4,7 @@ namespace Discord.API.Client { public class ExtendedGuild : Guild { - public sealed class ExtendedMemberInfo : Member + public class ExtendedMemberInfo : Member { [JsonProperty("mute")] public bool? IsServerMuted { get; set; } diff --git a/src/Discord.Net/API/Client/Common/Guild.cs b/src/Discord.Net/API/Client/Common/Guild.cs index bfa047c57..fd4519147 100644 --- a/src/Discord.Net/API/Client/Common/Guild.cs +++ b/src/Discord.Net/API/Client/Common/Guild.cs @@ -6,7 +6,7 @@ namespace Discord.API.Client { public class Guild : GuildReference { - public sealed class EmojiData + public class EmojiData { [JsonProperty("id")] public string Id { get; set; } diff --git a/src/Discord.Net/API/Client/Common/InviteReference.cs b/src/Discord.Net/API/Client/Common/InviteReference.cs index 4c25d9ad3..194165173 100644 --- a/src/Discord.Net/API/Client/Common/InviteReference.cs +++ b/src/Discord.Net/API/Client/Common/InviteReference.cs @@ -4,7 +4,7 @@ namespace Discord.API.Client { public class InviteReference { - public sealed class GuildData : GuildReference + public class GuildData : GuildReference { [JsonProperty("splash_hash")] public string Splash { get; set; } diff --git a/src/Discord.Net/API/Client/Common/MemberPresence.cs b/src/Discord.Net/API/Client/Common/MemberPresence.cs index 283b9a1d1..589ad46c1 100644 --- a/src/Discord.Net/API/Client/Common/MemberPresence.cs +++ b/src/Discord.Net/API/Client/Common/MemberPresence.cs @@ -5,7 +5,7 @@ namespace Discord.API.Client { public class MemberPresence : MemberReference { - public sealed class GameInfo + public class GameInfo { [JsonProperty("name")] public string Name { get; set; } diff --git a/src/Discord.Net/API/Client/Common/Message.cs b/src/Discord.Net/API/Client/Common/Message.cs index f89d81d96..7e9271dc9 100644 --- a/src/Discord.Net/API/Client/Common/Message.cs +++ b/src/Discord.Net/API/Client/Common/Message.cs @@ -5,7 +5,7 @@ namespace Discord.API.Client { public class Message : MessageReference { - public sealed class Attachment + public class Attachment { [JsonProperty("id")] public string Id { get; set; } @@ -18,14 +18,14 @@ namespace Discord.API.Client [JsonProperty("filename")] public string Filename { get; set; } [JsonProperty("width")] - public int Width { get; set; } + public int? Width { get; set; } [JsonProperty("height")] - public int Height { get; set; } + public int? Height { get; set; } } - public sealed class Embed + public class Embed { - public sealed class Reference + public class Reference { [JsonProperty("url")] public string Url { get; set; } @@ -33,25 +33,25 @@ namespace Discord.API.Client public string Name { get; set; } } - public sealed class ThumbnailInfo + public class ThumbnailInfo { [JsonProperty("url")] public string Url { get; set; } [JsonProperty("proxy_url")] public string ProxyUrl { get; set; } [JsonProperty("width")] - public int Width { get; set; } + public int? Width { get; set; } [JsonProperty("height")] - public int Height { get; set; } + public int? Height { get; set; } } - public sealed class VideoInfo + public class VideoInfo { [JsonProperty("url")] public string Url { get; set; } [JsonProperty("width")] - public int Width { get; set; } + public int? Width { get; set; } [JsonProperty("height")] - public int Height { get; set; } + public int? Height { get; set; } } [JsonProperty("url")] diff --git a/src/Discord.Net/API/Client/GatewaySocket/Commands/Heartbeat.cs b/src/Discord.Net/API/Client/GatewaySocket/Commands/Heartbeat.cs index 68a0e98e6..9f3f9cefb 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Commands/Heartbeat.cs +++ b/src/Discord.Net/API/Client/GatewaySocket/Commands/Heartbeat.cs @@ -3,7 +3,7 @@ namespace Discord.API.Client.GatewaySocket { [JsonObject(MemberSerialization.OptIn)] - public sealed class HeartbeatCommand : IWebSocketMessage + public class HeartbeatCommand : IWebSocketMessage { int IWebSocketMessage.OpCode => (int)OpCodes.Heartbeat; object IWebSocketMessage.Payload => EpochTime.GetMilliseconds(); diff --git a/src/Discord.Net/API/Client/GatewaySocket/Commands/Identify.cs b/src/Discord.Net/API/Client/GatewaySocket/Commands/Identify.cs index 76a3a3e6f..2a56143c8 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Commands/Identify.cs +++ b/src/Discord.Net/API/Client/GatewaySocket/Commands/Identify.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; namespace Discord.API.Client.GatewaySocket { [JsonObject(MemberSerialization.OptIn)] - public sealed class IdentifyCommand : IWebSocketMessage + public class IdentifyCommand : IWebSocketMessage { int IWebSocketMessage.OpCode => (int)OpCodes.Identify; object IWebSocketMessage.Payload => this; diff --git a/src/Discord.Net/API/Client/GatewaySocket/Commands/RequestMembers.cs b/src/Discord.Net/API/Client/GatewaySocket/Commands/RequestMembers.cs index 2a2799c12..ea8ab4a75 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Commands/RequestMembers.cs +++ b/src/Discord.Net/API/Client/GatewaySocket/Commands/RequestMembers.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json; namespace Discord.API.Client.GatewaySocket { [JsonObject(MemberSerialization.OptIn)] - public sealed class RequestMembersCommand : IWebSocketMessage + public class RequestMembersCommand : IWebSocketMessage { int IWebSocketMessage.OpCode => (int)OpCodes.RequestGuildMembers; object IWebSocketMessage.Payload => this; diff --git a/src/Discord.Net/API/Client/GatewaySocket/Commands/Resume.cs b/src/Discord.Net/API/Client/GatewaySocket/Commands/Resume.cs index f473369cf..15486e577 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Commands/Resume.cs +++ b/src/Discord.Net/API/Client/GatewaySocket/Commands/Resume.cs @@ -3,7 +3,7 @@ namespace Discord.API.Client.GatewaySocket { [JsonObject(MemberSerialization.OptIn)] - public sealed class ResumeCommand : IWebSocketMessage + public class ResumeCommand : IWebSocketMessage { int IWebSocketMessage.OpCode => (int)OpCodes.Resume; object IWebSocketMessage.Payload => this; diff --git a/src/Discord.Net/API/Client/GatewaySocket/Commands/UpdateStatus.cs b/src/Discord.Net/API/Client/GatewaySocket/Commands/UpdateStatus.cs index 75bfce892..dff18b08c 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Commands/UpdateStatus.cs +++ b/src/Discord.Net/API/Client/GatewaySocket/Commands/UpdateStatus.cs @@ -3,13 +3,13 @@ namespace Discord.API.Client.GatewaySocket { [JsonObject(MemberSerialization.OptIn)] - public sealed class UpdateStatusCommand : IWebSocketMessage + public class UpdateStatusCommand : IWebSocketMessage { int IWebSocketMessage.OpCode => (int)OpCodes.StatusUpdate; object IWebSocketMessage.Payload => this; bool IWebSocketMessage.IsPrivate => false; - public sealed class GameInfo + public class GameInfo { [JsonProperty("name")] public string Name { get; set; } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Commands/UpdateVoice.cs b/src/Discord.Net/API/Client/GatewaySocket/Commands/UpdateVoice.cs index 4eced5dcf..3ccf92c65 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Commands/UpdateVoice.cs +++ b/src/Discord.Net/API/Client/GatewaySocket/Commands/UpdateVoice.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json; namespace Discord.API.Client.GatewaySocket { [JsonObject(MemberSerialization.OptIn)] - public sealed class UpdateVoiceCommand : IWebSocketMessage + public class UpdateVoiceCommand : IWebSocketMessage { int IWebSocketMessage.OpCode => (int)OpCodes.VoiceStateUpdate; object IWebSocketMessage.Payload => this; diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/ChannelCreate.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/ChannelCreate.cs index da4e47aa9..ca26fecc7 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/ChannelCreate.cs +++ b/src/Discord.Net/API/Client/GatewaySocket/Events/ChannelCreate.cs @@ -1,4 +1,4 @@ namespace Discord.API.Client.GatewaySocket { - public sealed class ChannelCreateEvent : Channel { } + public class ChannelCreateEvent : Channel { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/ChannelDelete.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/ChannelDelete.cs index 64452dbe2..2b61a7d78 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/ChannelDelete.cs +++ b/src/Discord.Net/API/Client/GatewaySocket/Events/ChannelDelete.cs @@ -1,4 +1,4 @@ namespace Discord.API.Client.GatewaySocket { - public sealed class ChannelDeleteEvent : Channel { } + public class ChannelDeleteEvent : Channel { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/ChannelUpdate.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/ChannelUpdate.cs index 908c65267..4565ce1bc 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/ChannelUpdate.cs +++ b/src/Discord.Net/API/Client/GatewaySocket/Events/ChannelUpdate.cs @@ -1,4 +1,4 @@ namespace Discord.API.Client.GatewaySocket { - public sealed class ChannelUpdateEvent : Channel { } + public class ChannelUpdateEvent : Channel { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildBanAdd.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/GuildBanAdd.cs index 823e3506a..7ba24473a 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildBanAdd.cs +++ b/src/Discord.Net/API/Client/GatewaySocket/Events/GuildBanAdd.cs @@ -1,4 +1,4 @@ namespace Discord.API.Client.GatewaySocket { - public sealed class GuildBanAddEvent : MemberReference { } + public class GuildBanAddEvent : MemberReference { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildBanRemove.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/GuildBanRemove.cs index a977b8702..a56a98494 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildBanRemove.cs +++ b/src/Discord.Net/API/Client/GatewaySocket/Events/GuildBanRemove.cs @@ -1,4 +1,4 @@ namespace Discord.API.Client.GatewaySocket { - public sealed class GuildBanRemoveEvent : MemberReference { } + public class GuildBanRemoveEvent : MemberReference { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildCreate.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/GuildCreate.cs index 13e3393fc..41c1c71c7 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildCreate.cs +++ b/src/Discord.Net/API/Client/GatewaySocket/Events/GuildCreate.cs @@ -1,4 +1,4 @@ namespace Discord.API.Client.GatewaySocket { - public sealed class GuildCreateEvent : ExtendedGuild { } + public class GuildCreateEvent : ExtendedGuild { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildDelete.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/GuildDelete.cs index 6850cc2b9..cf824c40e 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildDelete.cs +++ b/src/Discord.Net/API/Client/GatewaySocket/Events/GuildDelete.cs @@ -1,4 +1,4 @@ namespace Discord.API.Client.GatewaySocket { - public sealed class GuildDeleteEvent : ExtendedGuild { } + public class GuildDeleteEvent : ExtendedGuild { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildEmojisUpdate.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/GuildEmojisUpdate.cs index 8d2400a7b..06255bdcf 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildEmojisUpdate.cs +++ b/src/Discord.Net/API/Client/GatewaySocket/Events/GuildEmojisUpdate.cs @@ -1,4 +1,4 @@ namespace Discord.API.Client.GatewaySocket.Events { - //public sealed class GuildEmojisUpdateEvent { } + //public class GuildEmojisUpdateEvent { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildIntegrationsUpdate.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/GuildIntegrationsUpdate.cs index d5ac59521..0767b2f8f 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildIntegrationsUpdate.cs +++ b/src/Discord.Net/API/Client/GatewaySocket/Events/GuildIntegrationsUpdate.cs @@ -1,4 +1,4 @@ namespace Discord.API.Client.GatewaySocket { - //public sealed class GuildIntegrationsUpdateEvent { } + //public class GuildIntegrationsUpdateEvent { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildMemberAdd.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/GuildMemberAdd.cs index ab543a41b..4d1d7fed5 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildMemberAdd.cs +++ b/src/Discord.Net/API/Client/GatewaySocket/Events/GuildMemberAdd.cs @@ -1,4 +1,4 @@ namespace Discord.API.Client.GatewaySocket { - public sealed class GuildMemberAddEvent : Member { } + public class GuildMemberAddEvent : Member { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildMemberRemove.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/GuildMemberRemove.cs index cd76cd81a..311186b11 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildMemberRemove.cs +++ b/src/Discord.Net/API/Client/GatewaySocket/Events/GuildMemberRemove.cs @@ -1,4 +1,4 @@ namespace Discord.API.Client.GatewaySocket { - public sealed class GuildMemberRemoveEvent : Member { } + public class GuildMemberRemoveEvent : Member { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildMemberUpdate.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/GuildMemberUpdate.cs index 8e67d5e12..9b56a95b0 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildMemberUpdate.cs +++ b/src/Discord.Net/API/Client/GatewaySocket/Events/GuildMemberUpdate.cs @@ -1,4 +1,4 @@ namespace Discord.API.Client.GatewaySocket { - public sealed class GuildMemberUpdateEvent : Member { } + public class GuildMemberUpdateEvent : Member { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildMembersChunk.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/GuildMembersChunk.cs index 1cb256c79..23be74855 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildMembersChunk.cs +++ b/src/Discord.Net/API/Client/GatewaySocket/Events/GuildMembersChunk.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Client.GatewaySocket { - public sealed class GuildMembersChunkEvent + public class GuildMembersChunkEvent { [JsonProperty("guild_id"), JsonConverter(typeof(LongStringConverter))] public ulong GuildId { get; set; } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildRoleCreate.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/GuildRoleCreate.cs index 28f999d5f..3d8e2f459 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildRoleCreate.cs +++ b/src/Discord.Net/API/Client/GatewaySocket/Events/GuildRoleCreate.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Client.GatewaySocket { - public sealed class GuildRoleCreateEvent + public class GuildRoleCreateEvent { [JsonProperty("guild_id"), JsonConverter(typeof(LongStringConverter))] public ulong GuildId { get; set; } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildRoleDelete.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/GuildRoleDelete.cs index 470420157..2ecd2edc5 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildRoleDelete.cs +++ b/src/Discord.Net/API/Client/GatewaySocket/Events/GuildRoleDelete.cs @@ -1,4 +1,4 @@ namespace Discord.API.Client.GatewaySocket { - public sealed class GuildRoleDeleteEvent : RoleReference { } + public class GuildRoleDeleteEvent : RoleReference { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildRoleUpdate.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/GuildRoleUpdate.cs index 291bab043..e26b65c4d 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildRoleUpdate.cs +++ b/src/Discord.Net/API/Client/GatewaySocket/Events/GuildRoleUpdate.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Client.GatewaySocket { - public sealed class GuildRoleUpdateEvent + public class GuildRoleUpdateEvent { [JsonProperty("guild_id"), JsonConverter(typeof(LongStringConverter))] public ulong GuildId { get; set; } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildUpdate.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/GuildUpdate.cs index ba1665f09..8fc0f1350 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildUpdate.cs +++ b/src/Discord.Net/API/Client/GatewaySocket/Events/GuildUpdate.cs @@ -1,4 +1,4 @@ namespace Discord.API.Client.GatewaySocket { - public sealed class GuildUpdateEvent : Guild { } + public class GuildUpdateEvent : Guild { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/MessageAck.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/MessageAck.cs index ed4048a62..64c106ef5 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/MessageAck.cs +++ b/src/Discord.Net/API/Client/GatewaySocket/Events/MessageAck.cs @@ -1,4 +1,4 @@ namespace Discord.API.Client.GatewaySocket { - public sealed class MessageAckEvent : MessageReference { } + public class MessageAckEvent : MessageReference { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/MessageCreate.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/MessageCreate.cs index 9eb514b61..d6d2ec1cc 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/MessageCreate.cs +++ b/src/Discord.Net/API/Client/GatewaySocket/Events/MessageCreate.cs @@ -1,4 +1,4 @@ namespace Discord.API.Client.GatewaySocket { - public sealed class MessageCreateEvent : Message { } + public class MessageCreateEvent : Message { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/MessageDelete.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/MessageDelete.cs index 9d160eb58..cfc2df7ff 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/MessageDelete.cs +++ b/src/Discord.Net/API/Client/GatewaySocket/Events/MessageDelete.cs @@ -1,4 +1,4 @@ namespace Discord.API.Client.GatewaySocket { - public sealed class MessageDeleteEvent : MessageReference { } + public class MessageDeleteEvent : MessageReference { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/MessageUpdate.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/MessageUpdate.cs index 6fa852749..23521fd93 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/MessageUpdate.cs +++ b/src/Discord.Net/API/Client/GatewaySocket/Events/MessageUpdate.cs @@ -1,4 +1,4 @@ namespace Discord.API.Client.GatewaySocket { - public sealed class MessageUpdateEvent : Message { } + public class MessageUpdateEvent : Message { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/PresenceUpdate.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/PresenceUpdate.cs index 4ecbccf05..c40853336 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/PresenceUpdate.cs +++ b/src/Discord.Net/API/Client/GatewaySocket/Events/PresenceUpdate.cs @@ -1,4 +1,4 @@ namespace Discord.API.Client.GatewaySocket { - public sealed class PresenceUpdateEvent : MemberPresence { } + public class PresenceUpdateEvent : MemberPresence { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/Ready.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/Ready.cs index b17ffd61d..c672a30ae 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/Ready.cs +++ b/src/Discord.Net/API/Client/GatewaySocket/Events/Ready.cs @@ -2,9 +2,9 @@ namespace Discord.API.Client.GatewaySocket { - public sealed class ReadyEvent + public class ReadyEvent { - public sealed class ReadState + public class ReadState { [JsonProperty("id")] public string ChannelId { get; set; } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/Redirect.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/Redirect.cs index 26b2afd2d..fe9d644d4 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/Redirect.cs +++ b/src/Discord.Net/API/Client/GatewaySocket/Events/Redirect.cs @@ -2,7 +2,7 @@ namespace Discord.API.Client.GatewaySocket { - public sealed class RedirectEvent + public class RedirectEvent { [JsonProperty("url")] public string Url { get; set; } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/Resumed.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/Resumed.cs index 7fd608b25..6a50fbe32 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/Resumed.cs +++ b/src/Discord.Net/API/Client/GatewaySocket/Events/Resumed.cs @@ -2,7 +2,7 @@ namespace Discord.API.Client.GatewaySocket { - public sealed class ResumedEvent + public class ResumedEvent { [JsonProperty("heartbeat_interval")] public int HeartbeatInterval { get; set; } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/TypingStart.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/TypingStart.cs index ee8abebe6..484cec1bc 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/TypingStart.cs +++ b/src/Discord.Net/API/Client/GatewaySocket/Events/TypingStart.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Client.GatewaySocket { - public sealed class TypingStartEvent + public class TypingStartEvent { [JsonProperty("user_id"), JsonConverter(typeof(LongStringConverter))] public ulong UserId { get; set; } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/UserSettingsUpdate.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/UserSettingsUpdate.cs index 04effff5e..aad938157 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/UserSettingsUpdate.cs +++ b/src/Discord.Net/API/Client/GatewaySocket/Events/UserSettingsUpdate.cs @@ -1,4 +1,4 @@ namespace Discord.API.Client.GatewaySocket { - //public sealed class UserSettingsUpdateEvent { } + //public class UserSettingsUpdateEvent { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/UserUpdate.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/UserUpdate.cs index 6b43b34b6..3c366310a 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/UserUpdate.cs +++ b/src/Discord.Net/API/Client/GatewaySocket/Events/UserUpdate.cs @@ -1,4 +1,4 @@ namespace Discord.API.Client.GatewaySocket { - public sealed class UserUpdateEvent : User { } + public class UserUpdateEvent : User { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/VoiceServerUpdate.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/VoiceServerUpdate.cs index 0725cc5d8..d305642a1 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/VoiceServerUpdate.cs +++ b/src/Discord.Net/API/Client/GatewaySocket/Events/VoiceServerUpdate.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Client.GatewaySocket { - public sealed class VoiceServerUpdateEvent + public class VoiceServerUpdateEvent { [JsonProperty("guild_id"), JsonConverter(typeof(LongStringConverter))] public ulong GuildId { get; set; } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/VoiceStateUpdate.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/VoiceStateUpdate.cs index 448920b42..f3ba96b17 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/VoiceStateUpdate.cs +++ b/src/Discord.Net/API/Client/GatewaySocket/Events/VoiceStateUpdate.cs @@ -1,4 +1,4 @@ namespace Discord.API.Client.GatewaySocket { - public sealed class VoiceStateUpdateEvent : MemberVoiceState { } + public class VoiceStateUpdateEvent : MemberVoiceState { } } diff --git a/src/Discord.Net/API/Client/ISerializable.cs b/src/Discord.Net/API/Client/ISerializable.cs new file mode 100644 index 000000000..d23dc3c6c --- /dev/null +++ b/src/Discord.Net/API/Client/ISerializable.cs @@ -0,0 +1,9 @@ +using System.IO; + +namespace Discord.API.Client +{ + public interface ISerializable + { + void Write(BinaryWriter writer); + } +} diff --git a/src/Discord.Net/API/Client/IWebSocketMessage.cs b/src/Discord.Net/API/Client/IWebSocketMessage.cs index 715110e51..6f6de535a 100644 --- a/src/Discord.Net/API/Client/IWebSocketMessage.cs +++ b/src/Discord.Net/API/Client/IWebSocketMessage.cs @@ -8,7 +8,7 @@ namespace Discord.API.Client object Payload { get; } bool IsPrivate { get; } } - public sealed class WebSocketMessage + public class WebSocketMessage { [JsonProperty("op")] public int? Operation { get; set; } diff --git a/src/Discord.Net/API/Client/Rest/AcceptInvite.cs b/src/Discord.Net/API/Client/Rest/AcceptInvite.cs index 639a558ec..865e37c2d 100644 --- a/src/Discord.Net/API/Client/Rest/AcceptInvite.cs +++ b/src/Discord.Net/API/Client/Rest/AcceptInvite.cs @@ -3,7 +3,7 @@ namespace Discord.API.Client.Rest { [JsonObject(MemberSerialization.OptIn)] - public sealed class AcceptInviteRequest : IRestRequest + public class AcceptInviteRequest : IRestRequest { string IRestRequest.Method => "POST"; string IRestRequest.Endpoint => $"invite/{InviteId}"; diff --git a/src/Discord.Net/API/Client/Rest/AckMessage.cs b/src/Discord.Net/API/Client/Rest/AckMessage.cs index 55b885e52..1678ed34b 100644 --- a/src/Discord.Net/API/Client/Rest/AckMessage.cs +++ b/src/Discord.Net/API/Client/Rest/AckMessage.cs @@ -3,7 +3,7 @@ namespace Discord.API.Client.Rest { [JsonObject(MemberSerialization.OptIn)] - public sealed class AckMessageRequest : IRestRequest + public class AckMessageRequest : IRestRequest { string IRestRequest.Method => "POST"; string IRestRequest.Endpoint => $"channels/{ChannelId}/messages/{MessageId}/ack"; diff --git a/src/Discord.Net/API/Client/Rest/AddChannelPermission.cs b/src/Discord.Net/API/Client/Rest/AddChannelPermission.cs index 7f878d609..c7a9f57ac 100644 --- a/src/Discord.Net/API/Client/Rest/AddChannelPermission.cs +++ b/src/Discord.Net/API/Client/Rest/AddChannelPermission.cs @@ -4,10 +4,10 @@ using Newtonsoft.Json; namespace Discord.API.Client.Rest { [JsonObject(MemberSerialization.OptIn)] - public sealed class AddChannelPermissionsRequest : IRestRequest + public class AddChannelPermissionsRequest : IRestRequest { string IRestRequest.Method => "PUT"; - string IRestRequest.Endpoint => $"channels/{ChannelId}/permissions"; + string IRestRequest.Endpoint => $"channels/{ChannelId}/permissions/{TargetId}"; object IRestRequest.Payload => this; bool IRestRequest.IsPrivate => false; diff --git a/src/Discord.Net/API/Client/Rest/AddGuildBan.cs b/src/Discord.Net/API/Client/Rest/AddGuildBan.cs index 295e39259..7699a74e4 100644 --- a/src/Discord.Net/API/Client/Rest/AddGuildBan.cs +++ b/src/Discord.Net/API/Client/Rest/AddGuildBan.cs @@ -3,7 +3,7 @@ namespace Discord.API.Client.Rest { [JsonObject(MemberSerialization.OptIn)] - public sealed class AddGuildBanRequest : IRestRequest + public class AddGuildBanRequest : IRestRequest { string IRestRequest.Method => "PUT"; string IRestRequest.Endpoint => $"guilds/{GuildId}/bans/{UserId}?delete-message-days={PruneDays}"; diff --git a/src/Discord.Net/API/Client/Rest/CreateChannel.cs b/src/Discord.Net/API/Client/Rest/CreateChannel.cs index 6a083e315..0dc45bc43 100644 --- a/src/Discord.Net/API/Client/Rest/CreateChannel.cs +++ b/src/Discord.Net/API/Client/Rest/CreateChannel.cs @@ -3,7 +3,7 @@ namespace Discord.API.Client.Rest { [JsonObject(MemberSerialization.OptIn)] - public sealed class CreateChannelRequest : IRestRequest + public class CreateChannelRequest : IRestRequest { string IRestRequest.Method => "POST"; string IRestRequest.Endpoint => $"guilds/{GuildId}/channels"; diff --git a/src/Discord.Net/API/Client/Rest/CreateGuild.cs b/src/Discord.Net/API/Client/Rest/CreateGuild.cs index e7b00934b..baa1f455e 100644 --- a/src/Discord.Net/API/Client/Rest/CreateGuild.cs +++ b/src/Discord.Net/API/Client/Rest/CreateGuild.cs @@ -3,7 +3,7 @@ namespace Discord.API.Client.Rest { [JsonObject(MemberSerialization.OptIn)] - public sealed class CreateGuildRequest : IRestRequest + public class CreateGuildRequest : IRestRequest { string IRestRequest.Method => "POST"; string IRestRequest.Endpoint => $"guilds"; diff --git a/src/Discord.Net/API/Client/Rest/CreateInvite.cs b/src/Discord.Net/API/Client/Rest/CreateInvite.cs index 2d9ae6a50..a55b9c7e9 100644 --- a/src/Discord.Net/API/Client/Rest/CreateInvite.cs +++ b/src/Discord.Net/API/Client/Rest/CreateInvite.cs @@ -3,7 +3,7 @@ namespace Discord.API.Client.Rest { [JsonObject(MemberSerialization.OptIn)] - public sealed class CreateInviteRequest : IRestRequest + public class CreateInviteRequest : IRestRequest { string IRestRequest.Method => "POST"; string IRestRequest.Endpoint => $"channels/{ChannelId}/invites"; diff --git a/src/Discord.Net/API/Client/Rest/CreatePrivateChannel.cs b/src/Discord.Net/API/Client/Rest/CreatePrivateChannel.cs index 526267590..2d413a8d9 100644 --- a/src/Discord.Net/API/Client/Rest/CreatePrivateChannel.cs +++ b/src/Discord.Net/API/Client/Rest/CreatePrivateChannel.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json; namespace Discord.API.Client.Rest { [JsonObject(MemberSerialization.OptIn)] - public sealed class CreatePrivateChannelRequest : IRestRequest + public class CreatePrivateChannelRequest : IRestRequest { string IRestRequest.Method => "POST"; string IRestRequest.Endpoint => $"users/@me/channels"; diff --git a/src/Discord.Net/API/Client/Rest/CreateRole.cs b/src/Discord.Net/API/Client/Rest/CreateRole.cs index 8daa8145c..87715490d 100644 --- a/src/Discord.Net/API/Client/Rest/CreateRole.cs +++ b/src/Discord.Net/API/Client/Rest/CreateRole.cs @@ -3,7 +3,7 @@ namespace Discord.API.Client.Rest { [JsonObject(MemberSerialization.OptIn)] - public sealed class CreateRoleRequest : IRestRequest + public class CreateRoleRequest : IRestRequest { string IRestRequest.Method => "POST"; string IRestRequest.Endpoint => $"guilds/{GuildId}/roles"; diff --git a/src/Discord.Net/API/Client/Rest/DeleteChannel.cs b/src/Discord.Net/API/Client/Rest/DeleteChannel.cs index 0a44d3b3c..6443c2387 100644 --- a/src/Discord.Net/API/Client/Rest/DeleteChannel.cs +++ b/src/Discord.Net/API/Client/Rest/DeleteChannel.cs @@ -3,7 +3,7 @@ namespace Discord.API.Client.Rest { [JsonObject(MemberSerialization.OptIn)] - public sealed class DeleteChannelRequest : IRestRequest + public class DeleteChannelRequest : IRestRequest { string IRestRequest.Method => "DELETE"; string IRestRequest.Endpoint => $"channels/{ChannelId}"; diff --git a/src/Discord.Net/API/Client/Rest/DeleteInvite.cs b/src/Discord.Net/API/Client/Rest/DeleteInvite.cs index b469caa00..5de8b348b 100644 --- a/src/Discord.Net/API/Client/Rest/DeleteInvite.cs +++ b/src/Discord.Net/API/Client/Rest/DeleteInvite.cs @@ -3,7 +3,7 @@ namespace Discord.API.Client.Rest { [JsonObject(MemberSerialization.OptIn)] - public sealed class DeleteInviteRequest : IRestRequest + public class DeleteInviteRequest : IRestRequest { string IRestRequest.Method => "DELETE"; string IRestRequest.Endpoint => $"invite/{InviteCode}"; diff --git a/src/Discord.Net/API/Client/Rest/DeleteMessage.cs b/src/Discord.Net/API/Client/Rest/DeleteMessage.cs index c209baa3a..33921cd1a 100644 --- a/src/Discord.Net/API/Client/Rest/DeleteMessage.cs +++ b/src/Discord.Net/API/Client/Rest/DeleteMessage.cs @@ -3,7 +3,7 @@ namespace Discord.API.Client.Rest { [JsonObject(MemberSerialization.OptIn)] - public sealed class DeleteMessageRequest : IRestRequest + public class DeleteMessageRequest : IRestRequest { string IRestRequest.Method => "DELETE"; string IRestRequest.Endpoint => $"channels/{ChannelId}/messages/{MessageId}"; diff --git a/src/Discord.Net/API/Client/Rest/DeleteRole.cs b/src/Discord.Net/API/Client/Rest/DeleteRole.cs index 3ad327121..650ece9f2 100644 --- a/src/Discord.Net/API/Client/Rest/DeleteRole.cs +++ b/src/Discord.Net/API/Client/Rest/DeleteRole.cs @@ -3,7 +3,7 @@ namespace Discord.API.Client.Rest { [JsonObject(MemberSerialization.OptIn)] - public sealed class DeleteRoleRequest : IRestRequest + public class DeleteRoleRequest : IRestRequest { string IRestRequest.Method => "DELETE"; string IRestRequest.Endpoint => $"guilds/{GuildId}/roles/{RoleId}"; diff --git a/src/Discord.Net/API/Client/Rest/Gateway.cs b/src/Discord.Net/API/Client/Rest/Gateway.cs index e728b46a0..ef9486ca1 100644 --- a/src/Discord.Net/API/Client/Rest/Gateway.cs +++ b/src/Discord.Net/API/Client/Rest/Gateway.cs @@ -3,7 +3,7 @@ namespace Discord.API.Client.Rest { [JsonObject(MemberSerialization.OptIn)] - public sealed class GatewayRequest : IRestRequest + public class GatewayRequest : IRestRequest { string IRestRequest.Method => "GET"; string IRestRequest.Endpoint => $"gateway"; @@ -11,7 +11,7 @@ namespace Discord.API.Client.Rest bool IRestRequest.IsPrivate => false; } - public sealed class GatewayResponse + public class GatewayResponse { [JsonProperty("url")] public string Url { get; set; } diff --git a/src/Discord.Net/API/Client/Rest/GetBans.cs b/src/Discord.Net/API/Client/Rest/GetBans.cs index e4638ce32..ee07cb242 100644 --- a/src/Discord.Net/API/Client/Rest/GetBans.cs +++ b/src/Discord.Net/API/Client/Rest/GetBans.cs @@ -3,7 +3,7 @@ namespace Discord.API.Client.Rest { [JsonObject(MemberSerialization.OptIn)] - public sealed class GetBansRequest : IRestRequest + public class GetBansRequest : IRestRequest { string IRestRequest.Method => "GET"; string IRestRequest.Endpoint => $"guilds/{GuildId}/bans"; diff --git a/src/Discord.Net/API/Client/Rest/GetInvite.cs b/src/Discord.Net/API/Client/Rest/GetInvite.cs index 708a99e46..27de264f0 100644 --- a/src/Discord.Net/API/Client/Rest/GetInvite.cs +++ b/src/Discord.Net/API/Client/Rest/GetInvite.cs @@ -3,7 +3,7 @@ namespace Discord.API.Client.Rest { [JsonObject(MemberSerialization.OptIn)] - public sealed class GetInviteRequest : IRestRequest + public class GetInviteRequest : IRestRequest { string IRestRequest.Method => "GET"; string IRestRequest.Endpoint => $"invite/{InviteCode}"; diff --git a/src/Discord.Net/API/Client/Rest/GetInvites.cs b/src/Discord.Net/API/Client/Rest/GetInvites.cs index dc056ad5a..079c54ef5 100644 --- a/src/Discord.Net/API/Client/Rest/GetInvites.cs +++ b/src/Discord.Net/API/Client/Rest/GetInvites.cs @@ -3,7 +3,7 @@ namespace Discord.API.Client.Rest { [JsonObject(MemberSerialization.OptIn)] - public sealed class GetInvitesRequest : IRestRequest + public class GetInvitesRequest : IRestRequest { string IRestRequest.Method => "GET"; string IRestRequest.Endpoint => $"guilds/{GuildId}/invites"; diff --git a/src/Discord.Net/API/Client/Rest/GetMessages.cs b/src/Discord.Net/API/Client/Rest/GetMessages.cs index c5809ded1..b72b05c8b 100644 --- a/src/Discord.Net/API/Client/Rest/GetMessages.cs +++ b/src/Discord.Net/API/Client/Rest/GetMessages.cs @@ -4,7 +4,7 @@ using System.Text; namespace Discord.API.Client.Rest { [JsonObject(MemberSerialization.OptIn)] - public sealed class GetMessagesRequest : IRestRequest + public class GetMessagesRequest : IRestRequest { string IRestRequest.Method => "GET"; string IRestRequest.Endpoint diff --git a/src/Discord.Net/API/Client/Rest/GetVoiceRegions.cs b/src/Discord.Net/API/Client/Rest/GetVoiceRegions.cs index 307fd01eb..7dc97ef31 100644 --- a/src/Discord.Net/API/Client/Rest/GetVoiceRegions.cs +++ b/src/Discord.Net/API/Client/Rest/GetVoiceRegions.cs @@ -3,7 +3,7 @@ namespace Discord.API.Client.Rest { [JsonObject(MemberSerialization.OptIn)] - public sealed class GetVoiceRegionsRequest : IRestRequest + public class GetVoiceRegionsRequest : IRestRequest { string IRestRequest.Method => "GET"; string IRestRequest.Endpoint => $"voice/regions"; @@ -11,7 +11,7 @@ namespace Discord.API.Client.Rest bool IRestRequest.IsPrivate => false; } - public sealed class GetVoiceRegionsResponse + public class GetVoiceRegionsResponse { [JsonProperty("sample_hostname")] public string Hostname { get; set; } diff --git a/src/Discord.Net/API/Client/Rest/GetWidget.cs b/src/Discord.Net/API/Client/Rest/GetWidget.cs index bdb9b3a99..3b1006358 100644 --- a/src/Discord.Net/API/Client/Rest/GetWidget.cs +++ b/src/Discord.Net/API/Client/Rest/GetWidget.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json; namespace Discord.API.Client.Rest { [JsonObject(MemberSerialization.OptIn)] - public sealed class GetWidgetRequest : IRestRequest + public class GetWidgetRequest : IRestRequest { string IRestRequest.Method => "GET"; string IRestRequest.Endpoint => $"servers/{GuildId}/widget.json"; @@ -19,9 +19,9 @@ namespace Discord.API.Client.Rest } } - public sealed class GetWidgetResponse + public class GetWidgetResponse { - public sealed class Channel + public class Channel { [JsonProperty("id"), JsonConverter(typeof(LongStringConverter))] public ulong Id { get; set; } @@ -30,7 +30,7 @@ namespace Discord.API.Client.Rest [JsonProperty("position")] public int Position { get; set; } } - public sealed class User : UserReference + public class User : UserReference { [JsonProperty("avatar_url")] public string AvatarUrl { get; set; } @@ -39,7 +39,7 @@ namespace Discord.API.Client.Rest [JsonProperty("game")] public UserGame Game { get; set; } } - public sealed class UserGame + public class UserGame { [JsonProperty("id")] public int Id { get; set; } diff --git a/src/Discord.Net/API/Client/Rest/KickMember.cs b/src/Discord.Net/API/Client/Rest/KickMember.cs index db69fbcd9..96804ff6b 100644 --- a/src/Discord.Net/API/Client/Rest/KickMember.cs +++ b/src/Discord.Net/API/Client/Rest/KickMember.cs @@ -3,7 +3,7 @@ namespace Discord.API.Client.Rest { [JsonObject(MemberSerialization.OptIn)] - public sealed class KickMemberRequest : IRestRequest + public class KickMemberRequest : IRestRequest { string IRestRequest.Method => "DELETE"; string IRestRequest.Endpoint => $"guilds/{GuildId}/members/{UserId}"; diff --git a/src/Discord.Net/API/Client/Rest/LeaveGuild.cs b/src/Discord.Net/API/Client/Rest/LeaveGuild.cs index 01dec28e0..6a8b3c0cf 100644 --- a/src/Discord.Net/API/Client/Rest/LeaveGuild.cs +++ b/src/Discord.Net/API/Client/Rest/LeaveGuild.cs @@ -3,7 +3,7 @@ namespace Discord.API.Client.Rest { [JsonObject(MemberSerialization.OptIn)] - public sealed class LeaveGuildRequest : IRestRequest + public class LeaveGuildRequest : IRestRequest { string IRestRequest.Method => "DELETE"; string IRestRequest.Endpoint => $"guilds/{GuildId}"; diff --git a/src/Discord.Net/API/Client/Rest/Login.cs b/src/Discord.Net/API/Client/Rest/Login.cs index f4b0b0c92..ab7efc31b 100644 --- a/src/Discord.Net/API/Client/Rest/Login.cs +++ b/src/Discord.Net/API/Client/Rest/Login.cs @@ -3,7 +3,7 @@ namespace Discord.API.Client.Rest { [JsonObject(MemberSerialization.OptIn)] - public sealed class LoginRequest : IRestRequest + public class LoginRequest : IRestRequest { string IRestRequest.Method => Email != null ? "POST" : "GET"; string IRestRequest.Endpoint => $"auth/login"; @@ -16,7 +16,7 @@ namespace Discord.API.Client.Rest public string Password { get; set; } } - public sealed class LoginResponse + public class LoginResponse { [JsonProperty("token")] public string Token { get; set; } diff --git a/src/Discord.Net/API/Client/Rest/Logout.cs b/src/Discord.Net/API/Client/Rest/Logout.cs index 5df18dbe1..78f8059e5 100644 --- a/src/Discord.Net/API/Client/Rest/Logout.cs +++ b/src/Discord.Net/API/Client/Rest/Logout.cs @@ -3,7 +3,7 @@ namespace Discord.API.Client.Rest { [JsonObject(MemberSerialization.OptIn)] - public sealed class LogoutRequest : IRestRequest + public class LogoutRequest : IRestRequest { string IRestRequest.Method => "POST"; string IRestRequest.Endpoint => $"auth/logout"; diff --git a/src/Discord.Net/API/Client/Rest/PruneMembers.cs b/src/Discord.Net/API/Client/Rest/PruneMembers.cs index ea0b86e41..41771f7d6 100644 --- a/src/Discord.Net/API/Client/Rest/PruneMembers.cs +++ b/src/Discord.Net/API/Client/Rest/PruneMembers.cs @@ -3,7 +3,7 @@ namespace Discord.API.Client.Rest { [JsonObject(MemberSerialization.OptIn)] - public sealed class PruneMembersRequest : IRestRequest + public class PruneMembersRequest : IRestRequest { string IRestRequest.Method => IsSimulation ? "GET" : "POST"; string IRestRequest.Endpoint => $"guilds/{GuildId}/prune?days={Days}"; @@ -21,7 +21,7 @@ namespace Discord.API.Client.Rest } } - public sealed class PruneMembersResponse + public class PruneMembersResponse { [JsonProperty("pruned")] public int Pruned { get; set; } diff --git a/src/Discord.Net/API/Client/Rest/RemoveChannelPermission.cs b/src/Discord.Net/API/Client/Rest/RemoveChannelPermission.cs index 3e6e06e90..c704eadbc 100644 --- a/src/Discord.Net/API/Client/Rest/RemoveChannelPermission.cs +++ b/src/Discord.Net/API/Client/Rest/RemoveChannelPermission.cs @@ -3,7 +3,7 @@ namespace Discord.API.Client.Rest { [JsonObject(MemberSerialization.OptIn)] - public sealed class RemoveChannelPermissionsRequest : IRestRequest + public class RemoveChannelPermissionsRequest : IRestRequest { string IRestRequest.Method => "DELETE"; string IRestRequest.Endpoint => $"channels/{ChannelId}/permissions/{TargetId}"; diff --git a/src/Discord.Net/API/Client/Rest/RemoveGuildBan.cs b/src/Discord.Net/API/Client/Rest/RemoveGuildBan.cs index e126f6bc2..c6d48c944 100644 --- a/src/Discord.Net/API/Client/Rest/RemoveGuildBan.cs +++ b/src/Discord.Net/API/Client/Rest/RemoveGuildBan.cs @@ -3,7 +3,7 @@ namespace Discord.API.Client.Rest { [JsonObject(MemberSerialization.OptIn)] - public sealed class RemoveGuildBanRequest : IRestRequest + public class RemoveGuildBanRequest : IRestRequest { string IRestRequest.Method => "DELETE"; string IRestRequest.Endpoint => $"guilds/{GuildId}/bans/{UserId}"; diff --git a/src/Discord.Net/API/Client/Rest/ReorderChannels.cs b/src/Discord.Net/API/Client/Rest/ReorderChannels.cs index 3f768cf07..c481eda43 100644 --- a/src/Discord.Net/API/Client/Rest/ReorderChannels.cs +++ b/src/Discord.Net/API/Client/Rest/ReorderChannels.cs @@ -5,7 +5,7 @@ using System.Linq; namespace Discord.API.Client.Rest { [JsonObject(MemberSerialization.OptIn)] - public sealed class ReorderChannelsRequest : IRestRequest + public class ReorderChannelsRequest : IRestRequest { string IRestRequest.Method => "PATCH"; string IRestRequest.Endpoint => $"guilds/{GuildId}/channels"; @@ -19,7 +19,7 @@ namespace Discord.API.Client.Rest } bool IRestRequest.IsPrivate => false; - public sealed class Channel + public class Channel { [JsonProperty("id"), JsonConverter(typeof(LongStringConverter))] public ulong Id { get; set; } diff --git a/src/Discord.Net/API/Client/Rest/ReorderRoles.cs b/src/Discord.Net/API/Client/Rest/ReorderRoles.cs index 5eb9a9d11..23d73541f 100644 --- a/src/Discord.Net/API/Client/Rest/ReorderRoles.cs +++ b/src/Discord.Net/API/Client/Rest/ReorderRoles.cs @@ -5,7 +5,7 @@ using System.Linq; namespace Discord.API.Client.Rest { [JsonObject(MemberSerialization.OptIn)] - public sealed class ReorderRolesRequest : IRestRequest + public class ReorderRolesRequest : IRestRequest { string IRestRequest.Method => "PATCH"; string IRestRequest.Endpoint => $"guilds/{GuildId}/roles"; @@ -19,7 +19,7 @@ namespace Discord.API.Client.Rest } bool IRestRequest.IsPrivate => false; - public sealed class Role + public class Role { [JsonProperty("id"), JsonConverter(typeof(LongStringConverter))] public ulong Id { get; set; } diff --git a/src/Discord.Net/API/Client/Rest/SendFile.cs b/src/Discord.Net/API/Client/Rest/SendFile.cs index 7b1a6b084..8d072d0e3 100644 --- a/src/Discord.Net/API/Client/Rest/SendFile.cs +++ b/src/Discord.Net/API/Client/Rest/SendFile.cs @@ -4,7 +4,7 @@ using System.IO; namespace Discord.API.Client.Rest { [JsonObject(MemberSerialization.OptIn)] - public sealed class SendFileRequest : IRestFileRequest + public class SendFileRequest : IRestFileRequest { string IRestRequest.Method => "POST"; string IRestRequest.Endpoint => $"channels/{ChannelId}/messages"; diff --git a/src/Discord.Net/API/Client/Rest/SendIsTyping.cs b/src/Discord.Net/API/Client/Rest/SendIsTyping.cs index abaceb96c..aab017c67 100644 --- a/src/Discord.Net/API/Client/Rest/SendIsTyping.cs +++ b/src/Discord.Net/API/Client/Rest/SendIsTyping.cs @@ -3,7 +3,7 @@ namespace Discord.API.Client.Rest { [JsonObject(MemberSerialization.OptIn)] - public sealed class SendIsTypingRequest : IRestRequest + public class SendIsTypingRequest : IRestRequest { string IRestRequest.Method => "POST"; string IRestRequest.Endpoint => $"channels/{ChannelId}/typing"; diff --git a/src/Discord.Net/API/Client/Rest/SendMessage.cs b/src/Discord.Net/API/Client/Rest/SendMessage.cs index c58d00d9e..6c6d1ae10 100644 --- a/src/Discord.Net/API/Client/Rest/SendMessage.cs +++ b/src/Discord.Net/API/Client/Rest/SendMessage.cs @@ -3,7 +3,7 @@ namespace Discord.API.Client.Rest { [JsonObject(MemberSerialization.OptIn)] - public sealed class SendMessageRequest : IRestRequest + public class SendMessageRequest : IRestRequest { string IRestRequest.Method => "POST"; string IRestRequest.Endpoint => $"channels/{ChannelId}/messages"; diff --git a/src/Discord.Net/API/Client/Rest/UpdateChannel.cs b/src/Discord.Net/API/Client/Rest/UpdateChannel.cs index f09c8ba87..cccd4b096 100644 --- a/src/Discord.Net/API/Client/Rest/UpdateChannel.cs +++ b/src/Discord.Net/API/Client/Rest/UpdateChannel.cs @@ -3,7 +3,7 @@ namespace Discord.API.Client.Rest { [JsonObject(MemberSerialization.OptIn)] - public sealed class UpdateChannelRequest : IRestRequest + public class UpdateChannelRequest : IRestRequest { string IRestRequest.Method => "PATCH"; string IRestRequest.Endpoint => $"channels/{ChannelId}"; diff --git a/src/Discord.Net/API/Client/Rest/UpdateGuild.cs b/src/Discord.Net/API/Client/Rest/UpdateGuild.cs index 163dc437e..4ff530554 100644 --- a/src/Discord.Net/API/Client/Rest/UpdateGuild.cs +++ b/src/Discord.Net/API/Client/Rest/UpdateGuild.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json; namespace Discord.API.Client.Rest { [JsonObject(MemberSerialization.OptIn)] - public sealed class UpdateGuildRequest : IRestRequest + public class UpdateGuildRequest : IRestRequest { string IRestRequest.Method => "PATCH"; string IRestRequest.Endpoint => $"guilds/{GuildId}"; diff --git a/src/Discord.Net/API/Client/Rest/UpdateMember.cs b/src/Discord.Net/API/Client/Rest/UpdateMember.cs index 1c90560ac..0bc5274d0 100644 --- a/src/Discord.Net/API/Client/Rest/UpdateMember.cs +++ b/src/Discord.Net/API/Client/Rest/UpdateMember.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; namespace Discord.API.Client.Rest { [JsonObject(MemberSerialization.OptIn)] - public sealed class UpdateMemberRequest : IRestRequest + public class UpdateMemberRequest : IRestRequest { string IRestRequest.Method => "PATCH"; string IRestRequest.Endpoint => $"guilds/{GuildId}/members/{UserId}"; diff --git a/src/Discord.Net/API/Client/Rest/UpdateMessage.cs b/src/Discord.Net/API/Client/Rest/UpdateMessage.cs index ede0a0797..5b4480a4b 100644 --- a/src/Discord.Net/API/Client/Rest/UpdateMessage.cs +++ b/src/Discord.Net/API/Client/Rest/UpdateMessage.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json; namespace Discord.API.Client.Rest { [JsonObject(MemberSerialization.OptIn)] - public sealed class UpdateMessageRequest : IRestRequest + public class UpdateMessageRequest : IRestRequest { string IRestRequest.Method => "PATCH"; string IRestRequest.Endpoint => $"channels/{ChannelId}/messages/{MessageId}"; diff --git a/src/Discord.Net/API/Client/Rest/UpdateProfile.cs b/src/Discord.Net/API/Client/Rest/UpdateProfile.cs index d89e60983..08f28d868 100644 --- a/src/Discord.Net/API/Client/Rest/UpdateProfile.cs +++ b/src/Discord.Net/API/Client/Rest/UpdateProfile.cs @@ -3,7 +3,7 @@ namespace Discord.API.Client.Rest { [JsonObject(MemberSerialization.OptIn)] - public sealed class UpdateProfileRequest : IRestRequest + public class UpdateProfileRequest : IRestRequest { string IRestRequest.Method => "PATCH"; string IRestRequest.Endpoint => $"users/@me"; diff --git a/src/Discord.Net/API/Client/Rest/UpdateRole.cs b/src/Discord.Net/API/Client/Rest/UpdateRole.cs index 9ebc1e76f..7aac774b7 100644 --- a/src/Discord.Net/API/Client/Rest/UpdateRole.cs +++ b/src/Discord.Net/API/Client/Rest/UpdateRole.cs @@ -3,7 +3,7 @@ namespace Discord.API.Client.Rest { [JsonObject(MemberSerialization.OptIn)] - public sealed class UpdateRoleRequest : IRestRequest + public class UpdateRoleRequest : IRestRequest { string IRestRequest.Method => "PATCH"; string IRestRequest.Endpoint => $"guilds/{GuildId}/roles/{RoleId}"; diff --git a/src/Discord.Net/API/Client/VoiceSocket/Commands/Heartbeat.cs b/src/Discord.Net/API/Client/VoiceSocket/Commands/Heartbeat.cs index a9727dd91..349a8a28b 100644 --- a/src/Discord.Net/API/Client/VoiceSocket/Commands/Heartbeat.cs +++ b/src/Discord.Net/API/Client/VoiceSocket/Commands/Heartbeat.cs @@ -1,6 +1,6 @@ namespace Discord.API.Client.VoiceSocket { - public sealed class HeartbeatCommand : IWebSocketMessage + public class HeartbeatCommand : IWebSocketMessage { int IWebSocketMessage.OpCode => (int)OpCodes.Heartbeat; object IWebSocketMessage.Payload => EpochTime.GetMilliseconds(); diff --git a/src/Discord.Net/API/Client/VoiceSocket/Commands/Identify.cs b/src/Discord.Net/API/Client/VoiceSocket/Commands/Identify.cs index 1836b234c..fbb38b9d0 100644 --- a/src/Discord.Net/API/Client/VoiceSocket/Commands/Identify.cs +++ b/src/Discord.Net/API/Client/VoiceSocket/Commands/Identify.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Client.VoiceSocket { - public sealed class IdentifyCommand : IWebSocketMessage + public class IdentifyCommand : IWebSocketMessage { int IWebSocketMessage.OpCode => (int)OpCodes.Identify; object IWebSocketMessage.Payload => this; diff --git a/src/Discord.Net/API/Client/VoiceSocket/Commands/SelectProtocol.cs b/src/Discord.Net/API/Client/VoiceSocket/Commands/SelectProtocol.cs index aa8e8127b..d860efe45 100644 --- a/src/Discord.Net/API/Client/VoiceSocket/Commands/SelectProtocol.cs +++ b/src/Discord.Net/API/Client/VoiceSocket/Commands/SelectProtocol.cs @@ -2,13 +2,13 @@ namespace Discord.API.Client.VoiceSocket { - public sealed class SelectProtocolCommand : IWebSocketMessage + public class SelectProtocolCommand : IWebSocketMessage { int IWebSocketMessage.OpCode => (int)OpCodes.SelectProtocol; object IWebSocketMessage.Payload => this; bool IWebSocketMessage.IsPrivate => false; - public sealed class Data + public class Data { [JsonProperty("address")] public string Address { get; set; } diff --git a/src/Discord.Net/API/Client/VoiceSocket/Commands/SetSpeaking.cs b/src/Discord.Net/API/Client/VoiceSocket/Commands/SetSpeaking.cs index 13ab00524..6022c4d58 100644 --- a/src/Discord.Net/API/Client/VoiceSocket/Commands/SetSpeaking.cs +++ b/src/Discord.Net/API/Client/VoiceSocket/Commands/SetSpeaking.cs @@ -2,7 +2,7 @@ namespace Discord.API.Client.VoiceSocket { - public sealed class SetSpeakingCommand : IWebSocketMessage + public class SetSpeakingCommand : IWebSocketMessage { int IWebSocketMessage.OpCode => (int)OpCodes.Speaking; object IWebSocketMessage.Payload => this; diff --git a/src/Discord.Net/API/Client/VoiceSocket/Events/Ready.cs b/src/Discord.Net/API/Client/VoiceSocket/Events/Ready.cs index b0fa34c1d..6fdced897 100644 --- a/src/Discord.Net/API/Client/VoiceSocket/Events/Ready.cs +++ b/src/Discord.Net/API/Client/VoiceSocket/Events/Ready.cs @@ -2,7 +2,7 @@ namespace Discord.API.Client.VoiceSocket { - public sealed class ReadyEvent + public class ReadyEvent { [JsonProperty("ssrc")] public uint SSRC { get; set; } diff --git a/src/Discord.Net/API/Client/VoiceSocket/Events/SessionDescription.cs b/src/Discord.Net/API/Client/VoiceSocket/Events/SessionDescription.cs index 13f190cfc..042c5278d 100644 --- a/src/Discord.Net/API/Client/VoiceSocket/Events/SessionDescription.cs +++ b/src/Discord.Net/API/Client/VoiceSocket/Events/SessionDescription.cs @@ -2,7 +2,7 @@ namespace Discord.API.Client.VoiceSocket { - public sealed class SessionDescriptionEvent + public class SessionDescriptionEvent { [JsonProperty("secret_key")] public byte[] SecretKey { get; set; } diff --git a/src/Discord.Net/API/Client/VoiceSocket/Events/Speaking.cs b/src/Discord.Net/API/Client/VoiceSocket/Events/Speaking.cs index b3de0f800..59268c4e6 100644 --- a/src/Discord.Net/API/Client/VoiceSocket/Events/Speaking.cs +++ b/src/Discord.Net/API/Client/VoiceSocket/Events/Speaking.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; namespace Discord.API.Client.VoiceSocket { - public sealed class SpeakingEvent + public class SpeakingEvent { [JsonProperty("user_id"), JsonConverter(typeof(LongStringConverter))] public ulong UserId { get; set; } diff --git a/src/Discord.Net/API/Converters.cs b/src/Discord.Net/API/Converters.cs index 1142e5755..5d80ca99f 100644 --- a/src/Discord.Net/API/Converters.cs +++ b/src/Discord.Net/API/Converters.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; namespace Discord.API.Converters { - public sealed class LongStringConverter : JsonConverter + public class LongStringConverter : JsonConverter { public override bool CanConvert(Type objectType) => objectType == typeof(ulong); @@ -14,7 +14,7 @@ namespace Discord.API.Converters => writer.WriteValue(((ulong)value).ToIdString()); } - public sealed class NullableLongStringConverter : JsonConverter + public class NullableLongStringConverter : JsonConverter { public override bool CanConvert(Type objectType) => objectType == typeof(ulong?); @@ -24,7 +24,7 @@ namespace Discord.API.Converters => writer.WriteValue(((ulong?)value).ToIdString()); } - /*public sealed class LongStringEnumerableConverter : JsonConverter + /*public class LongStringEnumerableConverter : JsonConverter { public override bool CanConvert(Type objectType) => objectType == typeof(IEnumerable); public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) @@ -55,7 +55,7 @@ namespace Discord.API.Converters } }*/ - internal sealed class LongStringArrayConverter : JsonConverter + internal class LongStringArrayConverter : JsonConverter { public override bool CanConvert(Type objectType) => objectType == typeof(IEnumerable); public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) diff --git a/src/Discord.Net/API/Status/Common/StatusResult.cs b/src/Discord.Net/API/Status/Common/StatusResult.cs index 314a180c7..74728c578 100644 --- a/src/Discord.Net/API/Status/Common/StatusResult.cs +++ b/src/Discord.Net/API/Status/Common/StatusResult.cs @@ -5,7 +5,7 @@ namespace Discord.API.Status { public class StatusResult { - public sealed class PageData + public class PageData { [JsonProperty("id")] public string Id { get; set; } @@ -17,7 +17,7 @@ namespace Discord.API.Status public DateTime? UpdatedAt { get; set; } } - public sealed class IncidentData + public class IncidentData { [JsonProperty("id")] public string Id { get; set; } @@ -50,7 +50,7 @@ namespace Discord.API.Status public IncidentUpdateData[] Updates { get; set; } } - public sealed class IncidentUpdateData + public class IncidentUpdateData { [JsonProperty("id")] public string Id { get; set; } diff --git a/src/Discord.Net/API/Status/Rest/ActiveMaintenances.cs b/src/Discord.Net/API/Status/Rest/ActiveMaintenances.cs index 5a8412f96..638c176a5 100644 --- a/src/Discord.Net/API/Status/Rest/ActiveMaintenances.cs +++ b/src/Discord.Net/API/Status/Rest/ActiveMaintenances.cs @@ -3,7 +3,7 @@ namespace Discord.API.Status.Rest { [JsonObject(MemberSerialization.OptIn)] - public sealed class GetActiveMaintenancesRequest : IRestRequest + public class GetActiveMaintenancesRequest : IRestRequest { string IRestRequest.Method => "GET"; string IRestRequest.Endpoint => $"scheduled-maintenances/active.json"; diff --git a/src/Discord.Net/API/Status/Rest/AllIncidents.cs b/src/Discord.Net/API/Status/Rest/AllIncidents.cs index 13f43e022..81a82ce51 100644 --- a/src/Discord.Net/API/Status/Rest/AllIncidents.cs +++ b/src/Discord.Net/API/Status/Rest/AllIncidents.cs @@ -3,7 +3,7 @@ namespace Discord.API.Status.Rest { [JsonObject(MemberSerialization.OptIn)] - public sealed class GetAllIncidentsRequest : IRestRequest + public class GetAllIncidentsRequest : IRestRequest { string IRestRequest.Method => "GET"; string IRestRequest.Endpoint => $"incidents.json"; diff --git a/src/Discord.Net/API/Status/Rest/UnresolvedIncidents.cs b/src/Discord.Net/API/Status/Rest/UnresolvedIncidents.cs index f07de061c..1665dde75 100644 --- a/src/Discord.Net/API/Status/Rest/UnresolvedIncidents.cs +++ b/src/Discord.Net/API/Status/Rest/UnresolvedIncidents.cs @@ -3,7 +3,7 @@ namespace Discord.API.Status.Rest { [JsonObject(MemberSerialization.OptIn)] - public sealed class GetUnresolvedIncidentsRequest : IRestRequest + public class GetUnresolvedIncidentsRequest : IRestRequest { string IRestRequest.Method => "GET"; string IRestRequest.Endpoint => $"incidents/unresolved.json"; diff --git a/src/Discord.Net/API/Status/Rest/UpcomingMaintenances.cs b/src/Discord.Net/API/Status/Rest/UpcomingMaintenances.cs index 769602381..afc812cc9 100644 --- a/src/Discord.Net/API/Status/Rest/UpcomingMaintenances.cs +++ b/src/Discord.Net/API/Status/Rest/UpcomingMaintenances.cs @@ -3,7 +3,7 @@ namespace Discord.API.Status.Rest { [JsonObject(MemberSerialization.OptIn)] - public sealed class GetUpcomingMaintenancesRequest : IRestRequest + public class GetUpcomingMaintenancesRequest : IRestRequest { string IRestRequest.Method => "GET"; string IRestRequest.Endpoint => $"scheduled-maintenances/upcoming.json"; diff --git a/src/Discord.Net/DiscordClient.Events.cs b/src/Discord.Net/DiscordClient.Events.cs index 5b620ef38..5d2577627 100644 --- a/src/Discord.Net/DiscordClient.Events.cs +++ b/src/Discord.Net/DiscordClient.Events.cs @@ -5,45 +5,43 @@ namespace Discord { public partial class DiscordClient { - public event EventHandler Connected = delegate { }; - public event EventHandler Disconnected = delegate { }; + public event EventHandler LoggedIn = delegate { }; + //public event EventHandler LoggedOut = delegate { }; public event EventHandler ChannelCreated = delegate { }; public event EventHandler ChannelDestroyed = delegate { }; - public event EventHandler ChannelUpdated = delegate { }; + public event EventHandler ChannelUpdated = delegate { }; public event EventHandler MessageAcknowledged = delegate { }; public event EventHandler MessageDeleted = delegate { }; public event EventHandler MessageReceived = delegate { }; public event EventHandler MessageSent = delegate { }; - public event EventHandler MessageUpdated = delegate { }; - public event EventHandler ProfileUpdated = delegate { }; + public event EventHandler MessageUpdated = delegate { }; + public event EventHandler ProfileUpdated = delegate { }; public event EventHandler RoleCreated = delegate { }; - public event EventHandler RoleUpdated = delegate { }; + public event EventHandler RoleUpdated = delegate { }; public event EventHandler RoleDeleted = delegate { }; public event EventHandler JoinedServer = delegate { }; public event EventHandler LeftServer = delegate { }; public event EventHandler ServerAvailable = delegate { }; - public event EventHandler ServerUpdated = delegate { }; + public event EventHandler ServerUpdated = delegate { }; public event EventHandler ServerUnavailable = delegate { }; public event EventHandler UserBanned = delegate { }; - public event EventHandler UserIsTypingUpdated = delegate { }; + public event EventHandler UserIsTyping = delegate { }; public event EventHandler UserJoined = delegate { }; public event EventHandler UserLeft = delegate { }; - public event EventHandler UserPresenceUpdated = delegate { }; - public event EventHandler UserUpdated = delegate { }; + public event EventHandler UserUpdated = delegate { }; public event EventHandler UserUnbanned = delegate { }; - public event EventHandler UserVoiceStateUpdated = delegate { }; - private void OnConnected() - => OnEvent(Connected); - private void OnDisconnected(bool wasUnexpected, Exception ex) - => OnEvent(Disconnected, new DisconnectedEventArgs(wasUnexpected, ex)); + private void OnLoggedIn() + => OnEvent(LoggedIn); + /*private void OnLoggedOut(bool wasUnexpected, Exception ex) + => OnEvent(LoggedOut, new DisconnectedEventArgs(wasUnexpected, ex));*/ private void OnChannelCreated(Channel channel) => OnEvent(ChannelCreated, new ChannelEventArgs(channel)); private void OnChannelDestroyed(Channel channel) => OnEvent(ChannelDestroyed, new ChannelEventArgs(channel)); - private void OnChannelUpdated(Channel channel) - => OnEvent(ChannelUpdated, new ChannelEventArgs(channel)); + private void OnChannelUpdated(Channel before, Channel after) + => OnEvent(ChannelUpdated, new ChannelUpdatedEventArgs(before, after)); private void OnMessageAcknowledged(Message msg) => OnEvent(MessageAcknowledged, new MessageEventArgs(msg)); @@ -53,18 +51,18 @@ namespace Discord => OnEvent(MessageReceived, new MessageEventArgs(msg)); internal void OnMessageSent(Message msg) => OnEvent(MessageSent, new MessageEventArgs(msg)); - private void OnMessageUpdated(Message msg) - => OnEvent(MessageUpdated, new MessageEventArgs(msg)); + private void OnMessageUpdated(Message before, Message after) + => OnEvent(MessageUpdated, new MessageUpdatedEventArgs(before, after)); - private void OnProfileUpdated(Profile profile) - => OnEvent(ProfileUpdated, new ProfileEventArgs(profile)); + private void OnProfileUpdated(Profile before, Profile after) + => OnEvent(ProfileUpdated, new ProfileUpdatedEventArgs(before, after)); private void OnRoleCreated(Role role) => OnEvent(RoleCreated, new RoleEventArgs(role)); private void OnRoleDeleted(Role role) => OnEvent(RoleDeleted, new RoleEventArgs(role)); - private void OnRoleUpdated(Role role) - => OnEvent(RoleUpdated, new RoleEventArgs(role)); + private void OnRoleUpdated(Role before, Role after) + => OnEvent(RoleUpdated, new RoleUpdatedEventArgs(before, after)); private void OnJoinedServer(Server server) => OnEvent(JoinedServer, new ServerEventArgs(server)); @@ -72,27 +70,23 @@ namespace Discord => OnEvent(LeftServer, new ServerEventArgs(server)); private void OnServerAvailable(Server server) => OnEvent(ServerAvailable, new ServerEventArgs(server)); - private void OnServerUpdated(Server server) - => OnEvent(ServerUpdated, new ServerEventArgs(server)); + private void OnServerUpdated(Server before, Server after) + => OnEvent(ServerUpdated, new ServerUpdatedEventArgs(before, after)); private void OnServerUnavailable(Server server) => OnEvent(ServerUnavailable, new ServerEventArgs(server)); private void OnUserBanned(User user) => OnEvent(UserBanned, new UserEventArgs(user)); private void OnUserIsTypingUpdated(Channel channel, User user) - => OnEvent(UserIsTypingUpdated, new ChannelUserEventArgs(channel, user)); + => OnEvent(UserIsTyping, new ChannelUserEventArgs(channel, user)); private void OnUserJoined(User user) => OnEvent(UserJoined, new UserEventArgs(user)); private void OnUserLeft(User user) => OnEvent(UserLeft, new UserEventArgs(user)); - private void OnUserPresenceUpdated(User user) - => OnEvent(UserPresenceUpdated, new UserEventArgs(user)); private void OnUserUnbanned(User user) => OnEvent(UserUnbanned, new UserEventArgs(user)); - private void OnUserUpdated(User user) - => OnEvent(UserUpdated, new UserEventArgs(user)); - private void OnUserVoiceStateUpdated(User user) - => OnEvent(UserVoiceStateUpdated, new UserEventArgs(user)); + private void OnUserUpdated(User before, User after) + => OnEvent(UserUpdated, new UserUpdatedEventArgs(before, after)); private void OnEvent(EventHandler handler, T eventArgs, [CallerMemberName] string callerName = null) { diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs index 282606031..5d929a581 100644 --- a/src/Discord.Net/DiscordClient.cs +++ b/src/Discord.Net/DiscordClient.cs @@ -31,7 +31,6 @@ namespace Discord private readonly ConcurrentDictionary _channels; private readonly ConcurrentDictionary _privateChannels; //Key = RecipientId private Dictionary _regions; - private CancellationTokenSource _cancelTokenSource; internal Logger Logger { get; } @@ -136,19 +135,19 @@ namespace Discord }; //Networking - ClientAPI = new RestClient(Config, DiscordConfig.ClientAPIUrl, Log.CreateLogger("ClientAPI")); - StatusAPI = new RestClient(Config, DiscordConfig.StatusAPIUrl, Log.CreateLogger("StatusAPI")); - GatewaySocket = new GatewaySocket(this, Log.CreateLogger("Gateway")); + ClientAPI = new JsonRestClient(Config, DiscordConfig.ClientAPIUrl, Log.CreateLogger("ClientAPI")); + StatusAPI = new JsonRestClient(Config, DiscordConfig.StatusAPIUrl, Log.CreateLogger("StatusAPI")); + GatewaySocket = new GatewaySocket(Config, Serializer, Log.CreateLogger("Gateway")); GatewaySocket.Connected += (s, e) => { if (State == ConnectionState.Connecting) EndConnect(); }; - GatewaySocket.Disconnected += (s, e) => OnDisconnected(e.WasUnexpected, e.Exception); + //GatewaySocket.Disconnected += (s, e) => OnDisconnected(e.WasUnexpected, e.Exception); GatewaySocket.ReceivedDispatch += (s, e) => OnReceivedEvent(e); if (Config.UseMessageQueue) - MessageQueue = new MessageQueue(this, Log.CreateLogger("MessageQueue")); + MessageQueue = new MessageQueue(ClientAPI, Log.CreateLogger("MessageQueue")); //Extensibility Services = new ServiceManager(this); @@ -182,9 +181,7 @@ namespace Discord { using (await _connectionLock.LockAsync().ConfigureAwait(false)) { - if (State != ConnectionState.Disconnected) - await Disconnect().ConfigureAwait(false); - await _taskManager.Stop().ConfigureAwait(false); + await Disconnect().ConfigureAwait(false); _taskManager.ClearException(); Stopwatch stopwatch = null; @@ -193,19 +190,20 @@ namespace Discord State = ConnectionState.Connecting; _disconnectedEvent.Reset(); - _cancelTokenSource = new CancellationTokenSource(); - CancelToken = _cancelTokenSource.Token; - GatewaySocket.ParentCancelToken = CancelToken; + var cancelSource = new CancellationTokenSource(); + CancelToken = cancelSource.Token; + ClientAPI.CancelToken = CancelToken; + StatusAPI.CancelToken = CancelToken; await Login(email, password, token).ConfigureAwait(false); - await GatewaySocket.Connect().ConfigureAwait(false); + await GatewaySocket.Connect(ClientAPI, CancelToken).ConfigureAwait(false); List tasks = new List(); tasks.Add(CancelToken.Wait()); if (Config.UseMessageQueue) tasks.Add(MessageQueue.Run(CancelToken, Config.MessageQueueInterval)); - await _taskManager.Start(tasks, _cancelTokenSource).ConfigureAwait(false); + await _taskManager.Start(tasks, cancelSource).ConfigureAwait(false); GatewaySocket.WaitForConnection(CancelToken); if (Config.LogLevel >= LogSeverity.Verbose) @@ -228,30 +226,28 @@ namespace Discord byte[] cacheKey = null; //Get Token - if (token == null && Config.CacheToken) + if (email != null && Config.CacheToken) { - Rfc2898DeriveBytes deriveBytes = new Rfc2898DeriveBytes(password, - new byte[] { 0x5A, 0x2A, 0xF8, 0xCF, 0x78, 0xD3, 0x7D, 0x0D }); - cacheKey = deriveBytes.GetBytes(16); - tokenPath = GetTokenCachePath(email); - oldToken = LoadToken(tokenPath, cacheKey); - ClientAPI.Token = oldToken; + if (token == null && password != null) + { + Rfc2898DeriveBytes deriveBytes = new Rfc2898DeriveBytes(password, + new byte[] { 0x5A, 0x2A, 0xF8, 0xCF, 0x78, 0xD3, 0x7D, 0x0D }); + cacheKey = deriveBytes.GetBytes(16); + + oldToken = LoadToken(tokenPath, cacheKey); + token = oldToken; + } } - else - ClientAPI.Token = token; - + + ClientAPI.Token = token; var request = new LoginRequest() { Email = email, Password = password }; var response = await ClientAPI.Send(request).ConfigureAwait(false); token = response.Token; - if (Config.CacheToken && token != oldToken) + if (Config.CacheToken && token != oldToken && tokenPath != null) SaveToken(tokenPath, cacheKey, token); - ClientAPI.Token = token; - GatewaySocket.Token = token; - GatewaySocket.SessionId = null; - //Cache other stuff var regionsResponse = (await ClientAPI.Send(new GetVoiceRegionsRequest()).ConfigureAwait(false)); _regions = regionsResponse.Select(x => new Region(x.Id, x.Name, x.Hostname, x.Port, x.Vip)) @@ -262,9 +258,8 @@ namespace Discord State = ConnectionState.Connected; _connectedEvent.Set(); - ClientAPI.CancelToken = CancelToken; SendStatus(); - OnConnected(); + OnLoggedIn(); } /// Disconnects from the Discord server, canceling any pending requests. @@ -275,7 +270,10 @@ namespace Discord State = ConnectionState.Disconnecting; if (oldState == ConnectionState.Connected) - await ClientAPI.Send(new LogoutRequest()).ConfigureAwait(false); + { + try { await ClientAPI.Send(new LogoutRequest()).ConfigureAwait(false); } + catch (OperationCanceledException) { } + } if (Config.UseMessageQueue) MessageQueue.Clear(); @@ -283,8 +281,6 @@ namespace Discord await GatewaySocket.Disconnect().ConfigureAwait(false); ClientAPI.Token = null; - GatewaySocket.Token = null; - GatewaySocket.SessionId = null; _servers.Clear(); _channels.Clear(); @@ -482,8 +478,6 @@ namespace Discord if (Config.LogLevel >= LogSeverity.Verbose) stopwatch = Stopwatch.StartNew(); var data = e.Payload.ToObject(Serializer); - GatewaySocket.StartHeartbeat(data.HeartbeatInterval); - GatewaySocket.SessionId = data.SessionId; SessionId = data.SessionId; PrivateUser = new User(this, data.User.Id, null); PrivateUser.Update(data.User); @@ -510,12 +504,6 @@ namespace Discord } } break; - case "RESUMED": - { - var data = e.Payload.ToObject(Serializer); - GatewaySocket.StartHeartbeat(data.HeartbeatInterval); - } - break; //Servers case "GUILD_CREATE": @@ -546,10 +534,11 @@ namespace Discord var server = GetServer(data.Id); if (server != null) { + var before = Config.EnablePreUpdateEvents ? server.Clone() : null; server.Update(data); if (Config.LogEvents) Logger.Info($"Server Updated: {server.Name}"); - OnServerUpdated(server); + OnServerUpdated(before, server); } else Logger.Warning("GUILD_UPDATE referenced an unknown guild."); @@ -609,10 +598,11 @@ namespace Discord var channel = GetChannel(data.Id); if (channel != null) { + var before = Config.EnablePreUpdateEvents ? channel.Clone() : null; channel.Update(data); if (Config.LogEvents) Logger.Info($"Channel Updated: {channel.Server?.Name ?? "[Private]"}/{channel.Name}"); - OnChannelUpdated(channel); + OnChannelUpdated(before, channel); } else Logger.Warning("CHANNEL_UPDATE referenced an unknown channel."); @@ -660,10 +650,11 @@ namespace Discord var user = server.GetUser(data.User.Id); if (user != null) { + var before = Config.EnablePreUpdateEvents ? user.Clone() : null; user.Update(data); if (Config.LogEvents) Logger.Info($"User Updated: {server.Name}/{user.Name}"); - OnUserUpdated(user); + OnUserUpdated(before, user); } else Logger.Warning("GUILD_MEMBER_UPDATE referenced an unknown user."); @@ -721,7 +712,7 @@ namespace Discord role.Update(data.Data); if (Config.LogEvents) Logger.Info($"Role Created: {server.Name}/{role.Name}"); - OnRoleUpdated(role); + OnRoleCreated(role); } else Logger.Warning("GUILD_ROLE_CREATE referenced an unknown guild."); @@ -736,10 +727,11 @@ namespace Discord var role = server.GetRole(data.Data.Id); if (role != null) { + var before = Config.EnablePreUpdateEvents ? role.Clone() : null; role.Update(data.Data); if (Config.LogEvents) Logger.Info($"Role Updated: {server.Name}/{role.Name}"); - OnRoleUpdated(role); + OnRoleUpdated(before, role); } else Logger.Warning("GUILD_ROLE_UPDATE referenced an unknown role."); @@ -860,10 +852,11 @@ namespace Discord if (channel != null) { var msg = channel.GetMessage(data.Id, data.Author?.Id); + var before = Config.EnablePreUpdateEvents ? msg.Clone() : null; msg.Update(data); if (Config.LogEvents) Logger.Verbose($"Message Update: {channel.Server?.Name ?? "[Private]"}/{channel.Name}"); - OnMessageUpdated(msg); + OnMessageUpdated(before, msg); } else Logger.Warning("MESSAGE_UPDATE referenced an unknown channel."); @@ -886,19 +879,16 @@ namespace Discord break; case "MESSAGE_ACK": { - if (Config.MessageCacheSize > 0) + if (Config.Mode == DiscordMode.Client) { var data = e.Payload.ToObject(Serializer); var channel = GetChannel(data.ChannelId); if (channel != null) { var msg = channel.GetMessage(data.MessageId, null); - if (msg != null) - { - if (Config.LogEvents) - Logger.Verbose($"Message Ack: {channel.Server?.Name ?? "[Private]"}/{channel.Name}"); - OnMessageAcknowledged(msg); - } + if (Config.LogEvents) + Logger.Verbose($"Message Ack: {channel.Server?.Name ?? "[Private]"}/{channel.Name}"); + OnMessageAcknowledged(msg); } else Logger.Warning("MESSAGE_ACK referenced an unknown channel."); @@ -936,9 +926,10 @@ namespace Discord if (user != null) { + var before = Config.EnablePreUpdateEvents ? user.Clone() : null; user.Update(data); //Logger.Verbose($"Presence Updated: {server.Name}/{user.Name}"); - OnUserPresenceUpdated(user); + OnUserUpdated(before, user); } /*else //Occurs when a user leaves a server Logger.Warning("PRESENCE_UPDATE referenced an unknown user.");*/ @@ -982,9 +973,10 @@ namespace Discord var user = server.GetUser(data.UserId); if (user != null) { + var before = Config.EnablePreUpdateEvents ? user.Clone() : null; user.Update(data); //Logger.Verbose($"Voice Updated: {server.Name}/{user.Name}"); - OnUserVoiceStateUpdated(user); + OnUserUpdated(before, user); } /*else //Occurs when a user leaves a server Logger.Warning("VOICE_STATE_UPDATE referenced an unknown user.");*/ @@ -1000,17 +992,22 @@ namespace Discord var data = e.Payload.ToObject(Serializer); if (data.Id == CurrentUser.Id) { + var before = Config.EnablePreUpdateEvents ? CurrentUser.Clone() : null; CurrentUser.Update(data); PrivateUser.Update(data); foreach (var server in _servers) server.Value.CurrentUser.Update(data); if (Config.LogEvents) Logger.Info("Profile Updated"); - OnProfileUpdated(CurrentUser); + OnProfileUpdated(before, CurrentUser); } } break; + //Handled in GatewaySocket + case "RESUMED": + break; + //Ignored case "USER_SETTINGS_UPDATE": case "GUILD_INTEGRATIONS_UPDATE": @@ -1032,17 +1029,13 @@ namespace Discord #endregion #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) + /// Blocking call that will execute the provided async method and wait until the client has been manually stopped. This is mainly intended for use in console applications. + public void ExecuteAndWait(Func asyncAction) { - try - { - AsyncContext.Run(asyncAction); - } - catch (TaskCanceledException) { } + asyncAction().GetAwaiter().GetResult(); _disconnectedEvent.WaitOne(); } - /// Blocking call that will not return until client has been stopped. This is mainly intended for use in console applications. + /// Blocking call and wait until the client has been manually stopped. This is mainly intended for use in console applications. public void Wait() { _disconnectedEvent.WaitOne(); diff --git a/src/Discord.Net/DiscordConfig.cs b/src/Discord.Net/DiscordConfig.cs index f3ff9567f..89f746fcd 100644 --- a/src/Discord.Net/DiscordConfig.cs +++ b/src/Discord.Net/DiscordConfig.cs @@ -57,7 +57,6 @@ namespace Discord private DiscordMode _mode = DiscordMode.Bot; /// User Agent string to use when connecting to Discord. - [JsonIgnore] public string UserAgent { get; private set; } //Rest @@ -103,6 +102,9 @@ namespace Discord /// Gets or sets whether the permissions cache should be used. This makes operations such as User.GetPermissions(Channel), User.ServerPermissions and Channel.Members public bool UsePermissionsCache { get { return _usePermissionsCache; } set { SetValue(ref _usePermissionsCache, value); } } private bool _usePermissionsCache = true; + /// Gets or sets whether the a copy of a model is generated on an update event to allow a user to check which properties changed. + public bool EnablePreUpdateEvents { get { return _enablePreUpdateEvents; } set { SetValue(ref _enablePreUpdateEvents, value); } } + private bool _enablePreUpdateEvents = true; public DiscordConfig() { diff --git a/src/Discord.Net/DynamicIL.cs b/src/Discord.Net/DynamicIL.cs new file mode 100644 index 000000000..bce89424d --- /dev/null +++ b/src/Discord.Net/DynamicIL.cs @@ -0,0 +1,49 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; + +namespace Discord +{ + internal static class DynamicIL + { + public static Action CreateCopyMethod() + { + var method = new DynamicMethod("CopyTo", null, new[] { typeof(T), typeof(T) }, typeof(T), true); + var generator = method.GetILGenerator(); + var typeInfo = typeof(T).GetTypeInfo(); + + typeInfo.ForEachField(f => + { + generator.Emit(OpCodes.Ldarg_1); //Stack: TargetRef + generator.Emit(OpCodes.Ldarg_0); //Stack: TargetRef, SourceRef + generator.Emit(OpCodes.Ldfld, f); //Stack: TargetRef, Value + generator.Emit(OpCodes.Stfld, f); //Stack: + }); + + generator.Emit(OpCodes.Ret); + + return method.CreateDelegate(typeof(Action)) as Action; + } + + public static void ForEachField(this TypeInfo typeInfo, Action func) + { + var baseType = typeInfo.BaseType; + if (baseType != null) + baseType.GetTypeInfo().ForEachField(func); + + foreach (var field in typeInfo.DeclaredFields.Where(x => !x.IsStatic)) + func(field); + } + public static void ForEachProperty(this TypeInfo typeInfo, Action func) + { + var baseType = typeInfo.BaseType; + if (baseType != null) + baseType.GetTypeInfo().ForEachProperty(func); + + foreach (var prop in typeInfo.DeclaredProperties.Where(x => + (!x.CanRead || !x.GetMethod.IsStatic) && (!x.CanWrite || !x.SetMethod.IsStatic))) + func(prop); + } + } +} diff --git a/src/Discord.Net/ETF/ETFReader.cs b/src/Discord.Net/ETF/ETFReader.cs new file mode 100644 index 000000000..d2f33dcbc --- /dev/null +++ b/src/Discord.Net/ETF/ETFReader.cs @@ -0,0 +1,9 @@ +using System; +using System.IO; + +namespace Discord.ETF +{ + public class ETFReader + { + } +} diff --git a/src/Discord.Net/ETF/ETFType.cs b/src/Discord.Net/ETF/ETFType.cs new file mode 100644 index 000000000..53499d5fa --- /dev/null +++ b/src/Discord.Net/ETF/ETFType.cs @@ -0,0 +1,32 @@ +namespace Discord.ETF +{ + public enum ETFType : byte + { + NEW_FLOAT_EXT = 70, + BIT_BINARY_EXT = 77, + ATOM_CACHE_REF = 82, + SMALL_INTEGER_EXT = 97, + INTEGER_EXT = 98, + FLOAT_EXT = 99, + ATOM_EXT = 100, + REFERENCE_EXT = 101, + PORT_EXT = 102, + PID_EXT = 103, + SMALL_TUPLE_EXT = 104, + LARGE_TUPLE_EXT = 105, + NIL_EXT = 106, + STRING_EXT = 107, + LIST_EXT = 108, + BINARY_EXT = 109, + SMALL_BIG_EXT = 110, + LARGE_BIG_EXT = 111, + NEW_FUN_EXT = 112, + EXPORT_EXT = 113, + NEW_REFERENCE_EXT = 114, + SMALL_ATOM_EXT = 115, + MAP_EXT = 116, + FUN_EXT = 117, + ATOM_UTF8_EXT = 118, + SMALL_ATOM_UTF8_EXT = 119 + } +} diff --git a/src/Discord.Net/ETF/ETFWriter.cs b/src/Discord.Net/ETF/ETFWriter.cs new file mode 100644 index 000000000..6222d6427 --- /dev/null +++ b/src/Discord.Net/ETF/ETFWriter.cs @@ -0,0 +1,498 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using System.Text; + +namespace Discord.ETF +{ + public unsafe class ETFWriter : IDisposable + { + private readonly static byte[] _nilBytes = new byte[] { (byte)ETFType.SMALL_ATOM_EXT, 3, (byte)'n', (byte)'i', (byte)'l' }; + private readonly static byte[] _nilExtBytes = new byte[] { (byte)ETFType.NIL_EXT}; + private readonly static byte[] _falseBytes = new byte[] { (byte)ETFType.SMALL_ATOM_EXT, 5, (byte)'f', (byte)'a', (byte)'l', (byte)'s', (byte)'e' }; + private readonly static byte[] _trueBytes = new byte[] { (byte)ETFType.SMALL_ATOM_EXT, 4, (byte)'t', (byte)'r', (byte)'u', (byte)'e' }; + + private readonly static MethodInfo _writeTMethod = typeof(ETFWriter).GetTypeInfo() + .GetDeclaredMethods(nameof(Write)) + .Where(x => x.IsGenericMethodDefinition && x.GetParameters()[0].ParameterType == x.GetGenericArguments()[0]) + .Single(); + private readonly static MethodInfo _writeNullableTMethod = typeof(ETFWriter).GetTypeInfo() + .GetDeclaredMethods(nameof(Write)) + .Where(x => + { + if (!x.IsGenericMethodDefinition) return false; + var p = x.GetParameters()[0].ParameterType.GetTypeInfo(); + return p.IsGenericType && p.GetGenericTypeDefinition() == typeof(Nullable<>); + }) + .Single(); + private readonly static DateTime _epochTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + private readonly Stream _stream; + private readonly byte[] _buffer; + private readonly bool _leaveOpen; + private readonly Encoding _encoding; + private readonly ConcurrentDictionary _serializers, _indirectSerializers; + + public virtual Stream BaseStream + { + get + { + Flush(); + return _stream; + } + } + + public ETFWriter(Stream stream, bool leaveOpen = false) + { + if (stream == null) throw new ArgumentNullException(nameof(stream)); + + _stream = stream; + _leaveOpen = leaveOpen; + _buffer = new byte[11]; + _encoding = Encoding.UTF8; + _serializers = new ConcurrentDictionary(); + _indirectSerializers = new ConcurrentDictionary(); + } + + enum EnumTest1 { A, B, C } + public static byte[] Test() + { + using (var stream = new MemoryStream()) + { + using (var writer = new ETFWriter(stream)) + { + var request = new API.Client.Rest.SendMessageRequest(109384029348) + { + Content = "TestMsg", + Nonce = null, + IsTTS = false + }; + writer.Write(request); + /*writer.Write((EnumTest1?)EnumTest1.C); + writer.Write((object)(EnumTest1?)EnumTest1.C); + writer.Write((EnumTest1?)null); + writer.Write((object)(EnumTest1?)null);*/ + } + return stream.ToArray(); + } + } + + public void Write(bool value) + { + if (value) + _stream.Write(_trueBytes, 0, _trueBytes.Length); + else + _stream.Write(_falseBytes, 0, _falseBytes.Length); + } + + public void Write(sbyte value) => Write((long)value); + public void Write(byte value) => Write((ulong)value); + public void Write(short value) => Write((long)value); + public void Write(ushort value) => Write((ulong)value); + public void Write(int value) => Write((long)value); + public void Write(uint value) => Write((ulong)value); + public void Write(long value) + { + if (value >= byte.MinValue && value <= byte.MaxValue) + { + _buffer[0] = (byte)ETFType.SMALL_INTEGER_EXT; + _buffer[1] = (byte)value; + _stream.Write(_buffer, 0, 2); + } + else if (value >= int.MinValue && value <= int.MaxValue) + { + _buffer[0] = (byte)ETFType.INTEGER_EXT; + _buffer[1] = (byte)((value >> 24) & 0xFF); + _buffer[2] = (byte)((value >> 16) & 0xFF); + _buffer[3] = (byte)((value >> 8) & 0xFF); + _buffer[4] = (byte)(value & 0xFF); + _stream.Write(_buffer, 0, 5); + } + else + { + _buffer[0] = (byte)ETFType.SMALL_BIG_EXT; + if (value < 0) + { + _buffer[2] = 1; //Is negative + value = -value; + } + + byte bytes = 0; + while (value > 0) + _buffer[3 + bytes++] = (byte)((value >>= 8) & 0xFF); + _buffer[1] = bytes; //Encoded bytes + + _stream.Write(_buffer, 0, 3 + bytes); + } + } + public void Write(ulong value) + { + if (value <= byte.MaxValue) + { + _buffer[0] = (byte)ETFType.SMALL_INTEGER_EXT; + _buffer[1] = (byte)value; + _stream.Write(_buffer, 0, 2); + } + else if (value <= int.MaxValue) + { + _buffer[0] = (byte)ETFType.INTEGER_EXT; + _buffer[1] = (byte)((value >> 24) & 0xFF); + _buffer[2] = (byte)((value >> 16) & 0xFF); + _buffer[3] = (byte)((value >> 8) & 0xFF); + _buffer[4] = (byte)(value & 0xFF); + _stream.Write(_buffer, 0, 5); + } + else + { + _buffer[0] = (byte)ETFType.SMALL_BIG_EXT; + _buffer[2] = 0; //Always positive + + byte bytes = 0; + while (value > 0) + _buffer[3 + bytes++] = (byte)((value >>= 8) & 0xFF); + _buffer[1] = bytes; //Encoded bytes + + _stream.Write(_buffer, 0, 3 + bytes); + } + } + + public void Write(float value) => Write((double)value); + public void Write(double value) + { + ulong value2 = *(ulong*)&value; + _buffer[0] = (byte)ETFType.NEW_FLOAT_EXT; + _buffer[1] = (byte)((value2 >> 56) & 0xFF); + _buffer[2] = (byte)((value2 >> 48) & 0xFF); + _buffer[3] = (byte)((value2 >> 40) & 0xFF); + _buffer[4] = (byte)((value2 >> 32) & 0xFF); + _buffer[5] = (byte)((value2 >> 24) & 0xFF); + _buffer[6] = (byte)((value2 >> 16) & 0xFF); + _buffer[7] = (byte)((value2 >> 8) & 0xFF); + _buffer[8] = (byte)(value2 & 0xFF); + _stream.Write(_buffer, 0, 9); + } + + public void Write(DateTime value) => Write((ulong)((value.Ticks - _epochTime.Ticks) / TimeSpan.TicksPerMillisecond)); + + public void Write(byte? value) { if (value.HasValue) Write((ulong)value.Value); else WriteNil(); } + public void Write(sbyte? value) { if (value.HasValue) Write((long)value.Value); else WriteNil(); } + public void Write(ushort? value) { if (value.HasValue) Write((ulong)value.Value); else WriteNil(); } + public void Write(short? value) { if (value.HasValue) Write((long)value.Value); else WriteNil(); } + public void Write(uint? value) { if (value.HasValue) Write((ulong)value.Value); else WriteNil(); } + public void Write(int? value) { if (value.HasValue) Write(value.Value); else WriteNil(); } + public void Write(ulong? value) { if (value.HasValue) Write(value.Value); else WriteNil(); } + public void Write(long? value) { if (value.HasValue) Write(value.Value); else WriteNil(); } + public void Write(float? value) { if (value.HasValue) Write((double)value.Value); else WriteNil(); } + public void Write(double? value) { if (value.HasValue) Write(value.Value); else WriteNil(); } + public void Write(DateTime? value) { if (value.HasValue) Write(value.Value); else WriteNil(); } + + public void Write(byte[] value) + { + if (value != null) + { + int count = value.Length; + _buffer[0] = (byte)ETFType.BINARY_EXT; + _buffer[1] = (byte)((count >> 24) & 0xFF); + _buffer[2] = (byte)((count >> 16) & 0xFF); + _buffer[3] = (byte)((count >> 8) & 0xFF); + _buffer[4] = (byte)(count & 0xFF); + _stream.Write(_buffer, 0, 5); + _stream.Write(value, 0, value.Length); + } + else + WriteNil(); + } + public void Write(string value) + { + if (value != null) + { + var bytes = _encoding.GetBytes(value); + int count = bytes.Length; + _buffer[0] = (byte)ETFType.BINARY_EXT; + _buffer[1] = (byte)((count >> 24) & 0xFF); + _buffer[2] = (byte)((count >> 16) & 0xFF); + _buffer[3] = (byte)((count >> 8) & 0xFF); + _buffer[4] = (byte)(count & 0xFF); + _stream.Write(_buffer, 0, 5); + _stream.Write(bytes, 0, bytes.Length); + } + else + WriteNil(); + } + + public void Write(T obj) + { + var type = typeof(T); + var typeInfo = type.GetTypeInfo(); + var action = _serializers.GetOrAdd(type, _ => CreateSerializer(type, typeInfo, false)) as Action; + action(this, obj); + } + public void Write(T? obj) + where T : struct + { + if (obj != null) + Write(obj.Value); + else + WriteNil(); + } + public void Write(IEnumerable obj) + { + if (obj != null) + { + var array = obj.ToArray(); + int length = array.Length; + _buffer[0] = (byte)ETFType.LIST_EXT; + _buffer[1] = (byte)((length >> 24) & 0xFF); + _buffer[2] = (byte)((length >> 16) & 0xFF); + _buffer[3] = (byte)((length >> 8) & 0xFF); + _buffer[4] = (byte)(length & 0xFF); + for (int i = 0; i < array.Length; i++) + Write(array[i]); + WriteNilExt(); + _stream.Write(_buffer, 0, 5); + } + else + WriteNil(); + } + public void Write(IDictionary obj) + { + if (obj != null) + { + int length = obj.Count; + _buffer[0] = (byte)ETFType.MAP_EXT; + _buffer[1] = (byte)((length >> 24) & 0xFF); + _buffer[2] = (byte)((length >> 16) & 0xFF); + _buffer[3] = (byte)((length >> 8) & 0xFF); + _buffer[4] = (byte)(length & 0xFF); + foreach (var pair in obj) + { + Write(pair.Key); + Write(pair.Value); + } + _stream.Write(_buffer, 0, 5); + } + else + WriteNil(); + } + public void Write(object obj) + { + if (obj != null) + { + var type = obj.GetType(); + var typeInfo = type.GetTypeInfo(); + var action = _indirectSerializers.GetOrAdd(type, _ => CreateSerializer(type, typeInfo, true)) as Action; + action(this, obj); + } + else + WriteNil(); + } + + public virtual void Flush() => _stream.Flush(); + public virtual long Seek(int offset, SeekOrigin origin) => _stream.Seek(offset, origin); + + private Action CreateSerializer(Type type, TypeInfo typeInfo, bool isObject) + { + var method = new DynamicMethod(!isObject ? "SerializeETF" : "SerializeIndirectETF", + null, new[] { typeof(ETFWriter), isObject? typeof(object) : type }, true); + var generator = method.GetILGenerator(); + + if (typeInfo.IsPrimitive || typeInfo.IsEnum) + { + generator.Emit(OpCodes.Ldarg_0); //ETFWriter(this) + generator.Emit(OpCodes.Ldarg_1); //ETFWriter(this), value + if (isObject && typeInfo.IsValueType) + generator.Emit(OpCodes.Unbox_Any, type); //ETFWriter(this), value + EmitETFWriteValue(generator, type, typeInfo); + } + else + { + //Scan for certain interfaces + Type dictionaryI = null, enumerableI = null; + foreach (var i in typeInfo.ImplementedInterfaces + .Where(x => x.IsConstructedGenericType)) + { + if (i.GetGenericTypeDefinition() == typeof(IDictionary<,>)) + { + //TODO: Emit null check + dictionaryI = i; + break; + } + else if (i.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + { + //TODO: Emit null check + enumerableI = i; + break; + } + } + + if (dictionaryI != null) + { + throw new NotImplementedException(); + } + else if (enumerableI != null) + { + throw new NotImplementedException(); + } + else + { + //TODO: Add field/property names + typeInfo.ForEachField(f => + { + string name; + if (!f.IsPublic || !IsETFProperty(f, out name)) return; + + generator.Emit(OpCodes.Ldarg_0); //ETFWriter(this) + generator.Emit(OpCodes.Ldstr, name); //ETFWriter(this), name + generator.EmitCall(OpCodes.Call, GetWriteMethod(typeof(string)), null); + generator.Emit(OpCodes.Ldarg_0); //ETFWriter(this) + generator.Emit(OpCodes.Ldarg_1); //ETFWriter(this), obj + generator.Emit(OpCodes.Ldfld, f); //ETFWriter(this), obj.fieldValue + if (isObject && typeInfo.IsValueType) + generator.Emit(OpCodes.Unbox_Any, type); //ETFWriter(this), obj.fieldValue + EmitETFWriteValue(generator, f.FieldType); + }); + + typeInfo.ForEachProperty(p => + { + string name; + if (!p.CanRead || !p.GetMethod.IsPublic || !IsETFProperty(p, out name)) return; + + generator.Emit(OpCodes.Ldarg_0); //ETFWriter(this) + generator.Emit(OpCodes.Ldstr, name); //ETFWriter(this), name + generator.EmitCall(OpCodes.Call, GetWriteMethod(typeof(string)), null); + generator.Emit(OpCodes.Ldarg_0); //ETFWriter(this) + generator.Emit(OpCodes.Ldarg_1); //ETFWriter(this), obj + generator.EmitCall(OpCodes.Callvirt, p.GetMethod, null); //ETFWriter(this), obj.propValue + if (isObject && typeInfo.IsValueType) + generator.Emit(OpCodes.Unbox_Any, type); //ETFWriter(this), obj.propValue + EmitETFWriteValue(generator, p.PropertyType); + }); + } + } + + generator.Emit(OpCodes.Ret); + return method.CreateDelegate(typeof(Action)) as Action; + } + + private void EmitETFWriteValue(ILGenerator generator, Type type, TypeInfo typeInfo = null) + { + if (typeInfo == null) + typeInfo = type.GetTypeInfo(); + + //Convert enum types to their base type + if (typeInfo.IsEnum) + { + type = Enum.GetUnderlyingType(type); + typeInfo = type.GetTypeInfo(); + } + + //Check if this type already has a direct call or simple conversion + Type targetType = null; + if (!typeInfo.IsEnum && IsType(type, typeof(long), typeof(ulong), typeof(double), typeof(bool), typeof(string), + typeof(sbyte?), typeof(byte?), typeof(short?), typeof(ushort?), + typeof(int?), typeof(uint?), typeof(long?), typeof(ulong?), + typeof(bool?), typeof(float?), typeof(double?), + typeof(object), typeof(DateTime))) + targetType = type; //Direct + else if (IsType(type, typeof(sbyte), typeof(short), typeof(int))) + { + generator.Emit(OpCodes.Conv_I8); + targetType = typeof(long); + } + else if (IsType(type, typeof(byte), typeof(ushort), typeof(uint))) + { + generator.Emit(OpCodes.Conv_U8); + targetType = typeof(ulong); + } + else if (IsType(type, typeof(float))) + { + generator.Emit(OpCodes.Conv_R8); + targetType = typeof(double); + } + + //Primitives/Enums + if (targetType != null) + generator.EmitCall(OpCodes.Call, GetWriteMethod(targetType), null); + + //Nullable Non-Primitives + else if (typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(Nullable<>)) + generator.EmitCall(OpCodes.Call, _writeNullableTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null); + + //Structs/Classes + else if (typeInfo.IsClass || (typeInfo.IsValueType && !typeInfo.IsPrimitive)) + generator.EmitCall(OpCodes.Call, _writeTMethod.MakeGenericMethod(type), null); + + //Unsupported (decimal, char) + else + throw new InvalidOperationException($"Serializing {type.Name} is not supported."); + } + + private bool IsType(Type type, params Type[] types) + { + for (int i = 0; i < types.Length; i++) + { + if (type == types[i]) + return true; + } + return false; + } + private MethodInfo GetWriteMethod(Type paramType) + { + return typeof(ETFWriter).GetTypeInfo().GetDeclaredMethods(nameof(Write)) + .Where(x => x.GetParameters()[0].ParameterType == paramType) + .FirstOrDefault(); + } + + private void WriteNil() => _stream.Write(_nilBytes, 0, _nilBytes.Length); + private void WriteNilExt() => _stream.Write(_nilExtBytes, 0, _nilExtBytes.Length); + + private bool IsETFProperty(FieldInfo f, out string name) + { + var attrib = f.CustomAttributes.Where(x => x.AttributeType == typeof(JsonPropertyAttribute)).FirstOrDefault(); + if (attrib != null) + { + name = attrib.ConstructorArguments.FirstOrDefault().Value as string ?? f.Name; + return true; + } + name = null; + return false; + } + private bool IsETFProperty(PropertyInfo p, out string name) + { + var attrib = p.CustomAttributes.Where(x => x.AttributeType == typeof(JsonPropertyAttribute)).FirstOrDefault(); + if (attrib != null) + { + name = attrib.ConstructorArguments.FirstOrDefault().Value as string ?? p.Name; + return true; + } + name = null; + return false; + } + + #region IDisposable + private bool _isDisposed = false; + + protected virtual void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + if (_leaveOpen) + _stream.Flush(); + else + _stream.Dispose(); + } + _isDisposed = true; + } + } + + public void Dispose() => Dispose(true); + #endregion + } +} diff --git a/src/Discord.Net/Enums/ChannelType.cs b/src/Discord.Net/Enums/ChannelType.cs index b3d6ce6e8..9ed49a701 100644 --- a/src/Discord.Net/Enums/ChannelType.cs +++ b/src/Discord.Net/Enums/ChannelType.cs @@ -2,7 +2,7 @@ namespace Discord { - public sealed class ChannelType : StringEnum, IEquatable + public class ChannelType : StringEnum, IEquatable { /// A text-only channel. public static ChannelType Text { get; } = new ChannelType("text"); diff --git a/src/Discord.Net/Enums/PermissionTarget.cs b/src/Discord.Net/Enums/PermissionTarget.cs index 2da27cabc..38a70e013 100644 --- a/src/Discord.Net/Enums/PermissionTarget.cs +++ b/src/Discord.Net/Enums/PermissionTarget.cs @@ -1,6 +1,6 @@ namespace Discord { - public sealed class PermissionTarget : StringEnum + public class PermissionTarget : StringEnum { /// A text-only channel. public static PermissionTarget Role { get; } = new PermissionTarget("role"); diff --git a/src/Discord.Net/Enums/Relative.cs b/src/Discord.Net/Enums/Relative.cs new file mode 100644 index 000000000..4bd44c5ab --- /dev/null +++ b/src/Discord.Net/Enums/Relative.cs @@ -0,0 +1,7 @@ +namespace Discord +{ + public enum Relative + { + Before, After + } +} diff --git a/src/Discord.Net/Enums/UserStatus.cs b/src/Discord.Net/Enums/UserStatus.cs index da126a261..80def4234 100644 --- a/src/Discord.Net/Enums/UserStatus.cs +++ b/src/Discord.Net/Enums/UserStatus.cs @@ -1,6 +1,6 @@ namespace Discord { - public sealed class UserStatus : StringEnum + public class UserStatus : StringEnum { /// User is currently online and active. public static UserStatus Online { get; } = new UserStatus("online"); diff --git a/src/Discord.Net/ChannelEventArgs.cs b/src/Discord.Net/Events/ChannelEventArgs.cs similarity index 100% rename from src/Discord.Net/ChannelEventArgs.cs rename to src/Discord.Net/Events/ChannelEventArgs.cs diff --git a/src/Discord.Net/Events/ChannelUpdatedEventArgs.cs b/src/Discord.Net/Events/ChannelUpdatedEventArgs.cs new file mode 100644 index 000000000..fa8da98ea --- /dev/null +++ b/src/Discord.Net/Events/ChannelUpdatedEventArgs.cs @@ -0,0 +1,18 @@ +using System; + +namespace Discord +{ + public class ChannelUpdatedEventArgs : EventArgs + { + public Channel Before { get; } + public Channel After { get; } + + public Server Server => After.Server; + + public ChannelUpdatedEventArgs(Channel before, Channel after) + { + Before = before; + After = after; + } + } +} diff --git a/src/Discord.Net/ChannelUserEventArgs.cs b/src/Discord.Net/Events/ChannelUserEventArgs.cs similarity index 100% rename from src/Discord.Net/ChannelUserEventArgs.cs rename to src/Discord.Net/Events/ChannelUserEventArgs.cs diff --git a/src/Discord.Net/DisconnectedEventArgs.cs b/src/Discord.Net/Events/DisconnectedEventArgs.cs similarity index 100% rename from src/Discord.Net/DisconnectedEventArgs.cs rename to src/Discord.Net/Events/DisconnectedEventArgs.cs diff --git a/src/Discord.Net/LogMessageEventArgs.cs b/src/Discord.Net/Events/LogMessageEventArgs.cs similarity index 100% rename from src/Discord.Net/LogMessageEventArgs.cs rename to src/Discord.Net/Events/LogMessageEventArgs.cs diff --git a/src/Discord.Net/MessageEventArgs.cs b/src/Discord.Net/Events/MessageEventArgs.cs similarity index 100% rename from src/Discord.Net/MessageEventArgs.cs rename to src/Discord.Net/Events/MessageEventArgs.cs diff --git a/src/Discord.Net/Events/MessageUpdatedEventArgs.cs b/src/Discord.Net/Events/MessageUpdatedEventArgs.cs new file mode 100644 index 000000000..849f234e1 --- /dev/null +++ b/src/Discord.Net/Events/MessageUpdatedEventArgs.cs @@ -0,0 +1,20 @@ +using System; + +namespace Discord +{ + public class MessageUpdatedEventArgs : EventArgs + { + public Message Before { get; } + public Message After { get; } + + public User User => After.User; + public Channel Channel => After.Channel; + public Server Server => After.Server; + + public MessageUpdatedEventArgs(Message before, Message after) + { + Before = before; + After = after; + } + } +} diff --git a/src/Discord.Net/Events/ProfileUpdatedEventArgs.cs b/src/Discord.Net/Events/ProfileUpdatedEventArgs.cs new file mode 100644 index 000000000..2365908e8 --- /dev/null +++ b/src/Discord.Net/Events/ProfileUpdatedEventArgs.cs @@ -0,0 +1,16 @@ +using System; + +namespace Discord +{ + public class ProfileUpdatedEventArgs : EventArgs + { + public Profile Before { get; } + public Profile After { get; } + + public ProfileUpdatedEventArgs(Profile before, Profile after) + { + Before = before; + After = after; + } + } +} diff --git a/src/Discord.Net/RoleEventArgs.cs b/src/Discord.Net/Events/RoleEventArgs.cs similarity index 100% rename from src/Discord.Net/RoleEventArgs.cs rename to src/Discord.Net/Events/RoleEventArgs.cs diff --git a/src/Discord.Net/Events/RoleUpdatedEventArgs.cs b/src/Discord.Net/Events/RoleUpdatedEventArgs.cs new file mode 100644 index 000000000..26151c98b --- /dev/null +++ b/src/Discord.Net/Events/RoleUpdatedEventArgs.cs @@ -0,0 +1,18 @@ +using System; + +namespace Discord +{ + public class RoleUpdatedEventArgs : EventArgs + { + public Role Before { get; } + public Role After { get; } + + public Server Server => After.Server; + + public RoleUpdatedEventArgs(Role before, Role after) + { + Before = before; + After = after; + } + } +} diff --git a/src/Discord.Net/ServerEventArgs.cs b/src/Discord.Net/Events/ServerEventArgs.cs similarity index 100% rename from src/Discord.Net/ServerEventArgs.cs rename to src/Discord.Net/Events/ServerEventArgs.cs diff --git a/src/Discord.Net/Events/ServerUpdatedEventArgs.cs b/src/Discord.Net/Events/ServerUpdatedEventArgs.cs new file mode 100644 index 000000000..8532f72dc --- /dev/null +++ b/src/Discord.Net/Events/ServerUpdatedEventArgs.cs @@ -0,0 +1,16 @@ +using System; + +namespace Discord +{ + public class ServerUpdatedEventArgs : EventArgs + { + public Server Before { get; } + public Server After { get; } + + public ServerUpdatedEventArgs(Server before, Server after) + { + Before = before; + After = after; + } + } +} diff --git a/src/Discord.Net/UserEventArgs.cs b/src/Discord.Net/Events/UserEventArgs.cs similarity index 100% rename from src/Discord.Net/UserEventArgs.cs rename to src/Discord.Net/Events/UserEventArgs.cs diff --git a/src/Discord.Net/Events/UserUpdatedEventArgs.cs b/src/Discord.Net/Events/UserUpdatedEventArgs.cs new file mode 100644 index 000000000..89e8cce0c --- /dev/null +++ b/src/Discord.Net/Events/UserUpdatedEventArgs.cs @@ -0,0 +1,17 @@ +using System; +namespace Discord +{ + public class UserUpdatedEventArgs : EventArgs + { + public User Before { get; } + public User After { get; } + + public Server Server => After.Server; + + public UserUpdatedEventArgs(User before, User after) + { + Before = before; + After = after; + } + } +} diff --git a/src/Discord.Net/Legacy.cs b/src/Discord.Net/Legacy.cs index 45eda9b42..e74494508 100644 --- a/src/Discord.Net/Legacy.cs +++ b/src/Discord.Net/Legacy.cs @@ -25,6 +25,17 @@ namespace Discord.Legacy public static class LegacyExtensions { + [Obsolete("Use DiscordClient.ExecuteAndWait")] + public static void Run(this DiscordClient client, Func asyncAction) + { + client.ExecuteAndWait(asyncAction); + } + [Obsolete("Use DiscordClient.Wait")] + public static void Run(this DiscordClient client) + { + client.Wait(); + } + [Obsolete("Use Server.FindChannels")] public static IEnumerable FindChannels(this DiscordClient client, Server server, string name, ChannelType type = null, bool exactMatch = false) { @@ -163,7 +174,7 @@ namespace Discord.Legacy } [Obsolete("Use Channel.DownloadMessages")] - public static Task DownloadMessages(this DiscordClient client, Channel channel, int limit = 100, ulong? relativeMessageId = null, RelativeDirection relativeDir = RelativeDirection.Before, bool useCache = true) + public static Task DownloadMessages(this DiscordClient client, Channel channel, int limit = 100, ulong? relativeMessageId = null, Relative relativeDir = Relative.Before, bool useCache = true) { if (channel == null) throw new ArgumentNullException(nameof(channel)); return channel.DownloadMessages(limit, relativeMessageId, relativeDir, useCache); diff --git a/src/Discord.Net/Logging/LogManager.cs b/src/Discord.Net/Logging/LogManager.cs index f52031df9..576ec7679 100644 --- a/src/Discord.Net/Logging/LogManager.cs +++ b/src/Discord.Net/Logging/LogManager.cs @@ -2,7 +2,7 @@ namespace Discord.Logging { - public sealed class LogManager + public class LogManager { private readonly DiscordClient _client; diff --git a/src/Discord.Net/Logging/Logger.cs b/src/Discord.Net/Logging/Logger.cs index 6c1cc1a4b..9d7a6111b 100644 --- a/src/Discord.Net/Logging/Logger.cs +++ b/src/Discord.Net/Logging/Logger.cs @@ -2,7 +2,7 @@ namespace Discord.Logging { - public sealed class Logger + public class Logger { private readonly LogManager _manager; diff --git a/src/Discord.Net/MessageQueue.cs b/src/Discord.Net/MessageQueue.cs index e4321b365..2759f458e 100644 --- a/src/Discord.Net/MessageQueue.cs +++ b/src/Discord.Net/MessageQueue.cs @@ -1,5 +1,6 @@ using Discord.API.Client.Rest; using Discord.Logging; +using Discord.Net.Rest; using System; using System.Collections.Concurrent; using System.Net; @@ -9,7 +10,7 @@ using System.Threading.Tasks; namespace Discord.Net { /// Manages an outgoing message queue for DiscordClient. - public sealed class MessageQueue + public class MessageQueue { private interface IQueuedAction { @@ -52,7 +53,7 @@ namespace Discord.Net private const int WarningStart = 30; private readonly Random _nonceRand; - private readonly DiscordClient _client; + private readonly RestClient _rest; private readonly Logger _logger; private readonly ConcurrentQueue _pendingActions; private readonly ConcurrentDictionary _pendingSends; @@ -61,9 +62,9 @@ namespace Discord.Net /// Gets the current number of queued actions. public int Count { get; private set; } - internal MessageQueue(DiscordClient client, Logger logger) + internal MessageQueue(RestClient rest, Logger logger) { - _client = client; + _rest = rest; _logger = logger; _nonceRand = new Random(); @@ -73,7 +74,7 @@ namespace Discord.Net internal Message QueueSend(Channel channel, string text, bool isTTS) { - Message msg = new Message(0, channel, channel.IsPrivate ? _client.PrivateUser : channel.Server.CurrentUser); + Message msg = new Message(0, channel, channel.IsPrivate ? channel.Client.PrivateUser : channel.Server.CurrentUser); msg.RawText = text; msg.Text = msg.Resolve(text); msg.Nonce = GenerateNonce(); @@ -135,7 +136,7 @@ namespace Discord.Net Nonce = msg.Nonce.ToString(), IsTTS = msg.IsTTS }; - var response = await _client.ClientAPI.Send(request).ConfigureAwait(false); + var response = await _rest.Send(request).ConfigureAwait(false); msg.Id = response.Id; msg.Update(response); msg.State = MessageState.Normal; @@ -153,9 +154,9 @@ namespace Discord.Net { Content = text }; - await _client.ClientAPI.Send(request).ConfigureAwait(false); + await _rest.Send(request).ConfigureAwait(false); } - catch (Exception ex) { msg.State = MessageState.Failed; _logger.Error("Failed to edit message", ex); } + catch (Exception ex) { _logger.Error("Failed to edit message", ex); } } } internal async Task Delete(Message msg) @@ -165,9 +166,10 @@ namespace Discord.Net try { var request = new DeleteMessageRequest(msg.Channel.Id, msg.Id); - await _client.ClientAPI.Send(request).ConfigureAwait(false); + await _rest.Send(request).ConfigureAwait(false); } - catch (Exception ex) { msg.State = MessageState.Failed; _logger.Error("Failed to delete message", ex); } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } //Ignore + catch (Exception ex) { _logger.Error("Failed to delete message", ex); } } } diff --git a/src/Discord.Net/Models/Channel.cs b/src/Discord.Net/Models/Channel.cs index b64fce45f..505db8f25 100644 --- a/src/Discord.Net/Models/Channel.cs +++ b/src/Discord.Net/Models/Channel.cs @@ -12,8 +12,10 @@ using APIChannel = Discord.API.Client.Channel; namespace Discord { - public sealed class Channel : IMentionable + public class Channel : IMentionable { + private readonly static Action _cloner = DynamicIL.CreateCopyMethod(); + private struct Member { public readonly User User; @@ -26,7 +28,7 @@ namespace Discord } } - public sealed class PermissionOverwrite + public class PermissionOverwrite { public PermissionTarget TargetType { get; } public ulong TargetId { get; } @@ -102,7 +104,7 @@ namespace Discord return Enumerable.Empty(); } } - + internal Channel(DiscordClient client, ulong id, Server server) : this(client, id) { @@ -283,7 +285,7 @@ namespace Discord } public async Task DownloadMessages(int limit = 100, ulong? relativeMessageId = null, - RelativeDirection relativeDir = RelativeDirection.Before, bool useCache = true) + Relative relativeDir = Relative.Before, bool useCache = true) { if (limit < 0) throw new ArgumentOutOfRangeException(nameof(limit)); if (limit == 0 || Type != ChannelType.Text) return new Message[0]; @@ -293,7 +295,7 @@ namespace Discord var request = new GetMessagesRequest(Id) { Limit = limit, - RelativeDir = relativeMessageId.HasValue ? relativeDir == RelativeDirection.Before ? "before" : "after" : null, + RelativeDir = relativeMessageId.HasValue ? relativeDir == Relative.Before ? "before" : "after" : null, RelativeId = relativeMessageId ?? 0 }; var msgs = await Client.ClientAPI.Send(request).ConfigureAwait(false); @@ -598,6 +600,14 @@ namespace Discord } #endregion + internal Channel Clone() + { + var result = new Channel(); + _cloner(this, result); + return result; + } + private Channel() { } + public override string ToString() => Name ?? Id.ToIdString(); } } diff --git a/src/Discord.Net/Models/Color.cs b/src/Discord.Net/Models/Color.cs index 1ae922bd7..be8a14a18 100644 --- a/src/Discord.Net/Models/Color.cs +++ b/src/Discord.Net/Models/Color.cs @@ -2,9 +2,11 @@ namespace Discord { - public sealed class Color : IEquatable - { - public static readonly Color Default = PresetColor(0); + public class Color : IEquatable + { + private readonly static Action _cloner = DynamicIL.CreateCopyMethod(); + + public static readonly Color Default = PresetColor(0); public static readonly Color Teal = PresetColor(0x1ABC9C); public static readonly Color DarkTeal = PresetColor(0x11806A); @@ -83,6 +85,14 @@ namespace Discord public override bool Equals(object obj) => (obj as Color)?.Equals(this) ?? false; public bool Equals(Color color) => color != null && color._rawValue == _rawValue; + internal Color Clone() + { + var result = new Color(); + _cloner(this, result); + return result; + } + private Color() { } //Used for cloning + public override string ToString() => '#' + _rawValue.ToString("X"); } } diff --git a/src/Discord.Net/Models/Emoji.cs b/src/Discord.Net/Models/Emoji.cs deleted file mode 100644 index cd9b8fcd4..000000000 --- a/src/Discord.Net/Models/Emoji.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Discord.API.Client; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Discord.Models -{ -} diff --git a/src/Discord.Net/Models/Invite.cs b/src/Discord.Net/Models/Invite.cs index 326b1bf82..53ad22470 100644 --- a/src/Discord.Net/Models/Invite.cs +++ b/src/Discord.Net/Models/Invite.cs @@ -8,9 +8,11 @@ using APIInvite = Discord.API.Client.Invite; namespace Discord { - public sealed class Invite - { - public sealed class ServerInfo + public class Invite + { + private readonly static Action _cloner = DynamicIL.CreateCopyMethod(); + + public class ServerInfo { /// Returns the unique identifier of this server. public ulong Id { get; } @@ -23,7 +25,7 @@ namespace Discord Name = name; } } - public sealed class ChannelInfo + public class ChannelInfo { /// Returns the unique identifier of this channel. public ulong Id { get; } @@ -36,7 +38,7 @@ namespace Discord Name = name; } } - public sealed class InviterInfo + public class InviterInfo { /// Returns the unique identifier for this user. public ulong Id { get; } @@ -126,8 +128,14 @@ namespace Discord public Task Accept() => Client.ClientAPI.Send(new AcceptInviteRequest(Code)); - public override bool Equals(object obj) => obj is Invite && (obj as Invite).Code == Code; - public override int GetHashCode() => unchecked(Code.GetHashCode() + 9980); - public override string ToString() => XkcdCode ?? Code; + internal Invite Clone() + { + var result = new Invite(); + _cloner(this, result); + return result; + } + private Invite() { } //Used for cloning + + public override string ToString() => XkcdCode ?? Code; } } diff --git a/src/Discord.Net/Models/Message.cs b/src/Discord.Net/Models/Message.cs index 9e66111a2..c1fd59bb8 100644 --- a/src/Discord.Net/Models/Message.cs +++ b/src/Discord.Net/Models/Message.cs @@ -1,12 +1,9 @@ using Discord.API.Client.Rest; using Discord.Net; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; using System; using System.Collections.Generic; using System.Linq; using System.Net; -using System.Reflection; using System.Text.RegularExpressions; using System.Threading.Tasks; using APIMessage = Discord.API.Client.Message; @@ -25,11 +22,13 @@ namespace Discord Failed } - public sealed class Message + public class Message { - private static readonly Regex _userRegex = new Regex(@"<@[0-9]+>", RegexOptions.Compiled); - private static readonly Regex _channelRegex = new Regex(@"<#[0-9]+>", RegexOptions.Compiled); - private static readonly Regex _roleRegex = new Regex(@"@everyone", RegexOptions.Compiled); + private readonly static Action _cloner = DynamicIL.CreateCopyMethod(); + + private static readonly Regex _userRegex = new Regex(@"<@[0-9]+>"); + private static readonly Regex _channelRegex = new Regex(@"<#[0-9]+>"); + private static readonly Regex _roleRegex = new Regex(@"@everyone"); private static readonly Attachment[] _initialAttachments = new Attachment[0]; private static readonly Embed[] _initialEmbeds = new Embed[0]; @@ -112,7 +111,7 @@ namespace Discord } }*/ - public sealed class Attachment : File + public class Attachment : File { /// Unique identifier for this file. public string Id { get; internal set; } @@ -124,7 +123,7 @@ namespace Discord internal Attachment() { } } - public sealed class Embed + public class Embed { /// URL of this embed. public string Url { get; internal set; } @@ -140,11 +139,13 @@ namespace Discord public EmbedLink Provider { get; internal set; } /// Returns the thumbnail of this embed. public File Thumbnail { get; internal set; } + /// Returns the video information of this embed. + public File Video { get; internal set; } - internal Embed() { } + internal Embed() { } } - public sealed class EmbedLink + public class EmbedLink { /// URL of this embed provider. public string Url { get; internal set; } @@ -242,7 +243,7 @@ namespace Discord Embeds = model.Embeds.Select(x => { EmbedLink author = null, provider = null; - File thumbnail = null; + File thumbnail = null, video = null; if (x.Author != null) author = new EmbedLink { Url = x.Author.Url, Name = x.Author.Name }; @@ -250,8 +251,10 @@ namespace Discord provider = new EmbedLink { Url = x.Provider.Url, Name = x.Provider.Name }; if (x.Thumbnail != null) thumbnail = new File { Url = x.Thumbnail.Url, ProxyUrl = x.Thumbnail.ProxyUrl, Width = x.Thumbnail.Width, Height = x.Thumbnail.Height }; + if (x.Video != null) + video = new File { Url = x.Video.Url, ProxyUrl = null, Width = x.Video.Width, Height = x.Video.Height }; - return new Embed + return new Embed { Url = x.Url, Type = x.Type, @@ -259,7 +262,8 @@ namespace Discord Description = x.Description, Author = author, Provider = provider, - Thumbnail = thumbnail + Thumbnail = thumbnail, + Video = video }; }).ToArray(); } @@ -369,8 +373,14 @@ namespace Discord return Resolve(Channel, text); } - public override bool Equals(object obj) => obj is Message && (obj as Message).Id == Id; - public override int GetHashCode() => unchecked(Id.GetHashCode() + 9979); - public override string ToString() => $"{User?.Name ?? "Unknown User"}: {RawText}"; + internal Message Clone() + { + var result = new Message(); + _cloner(this, result); + return result; + } + private Message() { } //Used for cloning + + public override string ToString() => $"{User?.Name ?? "Unknown User"}: {RawText}"; } } diff --git a/src/Discord.Net/Models/Permissions.cs b/src/Discord.Net/Models/Permissions.cs index 1d678bfbe..c842a8658 100644 --- a/src/Discord.Net/Models/Permissions.cs +++ b/src/Discord.Net/Models/Permissions.cs @@ -31,17 +31,25 @@ namespace Discord UseVoiceActivation = 25 } - public sealed class ServerPermissions : Permissions - { - public static ServerPermissions None { get; } = new ServerPermissions(); + public class ServerPermissions : Permissions + { + private readonly static Action _cloner = DynamicIL.CreateCopyMethod(); + + public static ServerPermissions None { get; } = new ServerPermissions(); public static ServerPermissions All { get; } = new ServerPermissions(Convert.ToUInt32("00000011111100111111110000111111", 2)); public ServerPermissions() : base() { } public ServerPermissions(uint rawValue) : base(rawValue) { } public ServerPermissions Copy() => new ServerPermissions(RawValue); + internal ServerPermissions Clone() + { + var result = new ServerPermissions(); + _cloner(this, result); + return result; + } - /// If True, a user may ban users from the server. - public bool BanMembers { get { return GetBit(PermissionsBits.BanMembers); } set { SetBit(PermissionsBits.BanMembers, value); } } + /// If True, a user may ban users from the server. + public bool BanMembers { get { return GetBit(PermissionsBits.BanMembers); } set { SetBit(PermissionsBits.BanMembers, value); } } /// If True, a user may kick users from the server. public bool KickMembers { get { return GetBit(PermissionsBits.KickMembers); } set { SetBit(PermissionsBits.KickMembers, value); } } /// If True, a user may adjust roles. This also implictly grants all other permissions. @@ -52,9 +60,11 @@ namespace Discord public bool ManageServer { get { return GetBit(PermissionsBits.ManageServer); } set { SetBit(PermissionsBits.ManageServer, value); } } } - public sealed class ChannelPermissions : Permissions - { - public static ChannelPermissions None { get; } = new ChannelPermissions(); + public class ChannelPermissions : Permissions + { + private readonly static Action _cloner = DynamicIL.CreateCopyMethod(); + + public static ChannelPermissions None { get; } = new ChannelPermissions(); public static ChannelPermissions TextOnly { get; } = new ChannelPermissions(Convert.ToUInt32("00000000000000111111110000011001", 2)); public static ChannelPermissions PrivateOnly { get; } = new ChannelPermissions(Convert.ToUInt32("00000000000000011100110000000000", 2)); public static ChannelPermissions VoiceOnly { get; } = new ChannelPermissions(Convert.ToUInt32("00000011111100000000000000011001", 2)); @@ -70,9 +80,15 @@ namespace Discord public ChannelPermissions() : base() { } public ChannelPermissions(uint rawValue) : base(rawValue) { } public ChannelPermissions Copy() => new ChannelPermissions(RawValue); + internal ChannelPermissions Clone() + { + var result = new ChannelPermissions(); + _cloner(this, result); + return result; + } - /// If True, a user may adjust permissions. This also implictly grants all other permissions. - public bool ManagePermissions { get { return GetBit(PermissionsBits.ManageRolesOrPermissions); } set { SetBit(PermissionsBits.ManageRolesOrPermissions, value); } } + /// If True, a user may adjust permissions. This also implictly grants all other permissions. + public bool ManagePermissions { get { return GetBit(PermissionsBits.ManageRolesOrPermissions); } set { SetBit(PermissionsBits.ManageRolesOrPermissions, value); } } /// If True, a user may create, delete and modify this channel. public bool ManageChannel { get { return GetBit(PermissionsBits.ManageChannel); } set { SetBit(PermissionsBits.ManageChannel, value); } } } @@ -151,9 +167,11 @@ namespace Discord public bool Equals(Permissions permission) => permission?._rawValue == _rawValue; } - public sealed class DualChannelPermissions - { - public ChannelPermissions Allow { get; } + public class DualChannelPermissions + { + private readonly static Action _cloner = DynamicIL.CreateCopyMethod(); + + public ChannelPermissions Allow { get; } public ChannelPermissions Deny { get; } public DualChannelPermissions(uint allow = 0, uint deny = 0) @@ -233,6 +251,12 @@ namespace Discord Deny.Lock(); } public DualChannelPermissions Copy() => new DualChannelPermissions(Allow.RawValue, Deny.RawValue); + internal DualChannelPermissions Clone() + { + var result = new DualChannelPermissions(); + _cloner(this, result); + return result; + } public static bool operator ==(DualChannelPermissions a, DualChannelPermissions b) => ((object)a == null && (object)b == null) || (a?.Equals(b) ?? false); public static bool operator !=(DualChannelPermissions a, DualChannelPermissions b) => !(a == b); diff --git a/src/Discord.Net/Models/Profile.cs b/src/Discord.Net/Models/Profile.cs index c6eeb6657..859fe002d 100644 --- a/src/Discord.Net/Models/Profile.cs +++ b/src/Discord.Net/Models/Profile.cs @@ -6,8 +6,10 @@ using APIUser = Discord.API.Client.User; namespace Discord { - public sealed class Profile + public class Profile { + private readonly static Action _cloner = DynamicIL.CreateCopyMethod(); + internal DiscordClient Client { get; } /// Gets the unique identifier for this user. @@ -70,11 +72,17 @@ namespace Discord }; var loginResponse = await Client.ClientAPI.Send(loginRequest).ConfigureAwait(false); Client.ClientAPI.Token = loginResponse.Token; - Client.GatewaySocket.Token = loginResponse.Token; - Client.GatewaySocket.SessionId = null; } } + internal Profile Clone() + { + var result = new Profile(); + _cloner(this, result); + return result; + } + private Profile() { } //Used for cloning + public override string ToString() => Id.ToIdString(); } } diff --git a/src/Discord.Net/Models/Region.cs b/src/Discord.Net/Models/Region.cs index 839d20907..5b0354048 100644 --- a/src/Discord.Net/Models/Region.cs +++ b/src/Discord.Net/Models/Region.cs @@ -1,6 +1,6 @@ namespace Discord { - public sealed class Region + public class Region { public string Id { get; } public string Name { get; } @@ -16,5 +16,5 @@ Port = port; Vip = vip; } - } + } } diff --git a/src/Discord.Net/Models/Role.cs b/src/Discord.Net/Models/Role.cs index fefe0f0fd..5fb8d7102 100644 --- a/src/Discord.Net/Models/Role.cs +++ b/src/Discord.Net/Models/Role.cs @@ -9,8 +9,10 @@ using APIRole = Discord.API.Client.Role; namespace Discord { - public sealed class Role : IMentionable + public class Role : IMentionable { + private readonly static Action _cloner = DynamicIL.CreateCopyMethod(); + internal DiscordClient Client => Server.Client; /// Gets the unique identifier for this role. @@ -117,7 +119,15 @@ namespace Discord try { await Client.ClientAPI.Send(new DeleteRoleRequest(Server.Id, Id)).ConfigureAwait(false); } catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } } - - public override string ToString() => Name ?? Id.ToIdString(); + + internal Role Clone() + { + var result = new Role(); + _cloner(this, result); + return result; + } + private Role() { } //Used for cloning + + public override string ToString() => Name ?? Id.ToIdString(); } } diff --git a/src/Discord.Net/Models/Server.cs b/src/Discord.Net/Models/Server.cs index c19e23c46..581e3b901 100644 --- a/src/Discord.Net/Models/Server.cs +++ b/src/Discord.Net/Models/Server.cs @@ -12,14 +12,16 @@ using System.Threading.Tasks; namespace Discord { /// Represents a Discord server (also known as a guild). - public sealed class Server + public class Server { + private readonly static Action _cloner = DynamicIL.CreateCopyMethod(); + internal static string GetIconUrl(ulong serverId, string iconId) => iconId != null ? $"{DiscordConfig.ClientAPIUrl}guilds/${serverId}/icons/${iconId}.jpg" : null; internal static string GetSplashUrl(ulong serverId, string splashId) => splashId != null ? $"{DiscordConfig.ClientAPIUrl}guilds/{serverId}/splashes/{splashId}.jpg" : null; - public sealed class Emoji + public class Emoji { public string Id { get; } @@ -85,8 +87,9 @@ namespace Discord public Channel AFKChannel => _afkChannelId != null ? GetChannel(_afkChannelId.Value) : null; /// Gets the current user in this server. public User CurrentUser => GetUser(Client.CurrentUser.Id); - /// Gets the URL to this user's current avatar. + /// Gets the URL to this server's current icon. public string IconUrl => GetIconUrl(Id, IconId); + /// Gets the URL to this servers's splash image. public string SplashUrl => GetSplashUrl(Id, SplashId); /// Gets a collection of all channels in this server. @@ -143,16 +146,15 @@ namespace Discord Roles = x.RoleIds.Select(y => GetRole(y)).Where(y => y != null).ToArray() }).ToArray(); } - - //Can be null - _afkChannelId = model.AFKChannelId; - SplashId = model.Splash; - if (model.Roles != null) { foreach (var x in model.Roles) AddRole(x.Id).Update(x); } + + //Can be null + _afkChannelId = model.AFKChannelId; + SplashId = model.Splash; } internal void Update(ExtendedGuild model) { @@ -498,7 +500,15 @@ namespace Discord public void RequestOfflineUsers() => Client.GatewaySocket.SendRequestMembers(Id, "", 0); #endregion - - public override string ToString() => Name ?? Id.ToIdString(); + + internal Server Clone() + { + var result = new Server(); + _cloner(this, result); + return result; + } + private Server() { } //Used for cloning + + public override string ToString() => Name ?? Id.ToIdString(); } } diff --git a/src/Discord.Net/Models/User.cs b/src/Discord.Net/Models/User.cs index 910b07faa..7fff08f25 100644 --- a/src/Discord.Net/Models/User.cs +++ b/src/Discord.Net/Models/User.cs @@ -9,8 +9,10 @@ using APIMember = Discord.API.Client.Member; namespace Discord { - public sealed class User : IMentionable + public class User { + private readonly static Action _cloner = DynamicIL.CreateCopyMethod(); + internal static string GetAvatarUrl(ulong userId, string avatarId) => avatarId != null ? $"{DiscordConfig.ClientAPIUrl}users/{userId}/avatars/{avatarId}.jpg" : null; @@ -338,6 +340,14 @@ namespace Discord => Edit(roles: Roles.Except(roles)); #endregion - public override string ToString() => Name != null ? $"{Name}#{Discriminator}" : Id.ToIdString(); + internal User Clone() + { + var result = new User(); + _cloner(this, result); + return result; + } + private User() { } //Used for cloning + + public override string ToString() => Name != null ? $"{Name}#{Discriminator}" : Id.ToIdString(); } } \ No newline at end of file diff --git a/src/Discord.Net/Net/HttpException.cs b/src/Discord.Net/Net/HttpException.cs index afb17bcaf..8bfdbf73b 100644 --- a/src/Discord.Net/Net/HttpException.cs +++ b/src/Discord.Net/Net/HttpException.cs @@ -7,7 +7,7 @@ namespace Discord.Net #if NET46 [Serializable] #endif - public sealed class HttpException : Exception + public class HttpException : Exception { public HttpStatusCode StatusCode { get; } diff --git a/src/Discord.Net/Net/Rest/BuiltInEngine.cs b/src/Discord.Net/Net/Rest/BuiltInEngine.cs index cb37d6648..7e422b49a 100644 --- a/src/Discord.Net/Net/Rest/BuiltInEngine.cs +++ b/src/Discord.Net/Net/Rest/BuiltInEngine.cs @@ -13,7 +13,7 @@ using Nito.AsyncEx; namespace Discord.Net.Rest { - internal sealed class BuiltInEngine : IRestEngine + internal class BuiltInEngine : IRestEngine { private const int HR_SECURECHANNELFAILED = -2146233079; diff --git a/src/Discord.Net/Net/Rest/ETFRestClient.cs b/src/Discord.Net/Net/Rest/ETFRestClient.cs new file mode 100644 index 000000000..b52da1037 --- /dev/null +++ b/src/Discord.Net/Net/Rest/ETFRestClient.cs @@ -0,0 +1,27 @@ +using Discord.ETF; +using System.IO; +using System; +using Discord.Logging; + +namespace Discord.Net.Rest +{ + public class ETFRestClient : RestClient + { + private readonly ETFWriter _serializer; + + public ETFRestClient(DiscordConfig config, string baseUrl, Logger logger) + : base(config, baseUrl, logger) + { + _serializer = new ETFWriter(new MemoryStream()); + } + + protected override string Serialize(T obj) + { + throw new NotImplementedException(); + } + protected override T Deserialize(string json) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Discord.Net/Net/Rest/JsonRestClient.cs b/src/Discord.Net/Net/Rest/JsonRestClient.cs new file mode 100644 index 000000000..37bb093ab --- /dev/null +++ b/src/Discord.Net/Net/Rest/JsonRestClient.cs @@ -0,0 +1,37 @@ +using Discord.Logging; +using Newtonsoft.Json; + +namespace Discord.Net.Rest +{ + public class JsonRestClient : RestClient + { + private JsonSerializerSettings _deserializeSettings; + + public JsonRestClient(DiscordConfig config, string baseUrl, Logger logger) + : base(config, baseUrl, logger) + { + _deserializeSettings = new JsonSerializerSettings(); +#if TEST_RESPONSES + _deserializeSettings.CheckAdditionalContent = true; + _deserializeSettings.MissingMemberHandling = MissingMemberHandling.Error; +#else + _deserializeSettings.CheckAdditionalContent = false; + _deserializeSettings.MissingMemberHandling = MissingMemberHandling.Ignore; +#endif + } + + protected override string Serialize(T obj) + { + return JsonConvert.SerializeObject(obj); + } + + protected override T Deserialize(string json) + { +#if TEST_RESPONSES + if (string.IsNullOrEmpty(json)) + throw new Exception("API check failed: Response is empty."); +#endif + return JsonConvert.DeserializeObject(json, _deserializeSettings); + } + } +} diff --git a/src/Discord.Net/Net/Rest/RestClient.cs b/src/Discord.Net/Net/Rest/RestClient.cs index 727182037..a32771e0d 100644 --- a/src/Discord.Net/Net/Rest/RestClient.cs +++ b/src/Discord.Net/Net/Rest/RestClient.cs @@ -1,14 +1,15 @@ using Discord.API; +using Discord.ETF; using Discord.Logging; -using Newtonsoft.Json; using System; using System.Diagnostics; +using System.IO; using System.Threading; using System.Threading.Tasks; namespace Discord.Net.Rest { - public sealed partial class RestClient + public abstract partial class RestClient { private struct RestResults { @@ -32,8 +33,8 @@ namespace Discord.Net.Rest private readonly DiscordConfig _config; private readonly IRestEngine _engine; + private readonly ETFWriter _serializer; private string _token; - private JsonSerializerSettings _deserializeSettings; internal Logger Logger { get; } @@ -49,26 +50,17 @@ namespace Discord.Net.Rest } } - public RestClient(DiscordConfig config, string baseUrl, Logger logger) + protected RestClient(DiscordConfig config, string baseUrl, Logger logger) { _config = config; Logger = logger; #if !DOTNET5_4 - _engine = new RestSharpEngine(config, baseUrl, logger); + _engine = new RestSharpEngine(config, baseUrl, logger); #else _engine = new BuiltInEngine(config, baseUrl, logger); #endif - _deserializeSettings = new JsonSerializerSettings(); -#if TEST_RESPONSES - _deserializeSettings.CheckAdditionalContent = true; - _deserializeSettings.MissingMemberHandling = MissingMemberHandling.Error; -#else - _deserializeSettings.CheckAdditionalContent = false; - _deserializeSettings.MissingMemberHandling = MissingMemberHandling.Ignore; -#endif - if (Logger.Level >= LogSeverity.Verbose) { this.SentRequest += (s, e) => @@ -98,7 +90,7 @@ namespace Discord.Net.Rest OnSendingRequest(request); var results = await Send(request, true).ConfigureAwait(false); - var response = DeserializeResponse(results.Response); + var response = Deserialize(results.Response); OnSentRequest(request, response, results.Response, results.Milliseconds); return response; @@ -118,9 +110,8 @@ namespace Discord.Net.Rest if (request == null) throw new ArgumentNullException(nameof(request)); OnSendingRequest(request); - var requestJson = JsonConvert.SerializeObject(request.Payload); var results = await SendFile(request, true).ConfigureAwait(false); - var response = DeserializeResponse(results.Response); + var response = Deserialize(results.Response); OnSentRequest(request, response, results.Response, results.Milliseconds); return response; @@ -139,7 +130,7 @@ namespace Discord.Net.Rest object payload = request.Payload; string requestJson = null; if (payload != null) - requestJson = JsonConvert.SerializeObject(payload); + requestJson = Serialize(payload); Stopwatch stopwatch = Stopwatch.StartNew(); string responseJson = await _engine.Send(request.Method, request.Endpoint, requestJson, CancelToken).ConfigureAwait(false); @@ -159,13 +150,7 @@ namespace Discord.Net.Rest return new RestResults(responseJson, milliseconds); } - private T DeserializeResponse(string json) - { -#if TEST_RESPONSES - if (string.IsNullOrEmpty(json)) - throw new Exception("API check failed: Response is empty."); -#endif - return JsonConvert.DeserializeObject(json, _deserializeSettings); - } + protected abstract string Serialize(T obj); + protected abstract T Deserialize(string json); } } diff --git a/src/Discord.Net/Net/Rest/SharpRestEngine.cs b/src/Discord.Net/Net/Rest/SharpRestEngine.cs index 8ec300c11..325808702 100644 --- a/src/Discord.Net/Net/Rest/SharpRestEngine.cs +++ b/src/Discord.Net/Net/Rest/SharpRestEngine.cs @@ -11,7 +11,7 @@ using RestSharpClient = RestSharp.RestClient; namespace Discord.Net.Rest { - internal sealed class RestSharpEngine : IRestEngine + internal class RestSharpEngine : IRestEngine { private const int HR_SECURECHANNELFAILED = -2146233079; diff --git a/src/Discord.Net/Net/TimeoutException.cs b/src/Discord.Net/Net/TimeoutException.cs index 542ee542a..051eeb263 100644 --- a/src/Discord.Net/Net/TimeoutException.cs +++ b/src/Discord.Net/Net/TimeoutException.cs @@ -5,7 +5,7 @@ namespace Discord.Net #if NET46 [Serializable] #endif - public sealed class TimeoutException : OperationCanceledException + public class TimeoutException : OperationCanceledException { public TimeoutException() : base("An operation has timed out.") diff --git a/src/Discord.Net/Net/WebSocketException.cs b/src/Discord.Net/Net/WebSocketException.cs index d2c61b644..b845d90c4 100644 --- a/src/Discord.Net/Net/WebSocketException.cs +++ b/src/Discord.Net/Net/WebSocketException.cs @@ -2,7 +2,7 @@ namespace Discord.Net { - public sealed class WebSocketException : Exception + public class WebSocketException : Exception { public int Code { get; } public string Reason { get; } diff --git a/src/Discord.Net/Net/WebSockets/BuiltInEngine.cs b/src/Discord.Net/Net/WebSockets/BuiltInEngine.cs index 7c454715d..34224720f 100644 --- a/src/Discord.Net/Net/WebSockets/BuiltInEngine.cs +++ b/src/Discord.Net/Net/WebSockets/BuiltInEngine.cs @@ -12,7 +12,7 @@ using WebSocketClient = System.Net.WebSockets.ClientWebSocket; namespace Discord.Net.WebSockets { - internal sealed class BuiltInEngine : IWebSocketEngine + internal class BuiltInEngine : IWebSocketEngine { private const int ReceiveChunkSize = 12 * 1024; //12KB private const int SendChunkSize = 4 * 1024; //4KB @@ -81,7 +81,7 @@ namespace Discord.Net.WebSockets try { - result = await _webSocket.ReceiveAsync(new ArraySegment(buffer), cancelToken);//.ConfigureAwait(false); + result = await _webSocket.ReceiveAsync(new ArraySegment(buffer), cancelToken).ConfigureAwait(false); } catch (Win32Exception ex) when (ex.HResult == HR_TIMEOUT) { diff --git a/src/Discord.Net/Net/WebSockets/GatewaySocket.cs b/src/Discord.Net/Net/WebSockets/GatewaySocket.cs index bd2a8ab6a..1bbe3296d 100644 --- a/src/Discord.Net/Net/WebSockets/GatewaySocket.cs +++ b/src/Discord.Net/Net/WebSockets/GatewaySocket.cs @@ -2,6 +2,7 @@ using Discord.API.Client.GatewaySocket; using Discord.API.Client.Rest; using Discord.Logging; +using Discord.Net.Rest; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; @@ -11,20 +12,21 @@ using System.Threading.Tasks; namespace Discord.Net.WebSockets { - public sealed class GatewaySocket : WebSocket + public class GatewaySocket : WebSocket { + private RestClient _rest; private uint _lastSequence; private int _reconnects; - public string Token { get; internal set; } - public string SessionId { get; internal set; } + //public string Token { get; private set; } + public string SessionId { get; private set; } public event EventHandler ReceivedDispatch = delegate { }; private void OnReceivedDispatch(string type, JToken payload) => ReceivedDispatch(this, new WebSocketEventEventArgs(type, payload)); - public GatewaySocket(DiscordClient client, Logger logger) - : base(client, logger) + public GatewaySocket(DiscordConfig config, JsonSerializer serializer, Logger logger) + : base(config, serializer, logger) { Disconnected += async (s, e) => { @@ -32,17 +34,19 @@ namespace Discord.Net.WebSockets await Reconnect().ConfigureAwait(false); }; } - - public async Task Connect() + + public async Task Connect(RestClient rest, CancellationToken parentCancelToken) { - var gatewayResponse = await _client.ClientAPI.Send(new GatewayRequest()).ConfigureAwait(false); - Host = gatewayResponse.Url; - if (Logger.Level >= LogSeverity.Verbose) - Logger.Verbose($"Login successful, gateway: {gatewayResponse.Url}"); + _rest = rest; + //Token = rest.Token; + + var gatewayResponse = await rest.Send(new GatewayRequest()).ConfigureAwait(false); + Logger.Verbose($"Login successful, gateway: {gatewayResponse.Url}"); - await BeginConnect().ConfigureAwait(false); + Host = gatewayResponse.Url; + await BeginConnect(parentCancelToken).ConfigureAwait(false); if (SessionId == null) - SendIdentify(Token); + SendIdentify(_rest.Token); else SendResume(); } @@ -50,17 +54,17 @@ namespace Discord.Net.WebSockets { try { - var cancelToken = ParentCancelToken.Value; + var cancelToken = _parentCancelToken; if (_reconnects++ == 0) - await Task.Delay(_client.Config.ReconnectDelay, cancelToken).ConfigureAwait(false); + await Task.Delay(_config.ReconnectDelay, cancelToken).ConfigureAwait(false); else - await Task.Delay(_client.Config.FailedReconnectDelay, cancelToken).ConfigureAwait(false); + await Task.Delay(_config.FailedReconnectDelay, cancelToken).ConfigureAwait(false); while (!cancelToken.IsCancellationRequested) { try { - await Connect().ConfigureAwait(false); + await Connect(_rest, _parentCancelToken).ConfigureAwait(false); break; } catch (OperationCanceledException) { throw; } @@ -68,20 +72,25 @@ namespace Discord.Net.WebSockets { Logger.Error("Reconnect failed", ex); //Net is down? We can keep trying to reconnect until the user runs Disconnect() - await Task.Delay(_client.Config.FailedReconnectDelay, cancelToken).ConfigureAwait(false); + await Task.Delay(_config.FailedReconnectDelay, cancelToken).ConfigureAwait(false); } } } catch (OperationCanceledException) { } } - public Task Disconnect() => _taskManager.Stop(true); + public async Task Disconnect() + { + await _taskManager.Stop(true).ConfigureAwait(false); + //Token = null; + SessionId = null; + } protected override async Task Run() { List tasks = new List(); tasks.AddRange(_engine.GetTasks(CancelToken)); tasks.Add(HeartbeatAsync(CancelToken)); - await _taskManager.Start(tasks, _cancelTokenSource).ConfigureAwait(false); + await _taskManager.Start(tasks, _cancelSource).ConfigureAwait(false); } protected override Task Cleanup() { @@ -103,11 +112,16 @@ namespace Discord.Net.WebSockets { case OpCodes.Dispatch: { - OnReceivedDispatch(msg.Type, msg.Payload as JToken); + if (msg.Type == "READY") + SessionId = (msg.Payload as JToken).Value("session_id"); + + OnReceivedDispatch(msg.Type, msg.Payload as JToken); + if (msg.Type == "READY" || msg.Type == "RESUMED") { + _heartbeatInterval = (msg.Payload as JToken).Value("heartbeat_interval"); _reconnects = 0; - await EndConnect(); //Complete the connect + await EndConnect().ConfigureAwait(false); //Complete the connect } } break; @@ -142,7 +156,7 @@ namespace Discord.Net.WebSockets Version = 3, Token = token, Properties = props, - LargeThreshold = _client.Config.UseLargeThreshold ? 100 : (int?)null, + LargeThreshold = _config.UseLargeThreshold ? 100 : (int?)null, UseCompression = true }; QueueMessage(msg); @@ -163,11 +177,6 @@ namespace Discord.Net.WebSockets public void SendRequestMembers(ulong serverId, string query, int limit) => QueueMessage(new RequestMembersCommand { GuildId = serverId, Query = query, Limit = limit }); - internal void StartHeartbeat(int interval) - { - _heartbeatInterval = interval; - } - //Cancel if either DiscordClient.Disconnect is called, data socket errors or timeout is reached public override void WaitForConnection(CancellationToken cancelToken) => base.WaitForConnection(CancellationTokenSource.CreateLinkedTokenSource(cancelToken, CancelToken).Token); diff --git a/src/Discord.Net/Net/WebSockets/WS4NetEngine.cs b/src/Discord.Net/Net/WebSockets/WS4NetEngine.cs index 785e4f813..98eb7db02 100644 --- a/src/Discord.Net/Net/WebSockets/WS4NetEngine.cs +++ b/src/Discord.Net/Net/WebSockets/WS4NetEngine.cs @@ -10,7 +10,7 @@ using WebSocketClient = WebSocket4Net.WebSocket; namespace Discord.Net.WebSockets { - internal sealed class WS4NetEngine : IWebSocketEngine + internal class WS4NetEngine : IWebSocketEngine { private readonly DiscordConfig _config; private readonly ConcurrentQueue _sendQueue; diff --git a/src/Discord.Net/Net/WebSockets/WebSocket.cs b/src/Discord.Net/Net/WebSockets/WebSocket.cs index accf31a07..f0052f29a 100644 --- a/src/Discord.Net/Net/WebSockets/WebSocket.cs +++ b/src/Discord.Net/Net/WebSockets/WebSocket.cs @@ -14,18 +14,18 @@ namespace Discord.Net.WebSockets { private readonly AsyncLock _lock; protected readonly IWebSocketEngine _engine; - protected readonly DiscordClient _client; + protected readonly DiscordConfig _config; protected readonly ManualResetEventSlim _connectedEvent; protected readonly TaskManager _taskManager; protected readonly JsonSerializer _serializer; - protected CancellationTokenSource _cancelTokenSource; + protected CancellationTokenSource _cancelSource; + protected CancellationToken _parentCancelToken; protected int _heartbeatInterval; private DateTime _lastHeartbeat; - + /// Gets the logger used for this client. protected internal Logger Logger { get; } public CancellationToken CancelToken { get; private set; } - public CancellationToken? ParentCancelToken { get; set; } public string Host { get; set; } /// Gets the current connection state of this client. @@ -38,11 +38,11 @@ namespace Discord.Net.WebSockets private void OnDisconnected(bool wasUnexpected, Exception error) => Disconnected(this, new DisconnectedEventArgs(wasUnexpected, error)); - public WebSocket(DiscordClient client, Logger logger) + public WebSocket(DiscordConfig config, JsonSerializer serializer, Logger logger) { - _client = client; + _config = config; + _serializer = serializer; Logger = logger; - _serializer = client.Serializer; _lock = new AsyncLock(); _taskManager = new TaskManager(Cleanup); @@ -50,9 +50,9 @@ namespace Discord.Net.WebSockets _connectedEvent = new ManualResetEventSlim(false); #if !DOTNET5_4 - _engine = new WS4NetEngine(client.Config, _taskManager); + _engine = new WS4NetEngine(config, _taskManager); #else - _engine = new BuiltInEngine(client.Config); + _engine = new BuiltInEngine(config); #endif _engine.BinaryMessage += (s, e) => { @@ -69,18 +69,20 @@ namespace Discord.Net.WebSockets _engine.TextMessage += (s, e) => ProcessMessage(e.Message).Wait(); } - protected async Task BeginConnect() + protected async Task BeginConnect(CancellationToken parentCancelToken) { try { using (await _lock.LockAsync().ConfigureAwait(false)) { + _parentCancelToken = parentCancelToken; + await _taskManager.Stop().ConfigureAwait(false); _taskManager.ClearException(); State = ConnectionState.Connecting; - _cancelTokenSource = new CancellationTokenSource(); - CancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelTokenSource.Token, ParentCancelToken.Value).Token; + _cancelSource = new CancellationTokenSource(); + CancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelSource.Token, parentCancelToken).Token; _lastHeartbeat = DateTime.UtcNow; await _engine.Connect(Host, CancelToken).ConfigureAwait(false); @@ -117,7 +119,7 @@ namespace Discord.Net.WebSockets State = ConnectionState.Disconnecting; await _engine.Disconnect().ConfigureAwait(false); - _cancelTokenSource = null; + _cancelSource = null; _connectedEvent.Reset(); if (oldState == ConnectionState.Connecting || oldState == ConnectionState.Connected) @@ -127,9 +129,11 @@ namespace Discord.Net.WebSockets Logger.Info("Disconnected"); else Logger.Error("Disconnected", ex); + State = ConnectionState.Disconnected; OnDisconnected(!_taskManager.WasStopExpected, _taskManager.Exception); } - State = ConnectionState.Disconnected; + else + State = ConnectionState.Disconnected; } protected virtual Task ProcessMessage(string json) @@ -154,7 +158,7 @@ namespace Discord.Net.WebSockets { while (!cancelToken.IsCancellationRequested) { - if (this.State == ConnectionState.Connected) + if (this.State == ConnectionState.Connected && _heartbeatInterval > 0) { SendHeartbeat(); await Task.Delay(_heartbeatInterval, cancelToken).ConfigureAwait(false); @@ -172,7 +176,7 @@ namespace Discord.Net.WebSockets { try { - if (!_connectedEvent.Wait(_client.Config.ConnectionTimeout, cancelToken)) + if (!_connectedEvent.Wait(_config.ConnectionTimeout, cancelToken)) { if (State != ConnectionState.Connected) throw new TimeoutException(); diff --git a/src/Discord.Net/ProfileEventArgs.cs b/src/Discord.Net/ProfileEventArgs.cs deleted file mode 100644 index 42c8fd6ec..000000000 --- a/src/Discord.Net/ProfileEventArgs.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace Discord -{ - public class ProfileEventArgs : EventArgs - { - public Profile Profile { get; } - - public ProfileEventArgs(Profile profile) - { - Profile = profile; - } - } -} diff --git a/src/Discord.Net/RelativeDirection.cs b/src/Discord.Net/RelativeDirection.cs deleted file mode 100644 index 787ad5844..000000000 --- a/src/Discord.Net/RelativeDirection.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Discord -{ - public enum RelativeDirection - { - Before, After - } -} diff --git a/src/Discord.Net/ServiceManager.cs b/src/Discord.Net/ServiceManager.cs index cb1e8e89a..0e6533b90 100644 --- a/src/Discord.Net/ServiceManager.cs +++ b/src/Discord.Net/ServiceManager.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; namespace Discord { - public sealed class ServiceManager + public class ServiceManager { private readonly Dictionary _services; diff --git a/src/Discord.Net/TaskManager.cs b/src/Discord.Net/TaskManager.cs index 4934e53d9..7c9a667ce 100644 --- a/src/Discord.Net/TaskManager.cs +++ b/src/Discord.Net/TaskManager.cs @@ -9,37 +9,41 @@ using System.Threading.Tasks; namespace Discord { /// Helper class used to manage several tasks and keep them in sync. If any single task errors or stops, all other tasks will also be stopped. - public sealed class TaskManager + public class TaskManager { private readonly AsyncLock _lock; private readonly Func _stopAction; + private ExceptionDispatchInfo _stopReason; private CancellationTokenSource _cancelSource; private Task _task; - public bool WasStopExpected => _wasStopExpected; - private bool _wasStopExpected; + public bool StopOnCompletion { get; } + public bool WasStopExpected { get; private set; } public Exception Exception => _stopReason?.SourceException; - private ExceptionDispatchInfo _stopReason; - internal TaskManager() + internal TaskManager(bool stopOnCompletion) { _lock = new AsyncLock(); + StopOnCompletion = stopOnCompletion; } - public TaskManager(Action stopAction) - : this() + public TaskManager(Action stopAction, bool stopOnCompletion = true) + : this(stopOnCompletion) { _stopAction = TaskHelper.ToAsync(stopAction); } - public TaskManager(Func stopAction) - : this() + public TaskManager(Func stopAction, bool stopOnCompletion = true) + : this(stopOnCompletion) { _stopAction = stopAction; } public async Task Start(IEnumerable tasks, CancellationTokenSource cancelSource) { + if (tasks == null) throw new ArgumentNullException(nameof(tasks)); + if (cancelSource == null) throw new ArgumentNullException(nameof(cancelSource)); + while (true) { var task = _task; @@ -54,27 +58,36 @@ namespace Discord continue; //Another thread sneaked in and started this manager before we got a lock, loop and try again _stopReason = null; - _wasStopExpected = false; + WasStopExpected = false; Task[] tasksArray = tasks.ToArray(); - Task anyTask = Task.WhenAny(tasksArray); - Task allTasks = Task.WhenAll(tasksArray); _task = Task.Run(async () => { - //Wait for the first task to stop or error - Task firstTask = await anyTask.ConfigureAwait(false); - - //Signal the rest of the tasks to stop - if (firstTask.Exception != null) - await SignalError(firstTask.Exception).ConfigureAwait(false); - else - await SignalStop().ConfigureAwait(false); - - //Wait for the other tasks, and signal their errors too just in case - try { await allTasks.ConfigureAwait(false); } - catch (AggregateException ex) { await SignalError(ex.InnerExceptions.First()).ConfigureAwait(false); } - catch (Exception ex) { await SignalError(ex).ConfigureAwait(false); } + if (tasksArray.Length > 0) + { + Task anyTask = tasksArray.Length > 0 ? Task.WhenAny(tasksArray) : null; + Task allTasks = tasksArray.Length > 0 ? Task.WhenAll(tasksArray) : null; + //Wait for the first task to stop or error + Task firstTask = await anyTask.ConfigureAwait(false); + + //Signal the rest of the tasks to stop + if (firstTask.Exception != null) + await SignalError(firstTask.Exception).ConfigureAwait(false); + else if (StopOnCompletion) //Unless we allow for natural completions + await SignalStop().ConfigureAwait(false); + + //Wait for the other tasks, and signal their errors too just in case + try { await allTasks.ConfigureAwait(false); } + catch (AggregateException ex) { await SignalError(ex.InnerExceptions.First()).ConfigureAwait(false); } + catch (Exception ex) { await SignalError(ex).ConfigureAwait(false); } + } + + if (!StopOnCompletion && !_cancelSource.IsCancellationRequested) + { + try { await Task.Delay(-1, _cancelSource.Token).ConfigureAwait(false); } //Pause until TaskManager is stopped + catch (OperationCanceledException) { } + } //Run the cleanup function within our lock if (_stopAction != null) @@ -92,13 +105,9 @@ namespace Discord using (await _lock.LockAsync().ConfigureAwait(false)) { if (isExpected) - _wasStopExpected = true; - - if (_task == null) return; //Are we running? - if (_cancelSource.IsCancellationRequested) return; + WasStopExpected = true; - if (_cancelSource != null) - _cancelSource.Cancel(); + Cancel(); } } public async Task Stop(bool isExpected = false) @@ -107,14 +116,11 @@ namespace Discord using (await _lock.LockAsync().ConfigureAwait(false)) { if (isExpected) - _wasStopExpected = true; + WasStopExpected = true; //Cache the task so we still have something to await if Cleanup is run really quickly - task = _task; - if (task == null) return; //Are we running? - - if (!_cancelSource.IsCancellationRequested && _cancelSource != null) - _cancelSource.Cancel(); + task = _task ?? TaskHelper.CompletedTask; + Cancel(); } await task.ConfigureAwait(false); } @@ -125,9 +131,7 @@ namespace Discord { if (_stopReason != null) return; - _stopReason = ExceptionDispatchInfo.Capture(ex); - if (_cancelSource != null) - _cancelSource.Cancel(); + Cancel(ex); } } public async Task Error(Exception ex) @@ -139,15 +143,20 @@ namespace Discord //Cache the task so we still have something to await if Cleanup is run really quickly task = _task ?? TaskHelper.CompletedTask; - if (!_cancelSource.IsCancellationRequested) - { - _stopReason = ExceptionDispatchInfo.Capture(ex); - if (_cancelSource != null) - _cancelSource.Cancel(); - } + Cancel(ex); } await task.ConfigureAwait(false); } + private void Cancel(Exception ex = null) + { + var source = _cancelSource; + if (source != null && !source.IsCancellationRequested) + { + if (ex != null) + _stopReason = ExceptionDispatchInfo.Capture(ex); + _cancelSource.Cancel(); + } + } /// Throws an exception if one was captured. public void ThrowException() @@ -160,7 +169,7 @@ namespace Discord using (_lock.Lock()) { _stopReason = null; - _wasStopExpected = false; + WasStopExpected = false; } } } diff --git a/src/Discord.Net/project.json b/src/Discord.Net/project.json index be0791385..ca2e42f49 100644 --- a/src/Discord.Net/project.json +++ b/src/Discord.Net/project.json @@ -15,8 +15,9 @@ "url": "git://github.com/RogueException/Discord.Net" }, "compile": [ "**/*.cs", "../Discord.Net.Shared/*.cs" ], - + "compilationOptions": { + "allowUnsafe": true, "warningsAsErrors": true }, @@ -50,6 +51,8 @@ "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.Reflection": "4.1.0-beta-23516", + "System.Reflection.Emit.Lightweight": "4.0.1-beta-23516", "System.Runtime.InteropServices": "4.0.21-beta-23516", "System.Security.Cryptography.Algorithms": "4.0.0-beta-23516", "System.Text.RegularExpressions": "4.0.11-beta-23516",