diff --git a/Discord.Net.sln b/Discord.Net.sln index db11e8b30..bfcda2fab 100644 --- a/Discord.Net.sln +++ b/Discord.Net.sln @@ -1,17 +1,13 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.24720.0 +VisualStudioVersion = 14.0.25123.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{8D7989F0-66CE-4DBB-8230-D8C811E9B1D7}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "netplatform", "netplatform", "{EA68EBE2-51C8-4440-9EF7-D633C90A5D35}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Discord.Net", "src\Discord.Net\Discord.Net.xproj", "{ACFB060B-EC8A-4926-B293-04C01E17EE23}" -EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Discord.Net.Commands", "src\Discord.Net.Commands\Discord.Net.Commands.xproj", "{19793545-EF89-48F4-8100-3EBAAD0A9141}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "net45", "net45", "{DF03D4E8-38F6-4FE1-BC52-E38124BE8AFD}" +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Discord.Net", "src\Discord.Net\Discord.Net.xproj", "{2C91BDD7-621D-460F-B768-EAD106D9BA62}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{6317A2E6-8E36-4C3E-949B-3F10EC888AB9}" EndProject @@ -22,104 +18,44 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution global.json = global.json EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net", "src\Discord.Net.Net45\Discord.Net.csproj", "{8D71A857-879A-4A10-859E-5FF824ED6688}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net.Commands", "src\Discord.Net.Commands.Net45\Discord.Net.Commands.csproj", "{1B5603B4-6F8F-4289-B945-7BAAE523D740}" -EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Discord.Net.Modules", "src\Discord.Net.Modules\Discord.Net.Modules.xproj", "{01584E8A-78DA-486F-9EF9-A894E435841B}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "net45", "net45", "{628A40F4-2D06-4BCE-82EF-0EE70DD5C1CA}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net.Modules", "src\Discord.Net.Modules.Net45\Discord.Net.Modules.csproj", "{3091164F-66AE-4543-A63D-167C1116241D}" -EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Discord.Net.Audio", "src\Discord.Net.Audio\Discord.Net.Audio.xproj", "{DFF7AFE3-CA77-4109-BADE-B4B49A4F6648}" -EndProject -Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Discord.Net.Shared", "src\Discord.Net.Shared\Discord.Net.Shared.shproj", "{2875DEB5-F248-4105-8EA2-5141E3DE8025}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net.Audio", "src\Discord.Net.Audio.Net45\Discord.Net.Audio.csproj", "{7BFEF748-B934-4621-9B11-6302E3A9F6B3}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net.Net45", "src\Discord.Net\Discord.Net.Net45.csproj", "{C6A50D24-CBD3-4E76-852C-4DCA60BBD608}" EndProject Global - GlobalSection(SharedMSBuildProjectFiles) = preSolution - src\Discord.Net.Shared\Discord.Net.Shared.projitems*{2875deb5-f248-4105-8ea2-5141e3de8025}*SharedItemsImports = 13 - src\Discord.Net.Shared\Discord.Net.Shared.projitems*{7bfef748-b934-4621-9b11-6302e3a9f6b3}*SharedItemsImports = 4 - src\Discord.Net.Shared\Discord.Net.Shared.projitems*{1b5603b4-6f8f-4289-b945-7baae523d740}*SharedItemsImports = 4 - src\Discord.Net.Shared\Discord.Net.Shared.projitems*{3091164f-66ae-4543-a63d-167c1116241d}*SharedItemsImports = 4 - src\Discord.Net.Shared\Discord.Net.Shared.projitems*{8d71a857-879a-4a10-859e-5ff824ed6688}*SharedItemsImports = 4 - EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU FullDebug|Any CPU = FullDebug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {ACFB060B-EC8A-4926-B293-04C01E17EE23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {ACFB060B-EC8A-4926-B293-04C01E17EE23}.Debug|Any CPU.Build.0 = Debug|Any CPU - {ACFB060B-EC8A-4926-B293-04C01E17EE23}.FullDebug|Any CPU.ActiveCfg = Debug|Any CPU - {ACFB060B-EC8A-4926-B293-04C01E17EE23}.FullDebug|Any CPU.Build.0 = Debug|Any CPU - {ACFB060B-EC8A-4926-B293-04C01E17EE23}.Release|Any CPU.ActiveCfg = Release|Any CPU - {ACFB060B-EC8A-4926-B293-04C01E17EE23}.Release|Any CPU.Build.0 = Release|Any CPU - {19793545-EF89-48F4-8100-3EBAAD0A9141}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {19793545-EF89-48F4-8100-3EBAAD0A9141}.Debug|Any CPU.Build.0 = Debug|Any CPU - {19793545-EF89-48F4-8100-3EBAAD0A9141}.FullDebug|Any CPU.ActiveCfg = Debug|Any CPU - {19793545-EF89-48F4-8100-3EBAAD0A9141}.FullDebug|Any CPU.Build.0 = Debug|Any CPU - {19793545-EF89-48F4-8100-3EBAAD0A9141}.Release|Any CPU.ActiveCfg = Release|Any CPU - {19793545-EF89-48F4-8100-3EBAAD0A9141}.Release|Any CPU.Build.0 = Release|Any CPU + {2C91BDD7-621D-460F-B768-EAD106D9BA62}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2C91BDD7-621D-460F-B768-EAD106D9BA62}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2C91BDD7-621D-460F-B768-EAD106D9BA62}.FullDebug|Any CPU.ActiveCfg = Debug|Any CPU + {2C91BDD7-621D-460F-B768-EAD106D9BA62}.FullDebug|Any CPU.Build.0 = Debug|Any CPU + {2C91BDD7-621D-460F-B768-EAD106D9BA62}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2C91BDD7-621D-460F-B768-EAD106D9BA62}.Release|Any CPU.Build.0 = Release|Any CPU {855D6B1D-847B-42DA-BE6A-23683EA89511}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {855D6B1D-847B-42DA-BE6A-23683EA89511}.Debug|Any CPU.Build.0 = Debug|Any CPU {855D6B1D-847B-42DA-BE6A-23683EA89511}.FullDebug|Any CPU.ActiveCfg = Debug|Any CPU {855D6B1D-847B-42DA-BE6A-23683EA89511}.FullDebug|Any CPU.Build.0 = Debug|Any CPU {855D6B1D-847B-42DA-BE6A-23683EA89511}.Release|Any CPU.ActiveCfg = Release|Any CPU {855D6B1D-847B-42DA-BE6A-23683EA89511}.Release|Any CPU.Build.0 = Release|Any CPU - {8D71A857-879A-4A10-859E-5FF824ED6688}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8D71A857-879A-4A10-859E-5FF824ED6688}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8D71A857-879A-4A10-859E-5FF824ED6688}.FullDebug|Any CPU.ActiveCfg = Debug|Any CPU - {8D71A857-879A-4A10-859E-5FF824ED6688}.FullDebug|Any CPU.Build.0 = Debug|Any CPU - {8D71A857-879A-4A10-859E-5FF824ED6688}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8D71A857-879A-4A10-859E-5FF824ED6688}.Release|Any CPU.Build.0 = Release|Any CPU - {1B5603B4-6F8F-4289-B945-7BAAE523D740}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1B5603B4-6F8F-4289-B945-7BAAE523D740}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1B5603B4-6F8F-4289-B945-7BAAE523D740}.FullDebug|Any CPU.ActiveCfg = Debug|Any CPU - {1B5603B4-6F8F-4289-B945-7BAAE523D740}.FullDebug|Any CPU.Build.0 = Debug|Any CPU - {1B5603B4-6F8F-4289-B945-7BAAE523D740}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1B5603B4-6F8F-4289-B945-7BAAE523D740}.Release|Any CPU.Build.0 = Release|Any CPU - {01584E8A-78DA-486F-9EF9-A894E435841B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {01584E8A-78DA-486F-9EF9-A894E435841B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {01584E8A-78DA-486F-9EF9-A894E435841B}.FullDebug|Any CPU.ActiveCfg = Debug|Any CPU - {01584E8A-78DA-486F-9EF9-A894E435841B}.FullDebug|Any CPU.Build.0 = Debug|Any CPU - {01584E8A-78DA-486F-9EF9-A894E435841B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {01584E8A-78DA-486F-9EF9-A894E435841B}.Release|Any CPU.Build.0 = Release|Any CPU - {3091164F-66AE-4543-A63D-167C1116241D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3091164F-66AE-4543-A63D-167C1116241D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3091164F-66AE-4543-A63D-167C1116241D}.FullDebug|Any CPU.ActiveCfg = Debug|Any CPU - {3091164F-66AE-4543-A63D-167C1116241D}.FullDebug|Any CPU.Build.0 = Debug|Any CPU - {3091164F-66AE-4543-A63D-167C1116241D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3091164F-66AE-4543-A63D-167C1116241D}.Release|Any CPU.Build.0 = Release|Any CPU - {DFF7AFE3-CA77-4109-BADE-B4B49A4F6648}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DFF7AFE3-CA77-4109-BADE-B4B49A4F6648}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DFF7AFE3-CA77-4109-BADE-B4B49A4F6648}.FullDebug|Any CPU.ActiveCfg = Debug|Any CPU - {DFF7AFE3-CA77-4109-BADE-B4B49A4F6648}.FullDebug|Any CPU.Build.0 = Debug|Any CPU - {DFF7AFE3-CA77-4109-BADE-B4B49A4F6648}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DFF7AFE3-CA77-4109-BADE-B4B49A4F6648}.Release|Any CPU.Build.0 = Release|Any CPU - {7BFEF748-B934-4621-9B11-6302E3A9F6B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7BFEF748-B934-4621-9B11-6302E3A9F6B3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7BFEF748-B934-4621-9B11-6302E3A9F6B3}.FullDebug|Any CPU.ActiveCfg = Debug|Any CPU - {7BFEF748-B934-4621-9B11-6302E3A9F6B3}.FullDebug|Any CPU.Build.0 = Debug|Any CPU - {7BFEF748-B934-4621-9B11-6302E3A9F6B3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7BFEF748-B934-4621-9B11-6302E3A9F6B3}.Release|Any CPU.Build.0 = Release|Any CPU + {C6A50D24-CBD3-4E76-852C-4DCA60BBD608}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C6A50D24-CBD3-4E76-852C-4DCA60BBD608}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C6A50D24-CBD3-4E76-852C-4DCA60BBD608}.FullDebug|Any CPU.ActiveCfg = Debug|Any CPU + {C6A50D24-CBD3-4E76-852C-4DCA60BBD608}.FullDebug|Any CPU.Build.0 = Debug|Any CPU + {C6A50D24-CBD3-4E76-852C-4DCA60BBD608}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C6A50D24-CBD3-4E76-852C-4DCA60BBD608}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {EA68EBE2-51C8-4440-9EF7-D633C90A5D35} = {8D7989F0-66CE-4DBB-8230-D8C811E9B1D7} - {ACFB060B-EC8A-4926-B293-04C01E17EE23} = {EA68EBE2-51C8-4440-9EF7-D633C90A5D35} - {19793545-EF89-48F4-8100-3EBAAD0A9141} = {EA68EBE2-51C8-4440-9EF7-D633C90A5D35} - {DF03D4E8-38F6-4FE1-BC52-E38124BE8AFD} = {8D7989F0-66CE-4DBB-8230-D8C811E9B1D7} + {2C91BDD7-621D-460F-B768-EAD106D9BA62} = {EA68EBE2-51C8-4440-9EF7-D633C90A5D35} {855D6B1D-847B-42DA-BE6A-23683EA89511} = {6317A2E6-8E36-4C3E-949B-3F10EC888AB9} - {8D71A857-879A-4A10-859E-5FF824ED6688} = {DF03D4E8-38F6-4FE1-BC52-E38124BE8AFD} - {1B5603B4-6F8F-4289-B945-7BAAE523D740} = {DF03D4E8-38F6-4FE1-BC52-E38124BE8AFD} - {01584E8A-78DA-486F-9EF9-A894E435841B} = {EA68EBE2-51C8-4440-9EF7-D633C90A5D35} - {3091164F-66AE-4543-A63D-167C1116241D} = {DF03D4E8-38F6-4FE1-BC52-E38124BE8AFD} - {DFF7AFE3-CA77-4109-BADE-B4B49A4F6648} = {EA68EBE2-51C8-4440-9EF7-D633C90A5D35} - {2875DEB5-F248-4105-8EA2-5141E3DE8025} = {DF03D4E8-38F6-4FE1-BC52-E38124BE8AFD} - {7BFEF748-B934-4621-9B11-6302E3A9F6B3} = {DF03D4E8-38F6-4FE1-BC52-E38124BE8AFD} + {628A40F4-2D06-4BCE-82EF-0EE70DD5C1CA} = {8D7989F0-66CE-4DBB-8230-D8C811E9B1D7} + {C6A50D24-CBD3-4E76-852C-4DCA60BBD608} = {628A40F4-2D06-4BCE-82EF-0EE70DD5C1CA} EndGlobalSection EndGlobal diff --git a/NuGet.config b/NuGet.config new file mode 100644 index 000000000..91bc2845a --- /dev/null +++ b/NuGet.config @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/docs/features/commands.rst b/docs/features/commands.rst index e6bd6bf82..8abfb18a9 100644 --- a/docs/features/commands.rst +++ b/docs/features/commands.rst @@ -1,7 +1,7 @@ |stub| Commands =============== -The `Discord.Net.Commands`_ package extends DiscordClient with a built-in Commands Handler. +The `Discord.Net.Commands`_ package DiscordBotClient extends DiscordClient with support for commands. .. _Discord.Net.Commands: https://www.nuget.org/packages/Discord.Net.Commands diff --git a/docs/features/events.rst b/docs/features/events.rst index d782c438e..2cfe27f54 100644 --- a/docs/features/events.rst +++ b/docs/features/events.rst @@ -3,19 +3,21 @@ Events Usage ----- -Messages from the Discord server are exposed via events on the DiscordClient class and follow the standard EventHandler C# pattern. +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. + 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. -Ready ------ - -The Ready Event is raised only once, when your client finishes processing the READY packet from Discord. +Connection State +---------------- -This has replaced the previous "Connected" event, and indicates that it is safe to begin retrieving users, channels, or servers from the cache. +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 -------- @@ -24,7 +26,7 @@ Messages Example of MessageReceived: -.. code-block:: csharp6 +.. code-block:: c# // (Preface: Echo Bots are discouraged, make sure your bot is not running in a public server if you use them) @@ -54,7 +56,7 @@ There are several user events: Examples: -.. code-block:: csharp6 +.. code-block:: c# // Register a Hook into the UserBanned event using a Lambda _client.UserBanned += async (s, e) => { diff --git a/docs/features/logging.rst b/docs/features/logging.rst index c2f9d6ce3..4b9f254a5 100644 --- a/docs/features/logging.rst +++ b/docs/features/logging.rst @@ -2,12 +2,12 @@ Logging ======= Discord.Net will log all of its events/exceptions using a built-in LogManager. -This LogManager can be accessed through ``DiscordClient.Log`` +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. +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. @@ -17,25 +17,19 @@ 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. +You can use DiscordClient.Log.Log(LogSeverity, Source, Message, Exception), or one of the shortcut helpers, to log data. Example: - -.. code-block:: csharp6 +.. 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); }; - -.. warning:: - - Starting in Discord.Net 1.0, you will not be able to log your own messages. You will need to create your own Logging manager, or use a pre-existing one. - Example ------- .. literalinclude:: /samples/logging.cs - :language: csharp6 + :language: c# :tab-width: 2 diff --git a/docs/features/permissions.rst b/docs/features/permissions.rst index aa3856807..058fe07cf 100644 --- a/docs/features/permissions.rst +++ b/docs/features/permissions.rst @@ -1,21 +1,8 @@ Permissions =========== -|outdated| - There are two types of permissions: *Channel Permissions* and *Server Permissions*. -Permission Overrides --------------------- - -Channel Permissions are expressed using an enum, ``PermValue``. - -The three states are fairly straightforward - - -``PermValue.Allow``: Allow the user to perform a permission. -``PermValue.Deny``: Deny the user to perform a permission. -``PermValue.Inherit``: The user will inherit the permission from its role. - Channel Permissions ------------------- Channel Permissions are controlled using a set of flags: @@ -42,24 +29,27 @@ Speak Voice Speak in a voice channel. UseVoiceActivation Voice Use Voice Activation in a text channel (for large channels where PTT is preferred) ======================= ======= ============== -Each flag is a PermValue; see the section above. +If a user has a permission, the value is true. Otherwise, it must be null. -Setting Channel Permissions ---------------------------- +Dual Channel Permissions +------------------------ +You may also access a user's permissions in a channel with the DualChannelPermissions class. +Unlike normal ChannelPermissions, DualChannelPermissions hold three values: -To set channel permissions, create a new ``ChannelPermissionOverrides``, and specify the flags/values that you want to override. +If a user has a permission, the value is true. If a user is denied a permission, it will be false. If the permission is not set, the value will return null. -Then, update the user, by doing ``Channel.AddPermissionsRule(_user, _overwrites);`` +Setting Channel Permissions +--------------------------- -Roles ------ +To set channel permissions, you may use either two ChannelPermissions, or one DualChannelPermissions. -Accessing/modifying permissions for roles is done the same way as user permissions, just using the overload for a Role. See above sections. +In the case of using two Channel Permissions, you must create one list of allowed permissions, and one list of denied permissions. +Otherwise, you can use a single DualChannelPermissions. Server Permissions ------------------ -Server Permissions can be viewed with ``User.ServerPermissions``, but **at the time of this writing** cannot be set. +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: @@ -71,7 +61,11 @@ KickMembers Server Kick users from the server. They can still rejoi 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. -======================= ======= ============== + +Roles +----- + +Managing permissions for roles is much easier than for users in channels. For roles, just access the flag under `Role.Permissions`. Example ------- diff --git a/docs/features/server-management.rst b/docs/features/server-management.rst index 765fd4e0f..d555875a8 100644 --- a/docs/features/server-management.rst +++ b/docs/features/server-management.rst @@ -10,7 +10,7 @@ You can create Channels, Invites, and Roles on a server using the CreateChannel, You may also edit a server's name, icon, and region. -.. code-block:: csharp6 +.. code-block:: c# // Create a Channel and retrieve the Channel object var _channel = await _server.CreateChannel("announcements", ChannelType.Text); diff --git a/docs/features/user-management.rst b/docs/features/user-management.rst index cf3305312..972b3ab4b 100644 --- a/docs/features/user-management.rst +++ b/docs/features/user-management.rst @@ -6,7 +6,7 @@ Banning To ban a user, invoke the Ban function on a Server object. -.. code-block:: csharp6 +.. code-block:: c# _server.Ban(_user, 30); @@ -17,6 +17,6 @@ Kicking To kick a user, invoke the Kick function on the User. -.. code-block:: csharp6 +.. code-block:: c# _user.Kick(); diff --git a/docs/features/voice.rst b/docs/features/voice.rst index a4dddeff5..fc6867b58 100644 --- a/docs/features/voice.rst +++ b/docs/features/voice.rst @@ -1,180 +1,13 @@ -Voice -===== +|stub| Voice +================= -Installation ------------- - -Before setting up the AudioService, you must first install the package `from NuGet`_ or `GitHub`_. - -Add the package to your solution, and then import the namespace ``Discord.Audio``. - -.. _from NuGet: https://www.nuget.org/packages/Discord.Net.Audio/0.9.0-rc3 -.. _GitHub: https://github.com/RogueException/Discord.Net/tree/master/src/Discord.Net.Audio - -Setup ------ - -To use audio, you must install the AudioService to your DiscordClient. - -.. code-block:: csharp6 - - var _client = new DiscordClient(); - - _client.UsingAudio(x => // Opens an AudioConfigBuilder so we can configure our AudioService - { - x.Mode = AudioMode.Outgoing; // Tells the AudioService that we will only be sending audio - }); - -Joining a Channel ------------------ - -Joining Voice Channels is pretty straight-forward, and is required to send Audio. This will also allow us to get an IAudioClient, which we will later use to send Audio. - -.. code-block:: csharp6 - - var voiceChannel = _client.FindServers("Music Bot Server").FirstOrDefault().VoiceChannels.FirstOrDefault(); // Finds the first VoiceChannel on the server 'Music Bot Server' - - var _vClient = await _client.GetService() // We use GetService to find the AudioService that we installed earlier. In previous versions, this was equivelent to _client.Audio() - .Join(VoiceChannel); // Join the Voice Channel, and return the IAudioClient. - -The client will sustain a connection to this channel until it is kicked, disconnected from Discord, or told to Disconnect. - -The IAudioClient ----------------- - -The IAudioClient is used to connect/disconnect to/from a Voice Channel, and to send audio to that Voice Channel. - -.. function:: IAudioClient.Disconnect(); - - Disconnects the IAudioClient from the Voice Server. - - -.. function:: IAudioClient.Join(Channel); - - Moves the IAudioClient to another channel on the Voice Server, or starts a connection if one has already been terminated. - -.. note:: - - Because versions previous to 0.9 do not discretely differentiate between Text and Voice Channels, you may want to ensure that users cannot request the audio client to join a text channel, as this will throw an exception, leading to potentially unexpected behavior - -.. function:: IAudioClient.Wait(); - - Blocks the current thread until the sending audio buffer has cleared out. - -.. function:: IAudioClient.Clear(); - - Clears the sending audio buffer. - -.. function:: IAudioClient.Send(byte[] data, int offset, int count); - - Adds a stream of data to the Audio Client's internal buffer, to be sent to Discord. Follows the standard c# Stream.Send() format. +|stub-desc| Broadcasting ------------ -There are multiple approaches to broadcasting audio. Discord.Net will convert your audio packets into Opus format, so the only work you need to do is converting your audio into a format that Discord will accept. The format Discord takes is 16-bit 48000Hz PCM. - -Broadcasting with NAudio ------------------------- - -`NAudio`_ is one of the easiest approaches to sending audio, although it is not multi-platform compatible. The following example will show you how to read an mp3 file, and send it to Discord. -You can `download NAudio from NuGet`_. - -.. code-block:: csharp6 - - using NAudio; - using NAudio.Wave; - using NAudio.CoreAudioApi; - - public void SendAudio(string filePath) - { - var channelCount = _client.GetService().Config.Channels; // Get the number of AudioChannels our AudioService has been configured to use. - var OutFormat = new WaveFormat(48000, 16, channelCount); // Create a new Output Format, using the spec that Discord will accept, and with the number of channels that our client supports. - using (var MP3Reader = new Mp3FileReader(filePath)) // Create a new Disposable MP3FileReader, to read audio from the filePath parameter - using (var resampler = new MediaFoundationResampler(MP3Reader, OutFormat)) // Create a Disposable Resampler, which will convert the read MP3 data to PCM, using our Output Format - { - resampler.ResamplerQuality = 60; // Set the quality of the resampler to 60, the highest quality - int blockSize = outFormat.AverageBytesPerSecond / 50; // Establish the size of our AudioBuffer - byte[] buffer = new byte[blockSize]; - int byteCount; - - while((byteCount = resampler.Read(buffer, 0, blockSize)) > 0) // Read audio into our buffer, and keep a loop open while data is present - { - if (byteCount < blockSize) - { - // Incomplete Frame - for (int i = byteCount; i < blockSize; i++) - buffer[i] = 0; - } - _vClient.Send(buffer, 0, blockSize); // Send the buffer to Discord - } - } - - } - -.. _NAudio: https://naudio.codeplex.com/ -.. _download NAudio from NuGet: https://www.nuget.org/packages/NAudio/ - -Broadcasting with FFmpeg ------------------------- - -`FFmpeg`_ allows for a more advanced approach to sending audio, although it is multiplatform safe. The following example will show you how to stream a file to Discord. - -.. code-block:: csharp6 - - public void SendAudio(string pathOrUrl) - { - var process = Process.Start(new ProcessStartInfo { // FFmpeg requires us to spawn a process and hook into its stdout, so we will create a Process - FileName = "ffmpeg", - Arguments = $"-i {pathOrUrl}" + // Here we provide a list of arguments to feed into FFmpeg. -i means the location of the file/URL it will read from - "-f s16le -ar 48000 -ac 2 pipe:1", // Next, we tell it to output 16-bit 48000Hz PCM, over 2 channels, to stdout. - UseShellExecute = false, - RedirectStandardOutput = true // Capture the stdout of the process - }); - Thread.Sleep(2000); // Sleep for a few seconds to FFmpeg can prebuffer. - - int blockSize = 3840; // The size of bytes to read per frame; 1920 for mono - byte[] buffer = new byte[blockSize]; - int byteCount; - - while (true) // Loop forever, so data will always be read - { - byteCount = process.StandardOutput.BaseStream // Access the underlying MemoryStream from the stdout of FFmpeg - .Read(buffer, 0, blockSize); // Read stdout into the buffer - - if (byteCount == 0) // FFmpeg did not output anything - break; // Break out of the while(true) loop, since there was nothing to read. - - _vClient.Send(buffer, 0, byteCount); // Send our data to Discord - } - _vClient.Wait(); // Wait for the Voice Client to finish sending data, as ffMPEG may have already finished buffering out a song, and it is unsafe to return now. - } - -.. _FFmpeg: https://ffmpeg.org/ - -.. note:: - - The code-block above assumes that your client is configured to stream 2-channel audio. It also may prematurely end a song. FFmpeg can — especially when streaming from a URL — stop to buffer data from a source, and cause your output stream to read empty data. Because the snippet above does not safely track for failed attempts, or buffers, an empty buffer will cause playback to stop. This is also not 'memory-friendly'. - Multi-Server Broadcasting ------------------------- -.. warning:: Multi-Server broadcasting is not supported by Discord, will cause performance issues for you, and is not encouraged. Proceed with caution. - -To prepare for Multi-Server Broadcasting, you must first enable it in your config. - -.. code-block::csharp6 - - _client.UsingAudio(x => - { - x.Mode = AudioMode.Outgoing; - x.EnableMultiserver = true; // Enable Multiserver - }); - -From here on, it is as easy as creating an IAudioClient for each server you want to join. See the sections on broadcasting to proceed. - - Receiving ---------- - -**Receiving is not implemented in the latest version of Discord.Net** \ No newline at end of file +--------- \ No newline at end of file diff --git a/docs/getting_started.rst b/docs/getting_started.rst index f86b36231..f9dfd857d 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -22,12 +22,12 @@ You can get Discord.Net from NuGet: If you have trouble installing from NuGet, try installing dependencies manually. -You can also pull the latest source from `GitHub`_ +You can also pull the latest source from `GitHub`_ .. _Discord.Net: https://www.nuget.org/packages/Discord.Net .. _Discord.Net.Commands: https://www.nuget.org/packages/Discord.Net.Commands .. _Discord.Net.Modules: https://www.nuget.org/packages/Discord.Net.Modules -.. _Discord.Net.Audio: https://www.nuget.org/packages/Discord.Net.Audio +.. _Discord.Net.Modules: https://www.nuget.org/packages/Discord.Net.Audio .. _GitHub: https://github.com/RogueException/Discord.Net/ Async @@ -42,7 +42,7 @@ For more information, go to `MSDN's Await-Async section`_. Example ------- - + .. literalinclude:: samples/getting_started.cs :language: csharp6 :tab-width: 2 diff --git a/docs/global.txt b/docs/global.txt index 25b33510e..e5b572c93 100644 --- a/docs/global.txt +++ b/docs/global.txt @@ -1,4 +1,2 @@ .. |stub| unicode:: U+1F527 -.. |stub-desc| replace:: This page is a placeholder and has not been written yet. It should be coming soon! -.. |outdated| replace:: **This page is currently out-of-date. The information below may be inaccurate.** -.. |incomplete| replace:: **This page is incomplete. While the information below is accurate, it should be noted that it is not thorough.** \ No newline at end of file +.. |stub-desc| replace:: This page is a placeholder and has not been written yet. It should be coming soon! \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index bf5676406..d2ff662af 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -9,13 +9,13 @@ 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 a beta! +This is a beta! - 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. +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`_. @@ -23,8 +23,6 @@ It is highly recommended that you always use the latest version and please repor This Documentation is **currently undergoing a rewrite**. Some pages (marked with a wrench) are not updated, or are not completed yet. -**The documentation is currently being written to reflect ``0.9-rc4``, which can be accessed via the latest git-master.** - .. toctree:: :caption: Documentation :maxdepth: 2 diff --git a/docs/samples/events.cs b/docs/samples/events.cs index 8a53c0bbc..7f68bf6cb 100644 --- a/docs/samples/events.cs +++ b/docs/samples/events.cs @@ -1,20 +1,20 @@ class Program { - private static DiscordClient _client; + private static DiscordBotClient _client; static void Main(string[] args) { - _client = new DiscordClient(); + var client = new DiscordClient(); // Handle Events using Lambdas - _client.MessageReceived += (s, e) => + client.MessageCreated += (s, e) => { if (!e.Message.IsAuthor) - await e.Channel.SendMessage("foo"); + await client.SendMessage(e.Message.ChannelId, "foo"); } // Handle Events using Event Handlers EventHandler handler = new EventHandler(HandleMessageCreated); - client.MessageReceived += handler; + client.MessageCreated += handler; } @@ -22,6 +22,6 @@ class Program static void HandleMessageCreated(object sender, EventArgs e) { if (!e.Message.IsAuthor) - await e.Channel.SendMessage("bar"); + await client.SendMessage(e.Message.ChannelId, "foo"); } -} +} \ No newline at end of file diff --git a/docs/samples/getting_started.cs b/docs/samples/getting_started.cs index d471fbc65..55f7923a4 100644 --- a/docs/samples/getting_started.cs +++ b/docs/samples/getting_started.cs @@ -2,13 +2,10 @@ class Program { static void Main(string[] args) { - var client = new DiscordClient(x => - { - LogLevel = LogSeverity.Info - }); + var client = new DiscordClient(); //Display all log messages in the console - client.Log.Message += (s, e) => Console.WriteLine($"[{e.Severity}] {e.Source}: {e.Message}"); + client.LogMessage += (s, e) => Console.WriteLine($"[{e.Severity}] {e.Source}: {e.Message}"); //Echo back any message received, provided it didn't come from the bot itself client.MessageReceived += async (s, e) => @@ -25,7 +22,7 @@ class Program //If we are not a member of any server, use our invite code (made beforehand in the official Discord Client) if (!client.Servers.Any()) - await (client.GetInvite("aaabbbcccdddeee")).Accept(); + await client.AcceptInvite(client.GetInvite("aaabbbcccdddeee")); }); } } diff --git a/docs/samples/logging.cs b/docs/samples/logging.cs index 4fd3e4959..c68b8aded 100644 --- a/docs/samples/logging.cs +++ b/docs/samples/logging.cs @@ -1,5 +1,6 @@ class Program { + private static DiscordBotClient _client; static void Main(string[] args) { var client = new DiscordClient(x => @@ -7,13 +8,13 @@ class Program LogLevel = LogSeverity.Info }); - client.Log.Message += (s, e) => Console.WriteLine($"[{e.Severity}] {e.Source}: {e.Message}"); + _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()) - await (client.GetInvite("aaabbbcccdddeee")).Accept(); + await client.AcceptInvite("aaabbbcccdddeee"); }); } } diff --git a/docs/samples/permissions.cs b/docs/samples/permissions.cs index 65681d0f9..419026714 100644 --- a/docs/samples/permissions.cs +++ b/docs/samples/permissions.cs @@ -1,8 +1,14 @@ - // Find a User's Channel Permissions -var UserPerms = _channel.GetPermissionsRule(_user); +var userChannelPermissions = user.GetPermissions(channel); + +// Find a User's Server Permissions +var userServerPermissions = user.ServerPermissions(); +var userServerPermissions = server.GetPermissions(user); -// Set a User's Channel Permissions +// Set a User's Channel Permissions (using DualChannelPermissions) -var NewOverwrites = new ChannelPermissionOverrides(sendMessages: PermValue.Deny); -await channel.AddPermissionsRule(_user, NewOverwrites); +var userPerms = user.GetPermissions(channel); +userPerms.ReadMessageHistory = false; +userPerms.AttachFiles = null; +channel.AddPermissionsRule(user, userPerms); +} diff --git a/global.json b/global.json index 4357be0d5..7f3ac9f7e 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { - "projects": [ "src" ], - "sdk": { - "version": "1.0.0-rc1-update1" - } + "projects": [ "src" ], + "sdk": { + "version": "1.0.0-rc2-20221" + } } \ No newline at end of file diff --git a/ref/Discord.Net.xproj b/ref/Discord.Net.xproj deleted file mode 100644 index d3559797d..000000000 --- a/ref/Discord.Net.xproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - 5b2afee6-fff6-4ba2-be12-61b283b72ac0 - Discord - ..\..\artifacts\obj\$(MSBuildProjectName) - ..\..\artifacts\bin\$(MSBuildProjectName)\ - - - 2.0 - - - True - - - \ No newline at end of file diff --git a/ref/DiscordClient.cs b/ref/DiscordClient.cs deleted file mode 100644 index aa777e04f..000000000 --- a/ref/DiscordClient.cs +++ /dev/null @@ -1,75 +0,0 @@ -using Discord.Net.Rest; -using Discord.Net.WebSockets; -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace Discord -{ - /// Provides a connection to the DiscordApp service. - public class DiscordClient : IDisposable - { - public event EventHandler Log = delegate { }; - - public event EventHandler LoggedIn = delegate { }; - public event EventHandler LoggedOut = delegate { }; - public event EventHandler Connected = delegate { }; - public event EventHandler Disconnected = delegate { }; - public event EventHandler VoiceConnected = delegate { }; - public event EventHandler VoiceDisconnected = delegate { }; - - public event EventHandler ChannelCreated = delegate { }; - public event EventHandler ChannelUpdated = delegate { }; - public event EventHandler ChannelDestroyed = 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 RoleCreated = 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 ServerUnavailable = delegate { }; - public event EventHandler UserBanned = delegate { }; - public event EventHandler UserIsTyping = delegate { }; - public event EventHandler UserJoined = delegate { }; - public event EventHandler UserLeft = delegate { }; - public event EventHandler UserUpdated = delegate { }; - public event EventHandler UserUnbanned = delegate { }; - - public MessageQueue MessageQueue { get; } - public IRestClient RestClient { get; } - public GatewaySocket GatewaySocket { get; } - public Profile CurrentUser { get; } - - public DiscordClient() { } - public DiscordClient(DiscordConfig config) { } - - public Task Login(string token) => null; - public Task Logout() => null; - - public Task Connect() => null; - public Task Connect(int connectionId, int totalConnections) => null; - public Task Disconnect() => null; - - public Task> GetPrivateChannels() => null; - public Task GetPrivateChannel(ulong userId) => null; - public Task GetInvite(string inviteIdOrXkcd) => null; - public Task> GetRegions() => null; - public Task GetRegion(string id) => null; - public Task> GetServers() => null; - public Task GetServer(ulong id) => null; - - public Task CreatePrivateChannel(ulong userId) => null; - public Task CreateServer(string name, Region region, ImageType iconType = ImageType.None, Stream icon = null) => null; - - public void Dispose() { } - } -} \ No newline at end of file diff --git a/ref/DiscordConfig.cs b/ref/DiscordConfig.cs deleted file mode 100644 index e6b1a5568..000000000 --- a/ref/DiscordConfig.cs +++ /dev/null @@ -1,65 +0,0 @@ -using Discord.Net.Rest; -using Discord.Net.WebSockets; -using System.Reflection; - -namespace Discord -{ - public class DiscordConfig - { - public const int MaxMessageSize = 2000; - public const int MaxMessagesPerBatch = 100; - - public const string LibName = "Discord.Net"; - public static string LibVersion => typeof(DiscordConfig).GetTypeInfo().Assembly?.GetName().Version.ToString(3) ?? "Unknown"; - public const string LibUrl = "https://github.com/RogueException/Discord.Net"; - - public const string ClientAPIUrl = "https://discordapp.com/api/"; - public const string CDNUrl = "https://cdn.discordapp.com/"; - public const string InviteUrl = "https://discord.gg/"; - - /// Gets or sets name of your application, used in the user agent. - public string AppName { get; set; } = null; - /// Gets or sets url to your application, used in the user agent. - public string AppUrl { get; set; } = null; - /// Gets or sets the version of your application, used in the user agent. - public string AppVersion { get; set; } = null; - - /// Gets or sets the minimum log level severity that will be sent to the LogMessage event. - public LogSeverity LogLevel { get; set; } = LogSeverity.Info; - - /// Gets or sets the time (in milliseconds) to wait for the websocket to connect and initialize. - public int ConnectionTimeout { get; set; } = 30000; - /// Gets or sets the time (in milliseconds) to wait after an unexpected disconnect before reconnecting. - public int ReconnectDelay { get; set; } = 1000; - /// Gets or sets the time (in milliseconds) to wait after an reconnect fails before retrying. - public int FailedReconnectDelay { get; set; } = 15000; - - //Performance - - /// Gets or sets the number of messages per channel that should be kept in cache. Setting this to zero disables the message cache entirely. - public int MessageCacheSize { get; set; } = 100; - /// - /// Gets or sets whether the permissions cache should be used. - /// This makes operations such as User.GetPermissions(Channel), User.ServerPermissions, Channel.GetUser, and Channel.Members much faster while increasing memory usage. - /// - public bool UsePermissionsCache { get; set; } = true; - /// Gets or sets whether the a copy of a model is generated on an update event to allow you to check which properties changed. - public bool EnablePreUpdateEvents { get; set; } = true; - /// - /// Gets or sets the max number of users a server may have for offline users to be included in the READY packet. Max is 250. - /// Decreasing this may reduce CPU usage while increasing login time and network usage. - /// - public int LargeThreshold { get; set; } = 250; - - //Engines - - /// Gets or sets the REST engine to use.. Defaults to DefaultRestClientProvider, which uses .Net's HttpClient class. - public IRestClientProvider RestClientProvider { get; set; } = null; - /// - /// Gets or sets the WebSocket engine to use. Defaults to DefaultWebSocketProvider, which uses .Net's WebSocketClient class. - /// WebSockets are only used if DiscordClient.Connect() is called. - /// - public IWebSocketProvider WebSocketProvider { get; set; } = null; - } -} - diff --git a/ref/Entities/Channels/IPublicChannel.cs b/ref/Entities/Channels/IPublicChannel.cs deleted file mode 100644 index bd005a288..000000000 --- a/ref/Entities/Channels/IPublicChannel.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Discord -{ - public interface IPublicChannel : IChannel - { - /// Gets the server this channel is a member of. - Server Server { get; } - /// Gets a collection of permission overwrites for this channel. - IEnumerable PermissionOverwrites { get; } - /// Gets the position of this public channel relative to others of the same type. - int Position { get; } - - /// Gets a user in this channel with the given id. - new Task GetUser(ulong id); - /// Gets a collection of all users in this channel. - new Task> GetUsers(); - - /// Gets the permission overwrite for a specific user, or null if one does not exist. - OverwritePermissions? GetPermissionOverwrite(ServerUser user); - /// Gets the permission overwrite for a specific role, or null if one does not exist. - OverwritePermissions? GetPermissionOverwrite(Role role); - /// Downloads a collection of all invites to this server. - Task> GetInvites(); - - /// Adds or updates the permission overwrite for the given user. - Task UpdatePermissionOverwrite(ServerUser user, OverwritePermissions permissions); - /// Adds or updates the permission overwrite for the given role. - Task UpdatePermissionOverwrite(Role role, OverwritePermissions permissions); - /// Removes the permission overwrite for the given user, if one exists. - Task RemovePermissionOverwrite(ServerUser user); - /// Removes the permission overwrite for the given role, if one exists. - Task RemovePermissionOverwrite(Role role); - - /// Creates a new invite to this channel. - /// Time (in seconds) until the invite expires. Set to null to never expire. - /// The max amount of times this invite may be used. Set to null to have unlimited uses. - /// If true, a user accepting this invite will be kicked from the server after closing their client. - /// If true, creates a human-readable link. Not supported if maxAge is set to null. - Task CreateInvite(int? maxAge = 1800, int? maxUses = null, bool tempMembership = false, bool withXkcd = false); - } -} diff --git a/ref/Entities/Channels/PrivateChannel.cs b/ref/Entities/Channels/PrivateChannel.cs deleted file mode 100644 index ee72c0828..000000000 --- a/ref/Entities/Channels/PrivateChannel.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; - -namespace Discord -{ - public class PrivateChannel : ITextChannel, IChannel - { - /// - public DiscordClient Discord { get; } - /// - public EntityState State { get; } - /// - public ulong Id { get; } - /// - public PrivateUser Recipient { get; } - /// - public PrivateUser CurrentUser { get; } - - /// - ChannelType IChannel.Type => ChannelType.Private | ChannelType.Text; - /// - public string Name { get; } - - /// - public Task GetUser(ulong id) => null; - /// - Task IChannel.GetUser(ulong id) => null; - /// - public Task> GetUsers() => null; - /// - Task> IChannel.GetUsers() => null; - /// - public Task GetMessage(ulong id) => null; - /// - public Task> GetMessages(int limit = 100) => null; - /// - public Task> GetMessages(int limit = 100, ulong? relativeMessageId = null, Relative relativeDir = Relative.Before) => null; - - /// - public Task SendMessage(string text, bool isTTS = false) => null; - /// - public Task SendFile(string filePath, string text = null, bool isTTS = false) => null; - /// - public Task SendFile(Stream stream, string filename, string text = null, bool isTTS = false) => null; - - /// - public Task SendIsTyping() => null; - - /// - public Task Update() => null; - /// - public Task Delete() => null; - } -} diff --git a/ref/Entities/Channels/TextChannel.cs b/ref/Entities/Channels/TextChannel.cs deleted file mode 100644 index 0b1b81c77..000000000 --- a/ref/Entities/Channels/TextChannel.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; - -namespace Discord -{ - public class TextChannel : ITextChannel, IMentionable, IModifiable - { - public sealed class Properties - { - public string Name { get; } - public string Topic { get; } - public int Position { get; } - } - - /// - public EntityState State { get; } - /// - public ulong Id { get; } - /// - public Server Server { get; } - - /// - public DiscordClient Discord { get; } - /// - public ChannelType Type => ChannelType.Public | ChannelType.Text; - - /// - public string Name { get; } - /// - public string Topic { get; } - /// - public int Position { get; } - - /// - public string Mention { get; } - /// - public IEnumerable PermissionOverwrites { get; } - - /// - public OverwritePermissions? GetPermissionOverwrite(ServerUser user) => null; - /// - public OverwritePermissions? GetPermissionOverwrite(Role role) => null; - /// - public Task GetUser(ulong id) => null; - /// - Task IChannel.GetUser(ulong id) => null; - /// - public Task> GetUsers() => null; - /// - Task> IChannel.GetUsers() => null; - /// - public Task GetMessage(ulong id) => null; - /// - public Task> GetMessages(int limit = 100) => null; - /// - public Task> GetMessages(int limit = 100, ulong? relativeMessageId = null, Relative relativeDir = Relative.Before) => null; - /// - public Task> GetInvites() => null; - - /// - public Task UpdatePermissionOverwrite(ServerUser user, OverwritePermissions permissions) => null; - /// - public Task UpdatePermissionOverwrite(Role role, OverwritePermissions permissions) => null; - /// - public Task RemovePermissionOverwrite(ServerUser user) => null; - /// - public Task RemovePermissionOverwrite(Role role) => null; - - /// - public Task SendMessage(string text, bool isTTS = false) => null; - /// - public Task SendFile(string filePath, string text = null, bool isTTS = false) => null; - /// - public Task SendFile(Stream stream, string filename, string text = null, bool isTTS = false) => null; - - /// - public Task SendIsTyping() => null; - - /// - public Task CreateInvite(int? maxAge = 1800, int? maxUses = null, bool tempMembership = false, bool withXkcd = false) => null; - - /// - public Task Update() => null; - /// - public Task Modify(Action func) => null; - /// - public Task Delete() => null; - } -} diff --git a/ref/Entities/Channels/VoiceChannel.cs b/ref/Entities/Channels/VoiceChannel.cs deleted file mode 100644 index 6552fadd7..000000000 --- a/ref/Entities/Channels/VoiceChannel.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Discord -{ - public class VoiceChannel : IPublicChannel, IModifiable - { - public sealed class Properties - { - public string Name { get; } - public int Bitrate { get; set; } - public int Position { get; } - } - - /// - public ulong Id { get; } - /// - public EntityState State { get; } - /// - public Server Server { get; } - - /// - public DiscordClient Discord { get; } - /// - ChannelType IChannel.Type => ChannelType.Public | ChannelType.Voice; - - /// - public string Name { get; } - /// - public int Position { get; } - /// - public int Bitrate { get; } - - /// - public string Mention { get; } - /// - public IEnumerable PermissionOverwrites { get; } - - /// - public OverwritePermissions? GetPermissionOverwrite(ServerUser user) => null; - /// - public OverwritePermissions? GetPermissionOverwrite(Role role) => null; - /// - public Task GetUser(ulong id) => null; - /// - Task IChannel.GetUser(ulong id) => null; - /// - public Task> GetUsers() => null; - /// - Task> IChannel.GetUsers() => null; - /// - public Task> GetInvites() => null; - - /// - public Task UpdatePermissionOverwrite(ServerUser user, OverwritePermissions permissions) => null; - /// - public Task UpdatePermissionOverwrite(Role role, OverwritePermissions permissions) => null; - /// - public Task RemovePermissionOverwrite(ServerUser user) => null; - /// - public Task RemovePermissionOverwrite(Role role) => null; - - /// - public Task CreateInvite(int? maxAge = 1800, int? maxUses = null, bool tempMembership = false, bool withXkcd = false) => null; - - /// - public Task Update() => null; - /// - public Task Modify(Action func) => null; - /// - public Task Delete() => null; - } -} diff --git a/ref/Entities/Color.cs b/ref/Entities/Color.cs deleted file mode 100644 index b3c78debf..000000000 --- a/ref/Entities/Color.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Discord -{ - public class Color - { - public static readonly Color Default = new Color(0); - - public uint RawValue { get; } - - public Color(uint rawValue) { } - public Color(byte r, byte g, byte b) { } - public Color(float r, float g, float b) { } - - public byte R { get; } - public byte G { get; } - public byte B { get; } - } -} diff --git a/ref/Entities/IModifiable.cs b/ref/Entities/IModifiable.cs deleted file mode 100644 index f264c96f2..000000000 --- a/ref/Entities/IModifiable.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace Discord -{ - public interface IModifiable - { - /// Modifies one or more of the properties of this object. - Task Modify(Action func); - } -} diff --git a/ref/Entities/Invite/BasicInvite.cs b/ref/Entities/Invite/BasicInvite.cs deleted file mode 100644 index 37cd1704d..000000000 --- a/ref/Entities/Invite/BasicInvite.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Threading.Tasks; - -namespace Discord -{ - public class BasicInvite : IEntity - { - public class TargetInfo - { - public ulong Id { get; } - public string Name { get; } - } - public class InviterInfo - { - public ulong Id { get; } - public string Name { get; } - public ushort Discriminator { get; } - public string AvatarId { get; } - public string AvatarUrl { get; } - } - - string IEntity.Id => Code; - public DiscordClient Discord { get; } - public EntityState State { get; } - - public string Code { get; } - public string XkcdCode { get; } - - public TargetInfo Server { get; } - public TargetInfo Channel { get; } - - public string Url { get; } - - public Task Accept() => null; - - public virtual Task Update() => null; - } -} diff --git a/ref/Entities/Invite/Invite.cs b/ref/Entities/Invite/Invite.cs deleted file mode 100644 index 11fead2af..000000000 --- a/ref/Entities/Invite/Invite.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace Discord -{ - public class Invite : BasicInvite - { - public int? MaxAge { get; } - public int Uses { get; } - public int? MaxUses { get; } - public bool IsRevoked { get; } - public bool IsTemporary { get; } - public DateTime CreatedAt { get; } - - public override Task Update() => null; - public Task Delete() => null; - } -} diff --git a/ref/Entities/Message.cs b/ref/Entities/Message.cs deleted file mode 100644 index 78c4e41bd..000000000 --- a/ref/Entities/Message.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Discord -{ - public class Message : IEntity - { - public class Attachment : File - { - public string Id { get; } - public int Size { get; } - public string Filename { get; } - } - - public class Embed - { - public string Url { get; } - public string Type { get; } - public string Title { get; } - public string Description { get; } - public EmbedLink Author { get; } - public EmbedLink Provider { get; } - public File Thumbnail { get; } - public File Video { get; } - } - - public class EmbedLink - { - public string Url { get; } - public string Name { get; } - } - - public class File - { - public string Url { get; } - public string ProxyUrl { get; } - public int? Width { get; } - public int? Height { get; } - } - - public ulong Id { get; } - public DiscordClient Discord { get; } - public EntityState State { get; } - - public ITextChannel Channel { get; } - public IUser User { get; } - public bool IsTTS { get; } - public string RawText { get; } - public string Text { get; } - public DateTime Timestamp { get; } - public DateTime? EditedTimestamp { get; } - public Attachment[] Attachments { get; } - public Embed[] Embeds { get; } - - public IReadOnlyList MentionedUsers { get; } - public IReadOnlyList MentionedChannels { get; } - public IReadOnlyList MentionedRoles { get; } - - public Server Server => null; - public bool IsAuthor => false; - - public bool IsMentioningMe(bool includeRoles = false) => false; - - public Task Update() => null; - public Task Delete() => null; - } -} diff --git a/ref/Entities/Permissions/ChannelPermissions.cs b/ref/Entities/Permissions/ChannelPermissions.cs deleted file mode 100644 index d01f0430e..000000000 --- a/ref/Entities/Permissions/ChannelPermissions.cs +++ /dev/null @@ -1,53 +0,0 @@ -namespace Discord -{ - public struct ChannelPermissions - { - public static ChannelPermissions None { get; } - public static ChannelPermissions TextOnly { get; } - public static ChannelPermissions PrivateOnly { get; } - public static ChannelPermissions VoiceOnly { get; } - public static ChannelPermissions All(ChannelType channelType) => default(ChannelPermissions); - - public uint RawValue { get; } - - public bool CreateInstantInvite { get; } - public bool ManagePermission { get; } - public bool ManageChannel { get; } - - public bool ReadMessages { get; } - public bool SendMessages { get; } - public bool SendTTSMessages { get; } - public bool ManageMessages { get; } - public bool EmbedLinks { get; } - public bool AttachFiles { get; } - public bool ReadMessageHistory { get; } - public bool MentionEveryone { get; } - - public bool Connect { get; } - public bool Speak { get; } - public bool MuteMembers { get; } - public bool DeafenMembers { get; } - public bool MoveMembers { get; } - public bool UseVoiceActivation { get; } - - public ChannelPermissions(bool? createInstantInvite = null, bool? managePermissions = null, - bool? manageChannel = null, bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, - bool? manageMessages = null, bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, - bool? mentionEveryone = null, bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, - bool? moveMembers = null, bool? useVoiceActivation = null) - : this() - { - } - public ChannelPermissions(uint rawValue) - : this() - { - } - - public ChannelPermissions Modify(bool? createInstantInvite = null, bool? managePermissions = null, - bool? manageChannel = null, bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, - bool? manageMessages = null, bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, - bool? mentionEveryone = null, bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, - bool? moveMembers = null, bool? useVoiceActivation = null) - => default(ChannelPermissions); - } -} diff --git a/ref/Entities/Permissions/OverwritePermissions.cs b/ref/Entities/Permissions/OverwritePermissions.cs deleted file mode 100644 index 1cda173ec..000000000 --- a/ref/Entities/Permissions/OverwritePermissions.cs +++ /dev/null @@ -1,50 +0,0 @@ -namespace Discord -{ - public struct OverwritePermissions - { - public static OverwritePermissions InheritAll { get; } - - public uint AllowValue { get; } - public uint DenyValue { get; } - - public PermValue CreateInstantInvite { get; } - public PermValue ManagePermissions { get; } - public PermValue ManageChannel { get; } - public PermValue ReadMessages { get; } - public PermValue SendMessages { get; } - public PermValue SendTTSMessages { get; } - public PermValue ManageMessages { get; } - public PermValue EmbedLinks { get; } - public PermValue AttachFiles { get; } - public PermValue ReadMessageHistory { get; } - public PermValue MentionEveryone { get; } - - public PermValue Connect { get; } - public PermValue Speak { get; } - public PermValue MuteMembers { get; } - public PermValue DeafenMembers { get; } - public PermValue MoveMembers { get; } - public PermValue UseVoiceActivation { get; } - - public OverwritePermissions(PermValue? createInstantInvite = null, PermValue? managePermissions = null, - PermValue? manageChannel = null, PermValue? readMessages = null, PermValue? sendMessages = null, PermValue? sendTTSMessages = null, - PermValue? manageMessages = null, PermValue? embedLinks = null, PermValue? attachFiles = null, PermValue? readMessageHistory = null, - PermValue? mentionEveryone = null, PermValue? connect = null, PermValue? speak = null, PermValue? muteMembers = null, PermValue? deafenMembers = null, - PermValue? moveMembers = null, PermValue? useVoiceActivation = null) - : this() - { - } - - public OverwritePermissions(uint allow = 0, uint deny = 0) - : this() - { - } - - public OverwritePermissions Modify(PermValue? createInstantInvite = null, PermValue? managePermissions = null, - PermValue? manageChannel = null, PermValue? readMessages = null, PermValue? sendMessages = null, PermValue? sendTTSMessages = null, - PermValue? manageMessages = null, PermValue? embedLinks = null, PermValue? attachFiles = null, PermValue? readMessageHistory = null, - PermValue? mentionEveryone = null, PermValue? connect = null, PermValue? speak = null, PermValue? muteMembers = null, PermValue? deafenMembers = null, - PermValue? moveMembers = null, PermValue? useVoiceActivation = null) - => default(OverwritePermissions); - } -} diff --git a/ref/Entities/Permissions/PermissionOverwriteEntry.cs b/ref/Entities/Permissions/PermissionOverwriteEntry.cs deleted file mode 100644 index bbc11fba8..000000000 --- a/ref/Entities/Permissions/PermissionOverwriteEntry.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Discord -{ - public struct PermissionOverwriteEntry - { - public PermissionTarget TargetType { get; } - public ulong TargetId { get; } - public OverwritePermissions Permissions { get; } - } -} diff --git a/ref/Entities/Permissions/ServerPermissions.cs b/ref/Entities/Permissions/ServerPermissions.cs deleted file mode 100644 index fe85c07dd..000000000 --- a/ref/Entities/Permissions/ServerPermissions.cs +++ /dev/null @@ -1,55 +0,0 @@ -namespace Discord -{ - public struct ServerPermissions - { - public static ServerPermissions None { get; } - public static ServerPermissions All { get; } - - public uint RawValue { get; } - - public bool CreateInstantInvite { get; } - public bool BanMembers { get; } - public bool KickMembers { get; } - public bool ManageRoles { get; } - public bool ManageChannels { get; } - public bool ManageServer { get; } - - public bool ReadMessages { get; } - public bool SendMessages { get; } - public bool SendTTSMessages { get; } - public bool ManageMessages { get; } - public bool EmbedLinks { get; } - public bool AttachFiles { get; } - public bool ReadMessageHistory { get; } - public bool MentionEveryone { get; } - - public bool Connect { get; } - public bool Speak { get; } - public bool MuteMembers { get; } - public bool DeafenMembers { get; } - public bool MoveMembers { get; } - public bool UseVoiceActivation { get; } - - public ServerPermissions(bool? createInstantInvite = null, bool? manageRoles = null, - bool? kickMembers = null, bool? banMembers = null, bool? manageChannel = null, bool? manageServer = null, - bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, bool? manageMessages = null, - bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, bool? mentionEveryone = null, - bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, - bool? moveMembers = null, bool? useVoiceActivation = null) - : this() - { - } - public ServerPermissions(uint rawValue) - : this() - { - } - - public ServerPermissions Modify(bool? createInstantInvite = null, bool? manageRoles = null, - bool? kickMembers = null, bool? banMembers = null, bool? manageChannel = null, bool? manageServer = null, - bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, bool? manageMessages = null, - bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, bool? mentionEveryone = null, - bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, - bool? moveMembers = null, bool? useVoiceActivation = null) - => default(ServerPermissions); - } -} diff --git a/ref/Entities/Profile.cs b/ref/Entities/Profile.cs deleted file mode 100644 index aa61e51b2..000000000 --- a/ref/Entities/Profile.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Threading.Tasks; - -namespace Discord -{ - public class Profile : IEntity - { - public ulong Id { get; } - public DiscordClient Discord { get; } - public EntityState State { get; } - - public string AvatarId { get; } - public string AvatarUrl { get; } - public ushort Discriminator { get; } - public string CurrentGame { get; } - public UserStatus Status { get; } - public string Mention { get; } - public string Email { get; } - public bool? IsVerified { get; } - - public string Name { get; set; } - - public Task Update() => null; - public Task Delete() => null; - } -} diff --git a/ref/Entities/Region.cs b/ref/Entities/Region.cs deleted file mode 100644 index fbb801eaa..000000000 --- a/ref/Entities/Region.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Discord -{ - public class Region - { - public string Id { get; } - public string Name { get; } - public string Hostname { get; } - public int Port { get; } - public bool Vip { get; } - } -} diff --git a/ref/Entities/Role.cs b/ref/Entities/Role.cs deleted file mode 100644 index f5155db2e..000000000 --- a/ref/Entities/Role.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Discord -{ - public class Role : IEntity, IMentionable - { - public ulong Id { get; } - public DiscordClient Discord { get; } - public EntityState State { get; } - - public Server Server { get; } - - public string Name { get; } - public bool IsHoisted { get; } - public int Position { get; } - public bool IsManaged { get; } - public ServerPermissions Permissions { get; } - public Color Color { get; } - - public bool IsEveryone { get; } - public IEnumerable Members { get; } - - public string Mention { get; } - - public Task Update() => null; - public Task Delete() => null; - } -} diff --git a/ref/Entities/Server.cs b/ref/Entities/Server.cs deleted file mode 100644 index a9078cb4b..000000000 --- a/ref/Entities/Server.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Discord -{ - public class Server : IEntity - { - public class Emoji - { - public string Id { get; } - public string Name { get; } - public bool IsManaged { get; } - public bool RequireColons { get; } - public IEnumerable Roles { get; } - } - - public ulong Id { get; } - public DiscordClient Discord { get; } - public EntityState State { get; } - - public ServerUser CurrentUser { get; } - public string IconId { get; } - public string SplashId { get; } - public string IconUrl { get; } - public string SplashUrl { get; } - public int ChannelCount { get; } - public int UserCount { get; } - public int RoleCount { get; } - public TextChannel DefaultChannel { get; } - public Role EveryoneRole { get; } - public IEnumerable Features { get; } - public IEnumerable CustomEmojis { get; } - public IEnumerable Channels { get; } - public IEnumerable TextChannels { get; } - public IEnumerable VoiceChannels { get; } - public IEnumerable Users { get; } - public IEnumerable Roles { get; } - - public string Name { get; set; } - public Region Region { get; set; } - public int AFKTimeout { get; set; } - public DateTime JoinedAt { get; set; } - public ServerUser Owner { get; set; } - public VoiceChannel AFKChannel { get; set; } - - public Task GetChannel(ulong id) => null; - public Task GetChannel(string mention) => null; - public Task GetRole(ulong id) => null; - public Task GetUser(ulong id) => null; - public Task GetUser(string name, ushort discriminator) => null; - public Task GetUser(string mention) => null; - public Task> GetBans() => null; - public Task> GetInvites() => null; - - public Task CreateTextChannel(string name) => null; - public Task CreateVoiceChannel(string name) => null; - public Task CreateInvite(int? maxAge = 1800, int? maxUses = null, bool tempMembership = false, bool withXkcd = false) => null; - public Task CreateRole(string name, ServerPermissions? permissions = null, Color color = null, bool isHoisted = false) => null; - - public Task PruneUsers(int days = 30, bool simulate = false) => null; - - public Task Update() => null; - public Task Leave() => null; - public Task Delete() => null; - } -} diff --git a/ref/Entities/Users/IUser.cs b/ref/Entities/Users/IUser.cs deleted file mode 100644 index 02dd2d85b..000000000 --- a/ref/Entities/Users/IUser.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Threading.Tasks; - -namespace Discord -{ - public interface IUser : IEntity, IMentionable - { - bool IsPrivate { get; } - - string Name { get; } - ushort Discriminator { get; } - bool IsBot { get; } - string AvatarId { get; } - string AvatarUrl { get; } - string CurrentGame { get; } - UserStatus Status { get; } - - Task GetPrivateChannel(); - } -} diff --git a/ref/Entities/Users/PrivateUser.cs b/ref/Entities/Users/PrivateUser.cs deleted file mode 100644 index a6cc9d6e7..000000000 --- a/ref/Entities/Users/PrivateUser.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Threading.Tasks; - -namespace Discord -{ - //TODO: Should this be linked directly to the Profile when it represents us, instead of maintaining a cache of values? - public class PrivateUser : IUser - { - /// - public EntityState State { get; internal set; } - /// - public ulong Id { get; } - /// Returns the private channel for this user. - public PrivateChannel Channel { get; } - - /// - bool IUser.IsPrivate => true; - - /// - public string Name { get; } - /// - public ushort Discriminator { get; } - /// - public bool IsBot { get; } - /// - public string AvatarId { get; } - /// - public string CurrentGame { get; } - /// - public UserStatus Status { get; } - - /// - public DiscordClient Discord => Channel.Discord; - /// - public string AvatarUrl { get; } - /// - public string Mention { get; } - - /// - Task IUser.GetPrivateChannel() => Task.FromResult(Channel); - - public Task Update() => null; - } -} diff --git a/ref/Entities/Users/ServerUser.cs b/ref/Entities/Users/ServerUser.cs deleted file mode 100644 index 4ff86f67a..000000000 --- a/ref/Entities/Users/ServerUser.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Discord -{ - public class ServerUser : IUser - { - /// - public EntityState State { get; } - /// - public ulong Id { get; } - /// Returns the private channel for this user. - public Server Server { get; } - - /// - bool IUser.IsPrivate => false; - - /// - public string Name { get; } - /// - public ushort Discriminator { get; } - /// - public bool IsBot { get; } - /// - public string AvatarId { get; } - /// - public string CurrentGame { get; } - /// - public UserStatus Status { get; } - /// - public DateTime JoinedAt { get; } - /// - public IReadOnlyList Roles { get; } - - /// Returns true if this user has marked themselves as muted. - public bool IsSelfMuted { get; } - /// Returns true if this user has marked themselves as deafened. - public bool IsSelfDeafened { get; } - /// Returns true if the server is blocking audio from this user. - public bool IsServerMuted { get; } - /// Returns true if the server is blocking audio to this user. - public bool IsServerDeafened { get; } - /// Returns true if the server is temporarily blocking audio to/from this user. - public bool IsServerSuppressed { get; } - /// Gets this user's current voice channel. - public VoiceChannel VoiceChannel { get; } - - /// - public DiscordClient Discord { get; } - /// - public string AvatarUrl { get; } - /// - public string Mention { get; } - - public ServerPermissions ServerPermissions { get; } - - public ChannelPermissions GetPermissions(IPublicChannel channel) => default(ChannelPermissions); - /// - public Task GetPrivateChannel() => null; - public Task> GetChannels() => null; - - public bool HasRole(Role role) => false; - - public Task AddRoles(params Role[] roles) => null; - public Task RemoveRoles(params Role[] roles) => null; - - public Task Update() => null; - public Task Kick() => null; - public Task Ban(int pruneDays = 0) => null; - public Task Unban() => null; - } -} \ No newline at end of file diff --git a/ref/Enums/ChannelType.cs b/ref/Enums/ChannelType.cs deleted file mode 100644 index 5ebbf3aa6..000000000 --- a/ref/Enums/ChannelType.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace Discord -{ - [Flags] - public enum ChannelType : byte - { - Public = 0x01, - Private = 0x02, - Text = 0x10, - Voice = 0x20 - } -} diff --git a/ref/Enums/ConnectionState.cs b/ref/Enums/ConnectionState.cs deleted file mode 100644 index dfd4ac9eb..000000000 --- a/ref/Enums/ConnectionState.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Discord -{ - public enum ConnectionState - { - Disconnected, - Connecting, - Connected, - Disconnecting - } -} diff --git a/ref/Enums/EntityState.cs b/ref/Enums/EntityState.cs deleted file mode 100644 index 6ae71e4a3..000000000 --- a/ref/Enums/EntityState.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Discord -{ - public enum EntityState : byte - { - /// Object is not attached to a cache manager nor receiving live updates. - Detached = 0, - /// Object is attached to a cache manager and receiving live updates. - Attached, - /// Object was deleted. - Deleted, - /// Object is currently waiting to be created. - Queued, - /// Object's creation was aborted. - Aborted, - /// Object's creation failed. - Failed - } -} diff --git a/ref/Enums/ImageType.cs b/ref/Enums/ImageType.cs deleted file mode 100644 index 738c67a3d..000000000 --- a/ref/Enums/ImageType.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Discord -{ - public enum ImageType - { - None, - Jpeg, - Png - } -} diff --git a/ref/Enums/LogSeverity.cs b/ref/Enums/LogSeverity.cs deleted file mode 100644 index 785b0ef46..000000000 --- a/ref/Enums/LogSeverity.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Discord -{ - public enum LogSeverity - { - Critical = 0, - Error = 1, - Warning = 2, - Info = 3, - Verbose = 4, - Debug = 5 - } -} diff --git a/ref/Enums/PermValue.cs b/ref/Enums/PermValue.cs deleted file mode 100644 index fe048b016..000000000 --- a/ref/Enums/PermValue.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Discord -{ - public enum PermValue - { - Allow, - Deny, - Inherit - } -} diff --git a/ref/Enums/PermissionTarget.cs b/ref/Enums/PermissionTarget.cs deleted file mode 100644 index 96595fb69..000000000 --- a/ref/Enums/PermissionTarget.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Discord -{ - public enum PermissionTarget - { - Role, - User - } -} diff --git a/ref/Enums/Relative.cs b/ref/Enums/Relative.cs deleted file mode 100644 index aade047d1..000000000 --- a/ref/Enums/Relative.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Discord -{ - public enum Relative - { - Before, - After - } -} diff --git a/ref/Enums/UserStatus.cs b/ref/Enums/UserStatus.cs deleted file mode 100644 index f2fdfda7c..000000000 --- a/ref/Enums/UserStatus.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Discord -{ - public enum UserStatus - { - Online, - Idle, - Offline - } -} diff --git a/ref/Events/ChannelEventArgs.cs b/ref/Events/ChannelEventArgs.cs deleted file mode 100644 index 583075e08..000000000 --- a/ref/Events/ChannelEventArgs.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace Discord -{ - public class ChannelEventArgs : EventArgs - { - public IChannel Channel => null; - } -} diff --git a/ref/Events/ChannelUpdatedEventArgs.cs b/ref/Events/ChannelUpdatedEventArgs.cs deleted file mode 100644 index bcd809521..000000000 --- a/ref/Events/ChannelUpdatedEventArgs.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace Discord -{ - public class ChannelUpdatedEventArgs : EventArgs - { - public IChannel Before => null; - public IChannel After => null; - } -} diff --git a/ref/Events/DisconnectedEventArgs.cs b/ref/Events/DisconnectedEventArgs.cs deleted file mode 100644 index 616f3f09d..000000000 --- a/ref/Events/DisconnectedEventArgs.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace Discord -{ - public class DisconnectedEventArgs : EventArgs - { - public bool WasUnexpected => false; - public Exception Exception => null; - } -} diff --git a/ref/Events/LogMessageEventArgs.cs b/ref/Events/LogMessageEventArgs.cs deleted file mode 100644 index 7dec182d1..000000000 --- a/ref/Events/LogMessageEventArgs.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace Discord -{ - public class LogMessageEventArgs : EventArgs - { - public LogSeverity Severity => default(LogSeverity); - public string Source => null; - public string Message => null; - public Exception Exception => null; - } -} diff --git a/ref/Events/MessageEventArgs.cs b/ref/Events/MessageEventArgs.cs deleted file mode 100644 index f75c7f1a8..000000000 --- a/ref/Events/MessageEventArgs.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace Discord -{ - public class MessageEventArgs : EventArgs - { - public Message Message => null; - public IUser User => null; - public ITextChannel Channel => null; - } -} diff --git a/ref/Events/MessageUpdatedEventArgs.cs b/ref/Events/MessageUpdatedEventArgs.cs deleted file mode 100644 index d323bf809..000000000 --- a/ref/Events/MessageUpdatedEventArgs.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace Discord -{ - public class MessageUpdatedEventArgs : EventArgs - { - public Message Before => null; - public Message After => null; - public IUser User => null; - public ITextChannel Channel => null; - } -} diff --git a/ref/Events/ProfileUpdatedEventArgs.cs b/ref/Events/ProfileUpdatedEventArgs.cs deleted file mode 100644 index dba55af3b..000000000 --- a/ref/Events/ProfileUpdatedEventArgs.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace Discord -{ - public class ProfileUpdatedEventArgs : EventArgs - { - public Profile Before => null; - public Profile After => null; - } -} diff --git a/ref/Events/RoleEventArgs.cs b/ref/Events/RoleEventArgs.cs deleted file mode 100644 index db1d09cbc..000000000 --- a/ref/Events/RoleEventArgs.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace Discord -{ - public class RoleEventArgs : EventArgs - { - public Role Role => null; - public Server Server => null; - } -} diff --git a/ref/Events/RoleUpdatedEventArgs.cs b/ref/Events/RoleUpdatedEventArgs.cs deleted file mode 100644 index 1fa0f2a81..000000000 --- a/ref/Events/RoleUpdatedEventArgs.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace Discord -{ - public class RoleUpdatedEventArgs : EventArgs - { - public Role Before => null; - public Role After => null; - public Server Server => null; - } -} diff --git a/ref/Events/ServerEventArgs.cs b/ref/Events/ServerEventArgs.cs deleted file mode 100644 index b06993de9..000000000 --- a/ref/Events/ServerEventArgs.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace Discord -{ - public class ServerEventArgs : EventArgs - { - public Server Server => null; - } -} diff --git a/ref/Events/ServerUpdatedEventArgs.cs b/ref/Events/ServerUpdatedEventArgs.cs deleted file mode 100644 index 1e05f1721..000000000 --- a/ref/Events/ServerUpdatedEventArgs.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace Discord -{ - public class ServerUpdatedEventArgs : EventArgs - { - public Server Before => null; - public Server After => null; - } -} diff --git a/ref/Events/TypingEventArgs.cs b/ref/Events/TypingEventArgs.cs deleted file mode 100644 index f45313687..000000000 --- a/ref/Events/TypingEventArgs.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Discord -{ - public class TypingEventArgs - { - public ITextChannel Channel { get; } - public IUser User { get; } - - public TypingEventArgs(ITextChannel channel, IUser user) - { - Channel = channel; - User = user; - } - } -} diff --git a/ref/Events/UserEventArgs.cs b/ref/Events/UserEventArgs.cs deleted file mode 100644 index f1cce29fc..000000000 --- a/ref/Events/UserEventArgs.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System; -namespace Discord -{ - public class UserEventArgs : EventArgs - { - public IUser User => null; - } -} diff --git a/ref/Events/UserUpdatedEventArgs.cs b/ref/Events/UserUpdatedEventArgs.cs deleted file mode 100644 index c45c60701..000000000 --- a/ref/Events/UserUpdatedEventArgs.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; -namespace Discord -{ - public class UserUpdatedEventArgs : EventArgs - { - public IUser Before => null; - public IUser After => null; - } -} diff --git a/ref/Format.cs b/ref/Format.cs deleted file mode 100644 index e30931ae9..000000000 --- a/ref/Format.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Discord -{ - public static class Format - { - public static string Escape(string text) => null; - - public static string Bold(string text, bool escape = true) => null; - public static string Italics(string text, bool escape = true) => null; - public static string Underline(string text, bool escape = true) => null; - public static string Strikeout(string text, bool escape = true) => null; - - public static string Code(string text, string language = null) => null; - } -} diff --git a/ref/ILogger.cs b/ref/ILogger.cs deleted file mode 100644 index a3123edc9..000000000 --- a/ref/ILogger.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; - -namespace Discord.Logging -{ - public interface ILogger - { - LogSeverity Level { get; } - - void Log(LogSeverity severity, string message, Exception exception = null); - void Error(string message, Exception exception = null); - void Error(Exception exception); - void Warning(string message, Exception exception = null); - void Warning(Exception exception); - void Info(string message, Exception exception = null); - void Info(Exception exception); - void Verbose(string message, Exception exception = null); - void Verbose(Exception exception); - void Debug(string message, Exception exception = null); - void Debug(Exception exception); - -#if DOTNET5_4 - void Log(LogSeverity severity, FormattableString message, Exception exception = null); - void Error(FormattableString message, Exception exception = null); - void Warning(FormattableString message, Exception exception = null); - void Info(FormattableString message, Exception exception = null); - void Verbose(FormattableString message, Exception exception = null); - void Debug(FormattableString message, Exception exception = null); -#endif - } -} diff --git a/ref/MessageQueue.cs b/ref/MessageQueue.cs deleted file mode 100644 index 5f56abd1e..000000000 --- a/ref/MessageQueue.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Discord -{ - public class MessageQueue - { - public int Count { get; } - - public void Clear() { } - } -} diff --git a/ref/Net/HttpException.cs b/ref/Net/HttpException.cs deleted file mode 100644 index 3704ffb83..000000000 --- a/ref/Net/HttpException.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Net; - -namespace Discord.Net -{ - public class HttpException : Exception - { - public HttpStatusCode StatusCode { get; } - - public HttpException(HttpStatusCode statusCode) - : base($"The server responded with error {(int)statusCode} ({statusCode})") - { - StatusCode = statusCode; - } - } -} diff --git a/ref/Net/Rest/CompletedRequestEventArgs.cs b/ref/Net/Rest/CompletedRequestEventArgs.cs deleted file mode 100644 index ed9d1673f..000000000 --- a/ref/Net/Rest/CompletedRequestEventArgs.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Discord.Net.Rest -{ - public class CompletedRequestEventArgs : RequestEventArgs - { - public object Response { get; set; } - public string ResponseJson { get; set; } - public double Milliseconds { get; set; } - - public CompletedRequestEventArgs(IRestRequest request, object response, string responseJson, double milliseconds) - : base(request) - { - Response = response; - ResponseJson = responseJson; - Milliseconds = milliseconds; - } - } -} diff --git a/ref/Net/Rest/IRestClient.cs b/ref/Net/Rest/IRestClient.cs deleted file mode 100644 index 83c0405c7..000000000 --- a/ref/Net/Rest/IRestClient.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Discord.Net.Rest -{ - public interface IRestClient - { - event EventHandler SendingRequest; - event EventHandler SentRequest; - - CancellationToken CancelToken { get; } - string Token { get; } - - Task Send(IRestRequest request) - where ResponseT : class; - Task Send(IRestRequest request); - - Task Send(IRestFileRequest request) - where ResponseT : class; - Task Send(IRestFileRequest request); - } -} diff --git a/ref/Net/Rest/IRestClientProvider.cs b/ref/Net/Rest/IRestClientProvider.cs deleted file mode 100644 index cb22a7474..000000000 --- a/ref/Net/Rest/IRestClientProvider.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Collections.Generic; -using System.Threading; - -namespace Discord.Net.Rest -{ - public interface IRestClientProvider - { - IRestClient Create(string baseUrl, CancellationToken cancelToken); - } -} diff --git a/ref/Net/Rest/RequestEventArgs.cs b/ref/Net/Rest/RequestEventArgs.cs deleted file mode 100644 index cac734fc6..000000000 --- a/ref/Net/Rest/RequestEventArgs.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace Discord.Net.Rest -{ - public class RequestEventArgs : EventArgs - { - public IRestRequest Request { get; set; } - public bool Cancel { get; set; } - - public RequestEventArgs(IRestRequest request) { } - } -} diff --git a/ref/Net/TimeoutException.cs b/ref/Net/TimeoutException.cs deleted file mode 100644 index d1a644049..000000000 --- a/ref/Net/TimeoutException.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace Discord.Net -{ - public class TimeoutException : OperationCanceledException - { - public TimeoutException() { } - } -} diff --git a/ref/Net/WebSocketException.cs b/ref/Net/WebSocketException.cs deleted file mode 100644 index df6377e13..000000000 --- a/ref/Net/WebSocketException.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace Discord.Net -{ - public class WebSocketException : Exception - { - public int Code { get; } - public string Reason { get; } - - public WebSocketException(int code, string reason) { } - } -} diff --git a/ref/Net/WebSockets/BinaryMessageEventArgs.cs b/ref/Net/WebSockets/BinaryMessageEventArgs.cs deleted file mode 100644 index 3fd4425fa..000000000 --- a/ref/Net/WebSockets/BinaryMessageEventArgs.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace Discord.Net.WebSockets -{ - public class BinaryMessageEventArgs : EventArgs - { - public byte[] Data { get; } - - public BinaryMessageEventArgs(byte[] data) { } - } -} diff --git a/ref/Net/WebSockets/GatewaySocket.cs b/ref/Net/WebSockets/GatewaySocket.cs deleted file mode 100644 index e8f2ddd3d..000000000 --- a/ref/Net/WebSockets/GatewaySocket.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Discord.Net.Rest; -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace Discord.Net.WebSockets -{ - public class GatewaySocket - { - public string SessionId { get; } - - public event EventHandler ReceivedDispatch = delegate { }; - - public Task Connect(IRestClient rest, CancellationToken parentCancelToken) => null; - public Task Disconnect() => null; - - public void SendIdentify(string token) { } - - public void SendResume() { } - public void SendHeartbeat() { } - public void SendUpdateStatus(long? idleSince, string gameName) { } - public void SendUpdateVoice(ulong? serverId, ulong? channelId, bool isSelfMuted, bool isSelfDeafened) { } - public void SendRequestMembers(IEnumerable serverId, string query, int limit) { } - - public void WaitForConnection(CancellationToken cancelToken) { } - } -} diff --git a/ref/Net/WebSockets/IWebSocket.cs b/ref/Net/WebSockets/IWebSocket.cs deleted file mode 100644 index 06a274305..000000000 --- a/ref/Net/WebSockets/IWebSocket.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Threading; - -namespace Discord.Net.WebSockets -{ - public interface IWebSocket - { - CancellationToken CancelToken { get; } - ConnectionState State { get; } - string Host { get; set; } - - event EventHandler Connected; - event EventHandler Disconnected; - } -} diff --git a/ref/Net/WebSockets/IWebSocketEngine.cs b/ref/Net/WebSockets/IWebSocketEngine.cs deleted file mode 100644 index 68f31f12b..000000000 --- a/ref/Net/WebSockets/IWebSocketEngine.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace Discord.Net.WebSockets -{ - public interface IWebSocketEngine - { - event EventHandler BinaryMessage; - event EventHandler TextMessage; - - Task Connect(string host, CancellationToken cancelToken); - Task Disconnect(); - void QueueMessage(string message); - IEnumerable GetTasks(CancellationToken cancelToken); - } -} diff --git a/ref/Net/WebSockets/IWebSocketProvider.cs b/ref/Net/WebSockets/IWebSocketProvider.cs deleted file mode 100644 index 20f7559be..000000000 --- a/ref/Net/WebSockets/IWebSocketProvider.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Threading; - -namespace Discord.Net.WebSockets -{ - public interface IWebSocketProvider - { - IWebSocket Create(CancellationToken cancelToken); - } -} diff --git a/ref/Net/WebSockets/TextMessageEventArgs.cs b/ref/Net/WebSockets/TextMessageEventArgs.cs deleted file mode 100644 index e4e186044..000000000 --- a/ref/Net/WebSockets/TextMessageEventArgs.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace Discord.Net.WebSockets -{ - public class TextMessageEventArgs : EventArgs - { - public string Message { get; } - - public TextMessageEventArgs(string msg) { Message = msg; } - } -} diff --git a/ref/Net/WebSockets/WebSocketEventEventArgs.cs b/ref/Net/WebSockets/WebSocketEventEventArgs.cs deleted file mode 100644 index 676c0ba6e..000000000 --- a/ref/Net/WebSockets/WebSocketEventEventArgs.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Newtonsoft.Json.Linq; -using System; - -namespace Discord.Net.WebSockets -{ - public class WebSocketEventEventArgs : EventArgs - { - public string Type { get; } - public JToken Payload { get; } - } -} diff --git a/ref/project.json b/ref/project.json deleted file mode 100644 index 565bc2e86..000000000 --- a/ref/project.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "version": "0.9.0-rc3-3", - "description": "An unofficial .Net API wrapper for the Discord client.", - "authors": [ - "RogueException" - ], - "tags": [ - "discord", - "discordapp" - ], - "projectUrl": "https://github.com/RogueException/Discord.Net", - "licenseUrl": "http://opensource.org/licenses/MIT", - "repository": { - "type": "git", - "url": "git://github.com/RogueException/Discord.Net" - }, - "compile": [ "**/*.cs", "../Discord.Net.Shared/*.cs" ], - - "compilationOptions": { - "allowUnsafe": true, - "warningsAsErrors": true - }, - - "configurations": { - "TestResponses": { - "compilationOptions": { - "define": [ - "DEBUG", - "TRACE", - "TEST_RESPONSES" - ] - } - } - }, - - "dependencies": { - "Newtonsoft.Json": "8.0.1", - "Nito.AsyncEx": "3.0.1" - }, - - "frameworks": { - "dotnet5.4": { - "dependencies": { - "System.Collections": "4.0.11-beta-23516", - "System.Collections.Concurrent": "4.0.11-beta-23516", - "System.Dynamic.Runtime": "4.0.11-beta-23516", - "System.IO.FileSystem": "4.0.1-beta-23516", - "System.IO.Compression": "4.1.0-beta-23516", - "System.Linq": "4.0.1-beta-23516", - "System.Net.Http": "4.0.1-beta-23516", - "System.Net.NameResolution": "4.0.0-beta-23516", - "System.Net.Sockets": "4.1.0-beta-23409", - "System.Net.Requests": "4.0.11-beta-23516", - "System.Net.WebSockets.Client": "4.0.0-beta-23516", - "System.Reflection": "4.1.0-beta-23516", - "System.Reflection.Emit.Lightweight": "4.0.1-beta-23516", - "System.Runtime.InteropServices": "4.0.21-beta-23516", - "System.Runtime.Serialization.Primitives": "4.1.0-beta-23516", - "System.Security.Cryptography.Algorithms": "4.0.0-beta-23516", - "System.Text.RegularExpressions": "4.0.11-beta-23516", - "System.Threading": "4.0.11-beta-23516" - } - }, - "net45": { - "frameworkAssemblies": { - "System.Runtime": { - "type": "build", - "version": "" - }, - "System.Threading.Tasks": { - "type": "build", - "version": "" - } - }, - "dependencies": { - "WebSocket4Net": "0.14.1", - "RestSharp": "105.2.3" - } - } - } -} \ No newline at end of file diff --git a/src/Discord.Net.Audio/AudioClient.cs b/src/Discord.Net.Audio/AudioClient.cs deleted file mode 100644 index 882dca1fe..000000000 --- a/src/Discord.Net.Audio/AudioClient.cs +++ /dev/null @@ -1,283 +0,0 @@ -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.Threading; -using System.Threading.Tasks; - -namespace Discord.Audio -{ - internal class AudioClient : IAudioClient - { - private readonly DiscordConfig _config; - private readonly AsyncLock _connectionLock; - private readonly TaskManager _taskManager; - private ConnectionState _gatewayState; - - 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 VoiceSocket VoiceSocket { get; } - public JsonSerializer Serializer { get; } - - public CancellationToken CancelToken { get; private set; } - public string SessionId => GatewaySocket.SessionId; - - public ConnectionState State => VoiceSocket.State; - public Server Server => VoiceSocket.Server; - public VoiceChannel Channel => VoiceSocket.Channel; - - public AudioClient(DiscordClient client, Server server, int id) - { - Id = id; - Service = client.GetService(); - Config = Service.Config; - Serializer = client.Serializer; - _gatewayState = (int)ConnectionState.Disconnected; - - //Logging - Logger = client.Log.CreateLogger($"AudioClient #{id}"); - - //Async - _taskManager = new TaskManager(Cleanup, false); - _connectionLock = new AsyncLock(); - CancelToken = new CancellationToken(true); - - //Networking - if (Config.EnableMultiserver) - { - //TODO: We can remove this hack when official API launches - var baseConfig = client.Config; - var builder = new DiscordConfigBuilder - { - AppName = baseConfig.AppName, - AppUrl = baseConfig.AppUrl, - AppVersion = baseConfig.AppVersion, - CacheToken = baseConfig.CacheDir != null, - ConnectionTimeout = baseConfig.ConnectionTimeout, - EnablePreUpdateEvents = false, - FailedReconnectDelay = baseConfig.FailedReconnectDelay, - LargeThreshold = 1, - LogLevel = baseConfig.LogLevel, - MessageCacheSize = 0, - ReconnectDelay = baseConfig.ReconnectDelay, - UsePermissionsCache = false - }; - _config = builder.Build(); - - 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 - { - _config = client.Config; - GatewaySocket = client.GatewaySocket; - } - GatewaySocket.ReceivedDispatch += (s, e) => OnReceivedEvent(e); - VoiceSocket = new VoiceSocket(_config, Config, client.Serializer, client.Log.CreateLogger($"Voice #{id}")); - VoiceSocket.Server = server; - } - - public async Task Connect() - { - if (Config.EnableMultiserver) - await BeginGatewayConnect().ConfigureAwait(false); - else - { - 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)) - { - 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 Disconnect() - { - await _taskManager.Stop(true).ConfigureAwait(false); - if (Config.EnableMultiserver) - ClientAPI.Token = null; - } - private async Task Cleanup() - { - var oldState = _gatewayState; - _gatewayState = ConnectionState.Disconnecting; - - if (Config.EnableMultiserver) - { - if (oldState == ConnectionState.Connected) - { - 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 Join(VoiceChannel 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 Task.Run(() => VoiceSocket.WaitForConnection(CancelToken)).ConfigureAwait(false); - } - - private async void OnReceivedEvent(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) as VoiceChannel; - 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); - } - } - - 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(offset)); - if (VoiceSocket.Server == null) return; //Has been closed - if (count == 0) return; - - VoiceSocket.SendPCMFrames(data, offset, count); - } - - public void Clear() - { - if (VoiceSocket.Server == null) return; //Has been closed - VoiceSocket.ClearPCMFrames(); - } - 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/AudioExtensions.cs b/src/Discord.Net.Audio/AudioExtensions.cs deleted file mode 100644 index 7def445a6..000000000 --- a/src/Discord.Net.Audio/AudioExtensions.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace Discord.Audio -{ - public static class AudioExtensions - { - public static DiscordClient UsingAudio(this DiscordClient client, AudioServiceConfig config = null) - { - client.AddService(new AudioService(config)); - return client; - } - public static DiscordClient UsingAudio(this DiscordClient client, Action configFunc = null) - { - var builder = new AudioServiceConfigBuilder(); - configFunc(builder); - client.AddService(new AudioService(builder)); - return client; - } - - public static Task JoinAudio(this VoiceChannel channel) => channel.Client.GetService().Join(channel); - public static Task LeaveAudio(this VoiceChannel channel) => channel.Client.GetService().Leave(channel); - public static Task LeaveAudio(this Server server) => server.Client.GetService().Leave(server); - public static IAudioClient GetAudioClient(this Server server) => server.Client.GetService().GetClient(server); - } -} diff --git a/src/Discord.Net.Audio/AudioMode.cs b/src/Discord.Net.Audio/AudioMode.cs deleted file mode 100644 index b9acdbf89..000000000 --- a/src/Discord.Net.Audio/AudioMode.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Discord.Audio -{ - public enum AudioMode : byte - { - Outgoing = 1, - Incoming = 2, - Both = Outgoing | Incoming - } -} diff --git a/src/Discord.Net.Audio/AudioService.cs b/src/Discord.Net.Audio/AudioService.cs deleted file mode 100644 index e44a4a1ce..000000000 --- a/src/Discord.Net.Audio/AudioService.cs +++ /dev/null @@ -1,193 +0,0 @@ -using Nito.AsyncEx; -using System; -using System.Collections.Concurrent; -using System.Linq; -using System.Threading.Tasks; - -namespace Discord.Audio -{ - public class AudioService : IService - { - 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; - - public DiscordClient Client { get; private set; } - public AudioServiceConfig Config { get; } - - public event EventHandler Connected = delegate { }; - public event EventHandler Disconnected = delegate { }; - public event EventHandler UserIsSpeakingUpdated = delegate { }; - - private void OnConnected() - => Connected(this, EventArgs.Empty); - private void OnDisconnected(ulong serverId, bool wasUnexpected, Exception ex) - => Disconnected(this, new VoiceDisconnectedEventArgs(serverId, wasUnexpected, ex)); - private void OnUserIsSpeakingUpdated(User user, bool isSpeaking) - => UserIsSpeakingUpdated(this, new UserIsSpeakingEventArgs(user, isSpeaking)); - - public AudioService() - : this(new AudioServiceConfigBuilder()) - { - } - public AudioService(AudioServiceConfigBuilder builder) - : this(builder.Build()) - { - } - public AudioService(AudioServiceConfig config) - { - Config = config; - _asyncLock = new AsyncLock(); - - } - void IService.Install(DiscordClient client) - { - Client = client; - - if (Config.EnableMultiserver) - _voiceClients = new ConcurrentDictionary(); - else - { - var logger = Client.Log.CreateLogger("Voice"); - _defaultClient = new AudioClient(Client, null, 0); - } - _talkingUsers = new ConcurrentDictionary(); - - client.GatewaySocket.Disconnected += async (s, e) => - { - if (Config.EnableMultiserver) - { - var tasks = _voiceClients - .Select(x => - { - var val = x.Value; - if (val != null) - return x.Value.Disconnect(); - else - return TaskHelper.CompletedTask; - }) - .ToArray(); - await Task.WhenAll(tasks).ConfigureAwait(false); - _voiceClients.Clear(); - } - foreach (var member in _talkingUsers) - { - bool ignored; - if (_talkingUsers.TryRemove(member.Key, out ignored)) - OnUserIsSpeakingUpdated(member.Key, false); - } - }; - } - - public IAudioClient GetClient(Server server) - { - if (server == null) throw new ArgumentNullException(nameof(server)); - - if (Config.EnableMultiserver) - { - AudioClient client; - if (_voiceClients.TryGetValue(server.Id, out client)) - return client; - else - return null; - } - else - { - if (server == _currentClient.Server) - return _currentClient; - else - return null; - } - } - - //Called from AudioClient.Disconnect - internal async Task RemoveClient(Server server, AudioClient client) - { - using (await _asyncLock.LockAsync().ConfigureAwait(false)) - { - if (_voiceClients.TryUpdate(server.Id, null, client)) - _voiceClients.TryRemove(server.Id, out client); - } - } - - public async Task Join(VoiceChannel channel) - { - if (channel == null) throw new ArgumentNullException(nameof(channel)); - - var server = channel.Server; - using (await _asyncLock.LockAsync().ConfigureAwait(false)) - { - 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().ConfigureAwait(false); - _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; - } - - } - } - - public Task Leave(Server server) => Leave(server, null); - public Task Leave(VoiceChannel channel) => Leave(channel.Server, channel); - private async Task Leave(Server server, VoiceChannel channel) - { - if (server == null) throw new ArgumentNullException(nameof(server)); - - if (Config.EnableMultiserver) - { - AudioClient client; - //Potential race condition if changing channels during this call, but that's acceptable - if (channel == null || (_voiceClients.TryGetValue(server.Id, out client) && client.Channel == channel)) - { - if (_voiceClients.TryRemove(server.Id, out client)) - await client.Disconnect().ConfigureAwait(false); - } - } - else - { - using (await _asyncLock.LockAsync().ConfigureAwait(false)) - { - var client = GetClient(server) as VirtualClient; - if (client != null && client.Channel == channel) - await _defaultClient.Disconnect().ConfigureAwait(false); - } - } - - } - } -} diff --git a/src/Discord.Net.Audio/AudioServiceConfig.cs b/src/Discord.Net.Audio/AudioServiceConfig.cs deleted file mode 100644 index 89d05d85b..000000000 --- a/src/Discord.Net.Audio/AudioServiceConfig.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace Discord.Audio -{ - public class AudioServiceConfigBuilder - { - /// Enables the voice websocket and UDP client and specifies how it will be used. - public AudioMode Mode { get; set; } = AudioMode.Outgoing; - - /// Enables the voice websocket and UDP client. This option requires the libsodium .dll or .so be in the local or system folder. - public bool EnableEncryption { get; set; } = true; - /// - /// Enables the client to be simultaneously connected to multiple channels at once (Discord still limits you to one channel per server). - /// This option uses a lot of CPU power and network bandwidth, as a new gateway connection needs to be spun up per server. Use sparingly. - /// - public bool EnableMultiserver { get; set; } = false; - - /// Gets or sets the buffer length (in milliseconds) for outgoing voice packets. - public int BufferLength { get; set; } = 1000; - /// 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; set; } = 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; set; } = 2; - - public AudioServiceConfig Build() => new AudioServiceConfig(this); - } - - public class AudioServiceConfig - { - public const int MaxBitrate = 128; - - public AudioMode Mode { get; } - - public bool EnableEncryption { get; } - public bool EnableMultiserver { get; } - - public int BufferLength { get; } - public int? Bitrate { get; } - public int Channels { get; } - - internal AudioServiceConfig(AudioServiceConfigBuilder builder) - { - Mode = builder.Mode; - - EnableEncryption = builder.EnableEncryption; - EnableMultiserver = builder.EnableMultiserver; - - BufferLength = builder.BufferLength; - Bitrate = builder.Bitrate; - Channels = builder.Channels; - } - } -} diff --git a/src/Discord.Net.Audio/Discord.Net.Audio.xproj b/src/Discord.Net.Audio/Discord.Net.Audio.xproj deleted file mode 100644 index 4eb480f88..000000000 --- a/src/Discord.Net.Audio/Discord.Net.Audio.xproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - dff7afe3-ca77-4109-bade-b4b49a4f6648 - Discord.Audio - ..\..\artifacts\obj\$(MSBuildProjectName) - ..\..\artifacts\bin\$(MSBuildProjectName)\ - - - 2.0 - - - True - - - \ No newline at end of file diff --git a/src/Discord.Net.Audio/IAudioClient.cs b/src/Discord.Net.Audio/IAudioClient.cs deleted file mode 100644 index a986fad7d..000000000 --- a/src/Discord.Net.Audio/IAudioClient.cs +++ /dev/null @@ -1,46 +0,0 @@ -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. - VoiceChannel Channel { get; } - /// Gets the server this client is bound to. - Server Server { 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(VoiceChannel 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. - /// PCM frame to send. This must be a single or collection of uncompressed 48Kz monochannel 20ms PCM frames. - /// Offset . - /// Number of bytes in this frame. - void Send(byte[] data, int offset, int count); - /// Clears the PCM buffer. - void Clear(); - /// Blocks until the voice output buffer is empty. - void Wait(); - } -} diff --git a/src/Discord.Net.Audio/InternalFrameEventArgs.cs b/src/Discord.Net.Audio/InternalFrameEventArgs.cs deleted file mode 100644 index b74dc8295..000000000 --- a/src/Discord.Net.Audio/InternalFrameEventArgs.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -namespace Discord -{ - internal class InternalFrameEventArgs : EventArgs - { - public ulong UserId { get; } - public ulong ChannelId { get; } - public byte[] Buffer { get; } - public int Offset { get; } - public int Count { get; } - - public InternalFrameEventArgs(ulong userId, ulong channelId, byte[] buffer, int offset, int count) - { - UserId = userId; - ChannelId = channelId; - Buffer = buffer; - Offset = offset; - Count = count; - } - } -} diff --git a/src/Discord.Net.Audio/InternalIsSpeakingEventArgs.cs b/src/Discord.Net.Audio/InternalIsSpeakingEventArgs.cs deleted file mode 100644 index 641e863f4..000000000 --- a/src/Discord.Net.Audio/InternalIsSpeakingEventArgs.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Discord.Audio -{ - internal class InternalIsSpeakingEventArgs - { - public ulong UserId { get; } - public bool IsSpeaking { get; } - - public InternalIsSpeakingEventArgs(ulong userId, bool isSpeaking) - { - UserId = userId; - IsSpeaking = isSpeaking; - } - } -} diff --git a/src/Discord.Net.Audio/Net/VoiceSocket.cs b/src/Discord.Net.Audio/Net/VoiceSocket.cs deleted file mode 100644 index 9ee5c60e0..000000000 --- a/src/Discord.Net.Audio/Net/VoiceSocket.cs +++ /dev/null @@ -1,516 +0,0 @@ -using Discord.API.Client; -using Discord.API.Client.VoiceSocket; -using Discord.Audio; -using Discord.Audio.Opus; -using Discord.Audio.Sodium; -using Discord.Logging; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Sockets; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace Discord.Net.WebSockets -{ - public partial class VoiceSocket : WebSocket - { - private const int MaxOpusSize = 4000; - private const string EncryptedMode = "xsalsa20_poly1305"; - private const string UnencryptedMode = "plain"; - - private readonly int _targetAudioBufferLength; - private readonly ConcurrentDictionary _decoders; - private readonly AudioServiceConfig _audioConfig; - private Task _sendTask, _receiveTask; - private VoiceBuffer _sendBuffer; - private OpusEncoder _encoder; - private uint _ssrc; - private ConcurrentDictionary _ssrcMapping; - private UdpClient _udp; - private IPEndPoint _endpoint; - private bool _isEncrypted; - private byte[] _secretKey, _encodingBuffer; - 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; } - public VoiceChannel Channel { get; internal set; } - - public int Ping => _ping; - internal VoiceBuffer OutputBuffer => _sendBuffer; - - internal event EventHandler UserIsSpeaking = delegate { }; - internal event EventHandler FrameReceived = delegate { }; - - private void OnUserIsSpeaking(ulong userId, bool isSpeaking) - => UserIsSpeaking(this, new InternalIsSpeakingEventArgs(userId, isSpeaking)); - internal void OnFrameReceived(ulong userId, ulong channelId, byte[] buffer, int offset, int count) - => FrameReceived(this, new InternalFrameEventArgs(userId, channelId, buffer, offset, count)); - - internal VoiceSocket(DiscordConfig config, AudioServiceConfig audioConfig, JsonSerializer serializer, Logger logger) - : base(config, serializer, logger) - { - _audioConfig = audioConfig; - _decoders = new ConcurrentDictionary(); - _targetAudioBufferLength = _audioConfig.BufferLength / 20; //20 ms frames - _encodingBuffer = new byte[MaxOpusSize]; - _ssrcMapping = new ConcurrentDictionary(); - _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(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; - await Task.Delay(_config.ReconnectDelay, cancelToken).ConfigureAwait(false); - while (!cancelToken.IsCancellationRequested) - { - try - { - await BeginConnect(_parentCancelToken).ConfigureAwait(false); - break; - } - catch (OperationCanceledException) { throw; } - catch (Exception ex) - { - Logger.Error("Reconnect failed", ex); - //Net is down? We can keep trying to reconnect until the user runs Disconnect() - await Task.Delay(_config.FailedReconnectDelay, cancelToken).ConfigureAwait(false); - } - } - } - catch (OperationCanceledException) { } - } - 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 (_audioConfig.Mode.HasFlag(AudioMode.Outgoing)) - _sendTask = Task.Run(() => SendVoiceAsync(CancelToken)); - _receiveTask = Task.Run(() => ReceiveVoiceAsync(CancelToken)); - - SendIdentify(_userId.Value, _sessionId); - -#if !DOTNET5_4 - tasks.Add(WatcherAsync()); -#endif - tasks.AddRange(_engine.GetTasks(CancelToken)); - tasks.Add(HeartbeatAsync(CancelToken)); - await _taskManager.Start(tasks, _cancelSource).ConfigureAwait(false); - } - protected override async Task Cleanup() - { - var sendThread = _sendTask; - if (sendThread != null) - { - try { await sendThread.ConfigureAwait(false); } - catch (Exception) { } //Ignore any errors during cleanup - } - _sendTask = null; - - var receiveThread = _receiveTask; - if (receiveThread != null) - { - try { await receiveThread.ConfigureAwait(false); } - catch (Exception) { } //Ignore any errors during cleanup - } - _receiveTask = null; - - OpusDecoder decoder; - foreach (var pair in _decoders) - { - if (_decoders.TryRemove(pair.Key, out decoder)) - decoder.Dispose(); - } - - ClearPCMFrames(); - _udp = null; - - await base.Cleanup().ConfigureAwait(false); - } - - private async Task ReceiveVoiceAsync(CancellationToken cancelToken) - { - var closeTask = cancelToken.Wait(); - try - { - byte[] packet, decodingBuffer = null, nonce = null, result; - int packetLength, resultOffset, resultLength; - IPEndPoint endpoint = new IPEndPoint(IPAddress.Any, 0); - - if ((_audioConfig.Mode & AudioMode.Incoming) != 0) - { - decodingBuffer = new byte[MaxOpusSize]; - nonce = new byte[24]; - } - - while (!cancelToken.IsCancellationRequested) - { - await Task.Delay(1).ConfigureAwait(false); - if (_udp.Available > 0) - { -#if !DOTNET5_4 - packet = _udp.Receive(ref endpoint); -#else - //TODO: Is this really the only way to end a Receive call in DOTNET5_4? - var receiveTask = _udp.ReceiveAsync(); - var task = Task.WhenAny(closeTask, receiveTask).Result; - if (task == closeTask) - break; - var udpPacket = receiveTask.Result; - packet = udpPacket.Buffer; - endpoint = udpPacket.RemoteEndPoint; -#endif - packetLength = packet.Length; - - if (packetLength > 0 && endpoint.Equals(_endpoint)) - { - if (State != ConnectionState.Connected) - { - if (packetLength != 70) - return; - - string ip = Encoding.UTF8.GetString(packet, 4, 70 - 6).TrimEnd('\0'); - int port = packet[68] | packet[69] << 8; - - SendSelectProtocol(ip, port); - if ((_audioConfig.Mode & AudioMode.Incoming) == 0) - return; //We dont need this thread anymore - } - else - { - //Parse RTP Data - if (packetLength < 12) return; - if (packet[0] != 0x80) return; //Flags - if (packet[1] != 0x78) return; //Payload Type - - ushort sequenceNumber = (ushort)((packet[2] << 8) | - packet[3] << 0); - uint timestamp = (uint)((packet[4] << 24) | - (packet[5] << 16) | - (packet[6] << 8) | - (packet[7] << 0)); - uint ssrc = (uint)((packet[8] << 24) | - (packet[9] << 16) | - (packet[10] << 8) | - (packet[11] << 0)); - - //Decrypt - if (_isEncrypted) - { - if (packetLength < 28) //12 + 16 (RTP + Poly1305 MAC) - return; - - Buffer.BlockCopy(packet, 0, nonce, 0, 12); - int ret = SecretBox.Decrypt(packet, 12, packetLength - 12, decodingBuffer, nonce, _secretKey); - if (ret != 0) - continue; - result = decodingBuffer; - resultOffset = 0; - resultLength = packetLength - 28; - } - else //Plain - { - result = packet; - resultOffset = 12; - resultLength = packetLength - 12; - } - - /*if (_logLevel >= LogMessageSeverity.Debug) - RaiseOnLog(LogMessageSeverity.Debug, $"Received {buffer.Length - 12} bytes.");*/ - - ulong userId; - if (_ssrcMapping.TryGetValue(ssrc, out userId)) - OnFrameReceived(userId, Channel.Id, result, resultOffset, resultLength); - } - } - } - } - } - catch (OperationCanceledException) { } - catch (InvalidOperationException) { } //Includes ObjectDisposedException - } - - private async Task SendVoiceAsync(CancellationToken cancelToken) - { - try - { - while (!cancelToken.IsCancellationRequested && State != ConnectionState.Connected) - await Task.Delay(1).ConfigureAwait(false); - - if (cancelToken.IsCancellationRequested) - return; - - byte[] frame = new byte[_encoder.FrameSize]; - byte[] encodedFrame = new byte[MaxOpusSize]; - byte[] voicePacket, pingPacket, nonce = null; - uint timestamp = 0; - double nextTicks = 0.0, nextPingTicks = 0.0; - long ticksPerSeconds = Stopwatch.Frequency; - double ticksPerMillisecond = Stopwatch.Frequency / 1000.0; - double ticksPerFrame = ticksPerMillisecond * _encoder.FrameLength; - double spinLockThreshold = 3 * ticksPerMillisecond; - uint samplesPerFrame = (uint)_encoder.SamplesPerFrame; - Stopwatch sw = Stopwatch.StartNew(); - - if (_isEncrypted) - { - nonce = new byte[24]; - voicePacket = new byte[MaxOpusSize + 12 + 16]; - } - else - voicePacket = new byte[MaxOpusSize + 12]; - - pingPacket = new byte[8]; - - int rtpPacketLength = 0; - voicePacket[0] = 0x80; //Flags; - voicePacket[1] = 0x78; //Payload Type - voicePacket[8] = (byte)(_ssrc >> 24); - voicePacket[9] = (byte)(_ssrc >> 16); - voicePacket[10] = (byte)(_ssrc >> 8); - voicePacket[11] = (byte)(_ssrc >> 0); - - if (_isEncrypted) - Buffer.BlockCopy(voicePacket, 0, nonce, 0, 12); - - bool hasFrame = false; - while (!cancelToken.IsCancellationRequested) - { - if (!hasFrame && _sendBuffer.Pop(frame)) - { - ushort sequence = unchecked(_sequence++); - voicePacket[2] = (byte)(sequence >> 8); - voicePacket[3] = (byte)(sequence >> 0); - voicePacket[4] = (byte)(timestamp >> 24); - voicePacket[5] = (byte)(timestamp >> 16); - voicePacket[6] = (byte)(timestamp >> 8); - voicePacket[7] = (byte)(timestamp >> 0); - - //Encode - int encodedLength = _encoder.EncodeFrame(frame, 0, encodedFrame); - - //Encrypt - if (_isEncrypted) - { - Buffer.BlockCopy(voicePacket, 2, nonce, 2, 6); //Update nonce - int ret = SecretBox.Encrypt(encodedFrame, encodedLength, voicePacket, 12, nonce, _secretKey); - if (ret != 0) - continue; - rtpPacketLength = encodedLength + 12 + 16; - } - else - { - Buffer.BlockCopy(encodedFrame, 0, voicePacket, 12, encodedLength); - rtpPacketLength = encodedLength + 12; - } - - timestamp = unchecked(timestamp + samplesPerFrame); - hasFrame = true; - } - - long currentTicks = sw.ElapsedTicks; - double ticksToNextFrame = nextTicks - currentTicks; - if (ticksToNextFrame <= 0.0) - { - if (hasFrame) - { - try - { - _udp.Send(voicePacket, rtpPacketLength); - } - catch (SocketException ex) - { - Logger.Error("Failed to send UDP packet.", ex); - } - hasFrame = false; - } - nextTicks += ticksPerFrame; - - //Is it time to send out another ping? - if (currentTicks > nextPingTicks) - { - //Increment in LE - for (int i = 0; i < 8; i++) - { - var b = pingPacket[i]; - if (b == byte.MaxValue) - pingPacket[i] = 0; - else - { - pingPacket[i] = (byte)(b + 1); - break; - } - } - await _udp.SendAsync(pingPacket, pingPacket.Length).ConfigureAwait(false); - nextPingTicks = currentTicks + 5 * ticksPerSeconds; - } - } - else - { - if (hasFrame) - { - int time = (int)Math.Floor(ticksToNextFrame / ticksPerMillisecond); - if (time > 0) - await Task.Delay(time).ConfigureAwait(false); - } - else - await Task.Delay(1).ConfigureAwait(false); //Give as much time to the encrypter as possible - } - } - } - catch (OperationCanceledException) { } - catch (InvalidOperationException) { } //Includes ObjectDisposedException - } -#if !DOTNET5_4 - //Closes the UDP socket when _disconnectToken is triggered, since UDPClient doesn't allow passing a canceltoken - private async Task WatcherAsync() - { - await CancelToken.Wait().ConfigureAwait(false); - _udp.Close(); - } -#endif - - protected override async Task ProcessMessage(string json) - { - await base.ProcessMessage(json).ConfigureAwait(false); - - WebSocketMessage msg; - using (var reader = new JsonTextReader(new StringReader(json))) - msg = _serializer.Deserialize(reader, typeof(WebSocketMessage)) as WebSocketMessage; - - var opCode = (OpCodes)msg.Operation; - switch (opCode) - { - case OpCodes.Ready: - { - if (State != ConnectionState.Connected) - { - var payload = (msg.Payload as JToken).ToObject(_serializer); - _heartbeatInterval = payload.HeartbeatInterval; - _ssrc = payload.SSRC; - var address = (await Dns.GetHostAddressesAsync(Host.Replace("wss://", "")).ConfigureAwait(false)).FirstOrDefault(); - _endpoint = new IPEndPoint(address, payload.Port); - - if (_audioConfig.EnableEncryption) - { - if (payload.Modes.Contains(EncryptedMode)) - { - _encryptionMode = EncryptedMode; - _isEncrypted = true; - } - else - throw new InvalidOperationException("Unexpected encryption format."); - } - else - { - _encryptionMode = UnencryptedMode; - _isEncrypted = false; - } - _udp.Connect(_endpoint); - - _sequence = 0;// (ushort)_rand.Next(0, ushort.MaxValue); - //No thread issue here because SendAsync doesn't start until _isReady is true - byte[] packet = new byte[70]; - packet[0] = (byte)(_ssrc >> 24); - packet[1] = (byte)(_ssrc >> 16); - packet[2] = (byte)(_ssrc >> 8); - packet[3] = (byte)(_ssrc >> 0); - await _udp.SendAsync(packet, 70).ConfigureAwait(false); - } - } - break; - case OpCodes.Heartbeat: - { - long time = EpochTime.GetMilliseconds(); - var payload = (long)msg.Payload; - _ping = (int)(payload - time); - //TODO: Use this to estimate latency - } - break; - case OpCodes.SessionDescription: - { - var payload = (msg.Payload as JToken).ToObject(_serializer); - _secretKey = payload.SecretKey; - SendSetSpeaking(true); - await EndConnect().ConfigureAwait(false); - } - break; - case OpCodes.Speaking: - { - var payload = (msg.Payload as JToken).ToObject(_serializer); - OnUserIsSpeaking(payload.UserId, payload.IsSpeaking); - } - break; - default: - Logger.Warning($"Unknown Opcode: {opCode}"); - break; - } - } - - public void SendPCMFrames(byte[] data, int offset, int count) - { - _sendBuffer.Push(data, offset, count, CancelToken); - } - public void ClearPCMFrames() - { - _sendBuffer.Clear(CancelToken); - } - - public void WaitForQueue() - { - _sendBuffer.Wait(CancelToken); - } - - public override void SendHeartbeat() - => QueueMessage(new HeartbeatCommand()); - public void SendIdentify(ulong id, string sessionId) - => QueueMessage(new IdentifyCommand - { - GuildId = Server.Id, - UserId = id, - SessionId = sessionId, - Token = Token - }); - public void SendSelectProtocol(string externalAddress, int externalPort) - => QueueMessage(new SelectProtocolCommand - { - Protocol = "udp", - ExternalAddress = externalAddress, - ExternalPort = externalPort, - EncryptionMode = _encryptionMode - }); - public void SendSetSpeaking(bool value) - => QueueMessage(new SetSpeakingCommand { IsSpeaking = value, Delay = 0 }); - - } -} \ No newline at end of file diff --git a/src/Discord.Net.Audio/Opus/OpusConverter.cs b/src/Discord.Net.Audio/Opus/OpusConverter.cs deleted file mode 100644 index d93337138..000000000 --- a/src/Discord.Net.Audio/Opus/OpusConverter.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System; -using System.Runtime.InteropServices; -#if NET45 -using System.Security; -#endif - -namespace Discord.Audio.Opus -{ - internal enum OpusApplication : int - { - Voice = 2048, - MusicOrMixed = 2049, - LowLatency = 2051 - } - internal enum OpusError : int - { - OK = 0, - BadArg = -1, - BufferToSmall = -2, - InternalError = -3, - InvalidPacket = -4, - Unimplemented = -5, - InvalidState = -6, - AllocFail = -7 - } - - internal abstract class OpusConverter : IDisposable - { - protected enum Ctl : int - { - SetBitrateRequest = 4002, - GetBitrateRequest = 4003, - SetInbandFECRequest = 4012, - GetInbandFECRequest = 4013 - } - -#if NET45 - [SuppressUnmanagedCodeSecurity] -#endif - protected unsafe static class UnsafeNativeMethods - { - [DllImport("opus", EntryPoint = "opus_encoder_create", CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr CreateEncoder(int Fs, int channels, int application, out OpusError error); - [DllImport("opus", EntryPoint = "opus_encoder_destroy", CallingConvention = CallingConvention.Cdecl)] - public static extern void DestroyEncoder(IntPtr encoder); - [DllImport("opus", EntryPoint = "opus_encode", CallingConvention = CallingConvention.Cdecl)] - public static extern int Encode(IntPtr st, byte* pcm, int frame_size, byte[] data, int max_data_bytes); - [DllImport("opus", EntryPoint = "opus_encoder_ctl", CallingConvention = CallingConvention.Cdecl)] - public static extern int EncoderCtl(IntPtr st, Ctl request, int value); - - [DllImport("opus", EntryPoint = "opus_decoder_create", CallingConvention = CallingConvention.Cdecl)] - public static extern IntPtr CreateDecoder(int Fs, int channels, out OpusError error); - [DllImport("opus", EntryPoint = "opus_decoder_destroy", CallingConvention = CallingConvention.Cdecl)] - public static extern void DestroyDecoder(IntPtr decoder); - [DllImport("opus", EntryPoint = "opus_decode", CallingConvention = CallingConvention.Cdecl)] - public static extern int Decode(IntPtr st, byte* data, int len, byte[] pcm, int frame_size, int decode_fec); - } - - protected IntPtr _ptr; - - /// Gets the bit rate of this converter. - public const int BitsPerSample = 16; - /// Gets the input sampling rate of this converter. - public int InputSamplingRate { get; } - /// Gets the number of channels of this converter. - public int InputChannels { get; } - /// Gets the milliseconds per frame. - public int FrameLength { get; } - /// Gets the number of samples per frame. - public int SamplesPerFrame { get; } - /// Gets the bytes per frame. - public int FrameSize { get; } - /// Gets the bytes per sample. - public int SampleSize { get; } - - protected OpusConverter(int samplingRate, int channels, int frameLength) - { - if (samplingRate != 8000 && samplingRate != 12000 && - samplingRate != 16000 && samplingRate != 24000 && - samplingRate != 48000) - throw new ArgumentOutOfRangeException(nameof(samplingRate)); - if (channels != 1 && channels != 2) - throw new ArgumentOutOfRangeException(nameof(channels)); - - InputSamplingRate = samplingRate; - InputChannels = channels; - FrameLength = frameLength; - SampleSize = (BitsPerSample / 8) * channels; - SamplesPerFrame = samplingRate / 1000 * FrameLength; - FrameSize = SamplesPerFrame * SampleSize; - } - - #region IDisposable Support - private bool disposedValue = false; // To detect redundant calls - - protected virtual void Dispose(bool disposing) - { - if (!disposedValue) - disposedValue = true; - } - ~OpusConverter() { - Dispose(false); - } - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - #endregion - } -} diff --git a/src/Discord.Net.Audio/Opus/OpusDecoder.cs b/src/Discord.Net.Audio/Opus/OpusDecoder.cs deleted file mode 100644 index d8e6b8087..000000000 --- a/src/Discord.Net.Audio/Opus/OpusDecoder.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; - -namespace Discord.Audio.Opus -{ - internal class OpusDecoder : OpusConverter - { - /// Creates a new Opus decoder. - /// Sampling rate of the input PCM (in Hz). Supported Values: 8000, 12000, 16000, 24000, or 48000 - /// Length, in milliseconds, of each frame. Supported Values: 2.5, 5, 10, 20, 40, or 60 - public OpusDecoder(int samplingRate, int channels, int frameLength) - : base(samplingRate, channels, frameLength) - { - OpusError error; - _ptr = UnsafeNativeMethods.CreateDecoder(samplingRate, channels, out error); - if (error != OpusError.OK) - throw new InvalidOperationException($"Error occured while creating decoder: {error}"); - } - - /// Produces PCM samples from Opus-encoded audio. - /// PCM samples to decode. - /// Offset of the frame in input. - /// Buffer to store the decoded frame. - public unsafe int DecodeFrame(byte[] input, int inputOffset, int inputCount, byte[] output) - { - int result = 0; - fixed (byte* inPtr = input) - result = UnsafeNativeMethods.Decode(_ptr, inPtr + inputOffset, inputCount, output, SamplesPerFrame, 0); - - if (result < 0) - throw new Exception(((OpusError)result).ToString()); - return result; - } - - protected override void Dispose(bool disposing) - { - if (_ptr != IntPtr.Zero) - { - UnsafeNativeMethods.DestroyDecoder(_ptr); - _ptr = IntPtr.Zero; - } - } - } -} \ No newline at end of file diff --git a/src/Discord.Net.Audio/Opus/OpusEncoder.cs b/src/Discord.Net.Audio/Opus/OpusEncoder.cs deleted file mode 100644 index be0623c6b..000000000 --- a/src/Discord.Net.Audio/Opus/OpusEncoder.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; - -namespace Discord.Audio.Opus -{ - internal class OpusEncoder : OpusConverter - { - /// Gets the bit rate in kbit/s. - public int? BitRate { get; } - /// Gets the coding mode of the encoder. - public OpusApplication Application { get; } - - /// Creates a new Opus encoder. - /// Sampling rate of the input signal (Hz). Supported Values: 8000, 12000, 16000, 24000, or 48000 - /// Number of channels in input signal. Supported Values: 1 or 2 - /// Length, in milliseconds, that each frame takes. Supported Values: 2.5, 5, 10, 20, 40, 60 - /// Bitrate (kbit/s) used for this encoder. Supported Values: 1-512. Null will use the recommended bitrate. - /// Coding mode. - public OpusEncoder(int samplingRate, int channels, int frameLength, int? bitrate, OpusApplication application) - : base(samplingRate, channels, frameLength) - { - if (bitrate != null && (bitrate < 1 || bitrate > AudioServiceConfig.MaxBitrate)) - throw new ArgumentOutOfRangeException(nameof(bitrate)); - - BitRate = bitrate; - Application = application; - - OpusError error; - _ptr = UnsafeNativeMethods.CreateEncoder(samplingRate, channels, (int)application, out error); - if (error != OpusError.OK) - throw new InvalidOperationException($"Error occured while creating encoder: {error}"); - - SetForwardErrorCorrection(true); - if (bitrate != null) - SetBitrate(bitrate.Value); - } - - /// Produces Opus encoded audio from PCM samples. - /// PCM samples to encode. - /// Offset of the frame in pcmSamples. - /// Buffer to store the encoded frame. - /// Length of the frame contained in outputBuffer. - public unsafe int EncodeFrame(byte[] input, int inputOffset, byte[] output) - { - int result = 0; - fixed (byte* inPtr = input) - result = UnsafeNativeMethods.Encode(_ptr, inPtr + inputOffset, SamplesPerFrame, output, output.Length); - - if (result < 0) - throw new Exception(((OpusError)result).ToString()); - return result; - } - - /// Gets or sets whether Forward Error Correction is enabled. - public void SetForwardErrorCorrection(bool value) - { - var result = UnsafeNativeMethods.EncoderCtl(_ptr, Ctl.SetInbandFECRequest, value ? 1 : 0); - if (result < 0) - throw new Exception(((OpusError)result).ToString()); - } - - /// Gets or sets whether Forward Error Correction is enabled. - public void SetBitrate(int value) - { - var result = UnsafeNativeMethods.EncoderCtl(_ptr, Ctl.SetBitrateRequest, value * 1000); - if (result < 0) - throw new Exception(((OpusError)result).ToString()); - } - - protected override void Dispose(bool disposing) - { - if (_ptr != IntPtr.Zero) - { - UnsafeNativeMethods.DestroyEncoder(_ptr); - _ptr = IntPtr.Zero; - } - } - } -} \ No newline at end of file diff --git a/src/Discord.Net.Audio/Sodium/SecretBox.cs b/src/Discord.Net.Audio/Sodium/SecretBox.cs deleted file mode 100644 index f73093316..000000000 --- a/src/Discord.Net.Audio/Sodium/SecretBox.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Runtime.InteropServices; -#if NET45 -using System.Security; -#endif - -namespace Discord.Audio.Sodium -{ - internal unsafe static class SecretBox - { -#if NET45 - [SuppressUnmanagedCodeSecurity] -#endif - private static class SafeNativeMethods - { - [DllImport("libsodium", EntryPoint = "crypto_secretbox_easy", CallingConvention = CallingConvention.Cdecl)] - public static extern int SecretBoxEasy(byte* output, byte[] input, long inputLength, byte[] nonce, byte[] secret); - [DllImport("libsodium", EntryPoint = "crypto_secretbox_open_easy", CallingConvention = CallingConvention.Cdecl)] - public static extern int SecretBoxOpenEasy(byte[] output, byte* input, long inputLength, byte[] nonce, byte[] secret); - } - - public static int Encrypt(byte[] input, long inputLength, byte[] output, int outputOffset, byte[] nonce, byte[] secret) - { - fixed (byte* outPtr = output) - return SafeNativeMethods.SecretBoxEasy(outPtr + outputOffset, input, inputLength, nonce, secret); - } - public static int Decrypt(byte[] input, int inputOffset, long inputLength, byte[] output, byte[] nonce, byte[] secret) - { - fixed (byte* inPtr = input) - return SafeNativeMethods.SecretBoxOpenEasy(output, inPtr + inputLength, inputLength, nonce, secret); - } - } -} diff --git a/src/Discord.Net.Audio/UserIsTalkingEventArgs.cs b/src/Discord.Net.Audio/UserIsTalkingEventArgs.cs deleted file mode 100644 index 698f44d4c..000000000 --- a/src/Discord.Net.Audio/UserIsTalkingEventArgs.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Discord -{ - public class UserIsSpeakingEventArgs : UserEventArgs - { - public bool IsSpeaking { get; } - - public UserIsSpeakingEventArgs(User user, bool isSpeaking) - : base(user) - { - IsSpeaking = isSpeaking; - } - } -} diff --git a/src/Discord.Net.Audio/VirtualClient.cs b/src/Discord.Net.Audio/VirtualClient.cs deleted file mode 100644 index 9c8100e47..000000000 --- a/src/Discord.Net.Audio/VirtualClient.cs +++ /dev/null @@ -1,39 +0,0 @@ -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 VoiceChannel Channel => _client.Server == Server ? _client.Channel : 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(VoiceChannel 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/VoiceBuffer.cs b/src/Discord.Net.Audio/VoiceBuffer.cs deleted file mode 100644 index 054ab81a0..000000000 --- a/src/Discord.Net.Audio/VoiceBuffer.cs +++ /dev/null @@ -1,140 +0,0 @@ -using Nito.AsyncEx; -using System; -using System.Threading; - -namespace Discord.Audio -{ - internal class VoiceBuffer - { - private readonly int _frameSize, _frameCount, _bufferSize; - private readonly byte[] _buffer; - private readonly byte[] _blankFrame; - private ushort _readCursor, _writeCursor; - private ManualResetEventSlim _notOverflowEvent; - private bool _isClearing; - private AsyncLock _lock; - - public int FrameSize => _frameSize; - public int FrameCount => _frameCount; - public ushort ReadPos => _readCursor; - public ushort WritePos => _writeCursor; - - public VoiceBuffer(int frameCount, int frameSize) - { - _frameSize = frameSize; - _frameCount = frameCount; - _bufferSize = _frameSize * _frameCount; - _readCursor = 0; - _writeCursor = 0; - _buffer = new byte[_bufferSize]; - _blankFrame = new byte[_frameSize]; - _notOverflowEvent = new ManualResetEventSlim(); //Notifies when an overflow is solved - _lock = new AsyncLock(); - } - - public void Push(byte[] buffer, int offset, int count, CancellationToken cancelToken) - { - if (cancelToken.IsCancellationRequested) - throw new OperationCanceledException("Client is disconnected.", cancelToken); - - int wholeFrames = count / _frameSize; - int expectedBytes = wholeFrames * _frameSize; - int lastFrameSize = count - expectedBytes; - - using (_lock.Lock()) - { - for (int i = 0, pos = offset; i <= wholeFrames; i++, pos += _frameSize) - { - //If the read cursor is in the next position, wait for it to move. - ushort nextPosition = _writeCursor; - AdvanceCursorPos(ref nextPosition); - if (_readCursor == nextPosition) - { - _notOverflowEvent.Reset(); - try - { - _notOverflowEvent.Wait(cancelToken); - } - catch (OperationCanceledException ex) - { - throw new OperationCanceledException("Client is disconnected.", ex, cancelToken); - } - } - - if (i == wholeFrames) - { - //If there are no partial frames, skip this step - if (lastFrameSize == 0) - break; - - //Copy partial frame - Buffer.BlockCopy(buffer, pos, _buffer, _writeCursor * _frameSize, lastFrameSize); - - //Wipe the end of the buffer - Buffer.BlockCopy(_blankFrame, 0, _buffer, _writeCursor * _frameSize + lastFrameSize, _frameSize - lastFrameSize); - } - else - { - //Copy full frame - Buffer.BlockCopy(buffer, pos, _buffer, _writeCursor * _frameSize, _frameSize); - } - - //Advance the write cursor to the next position - AdvanceCursorPos(ref _writeCursor); - } - } - } - - public bool Pop(byte[] buffer) - { - //using (_lock.Lock()) - //{ - if (_writeCursor == _readCursor) - { - _notOverflowEvent.Set(); - return false; - } - - bool isClearing = _isClearing; - if (!isClearing) - Buffer.BlockCopy(_buffer, _readCursor * _frameSize, buffer, 0, _frameSize); - - //Advance the read cursor to the next position - AdvanceCursorPos(ref _readCursor); - _notOverflowEvent.Set(); - return !isClearing; - //} - } - - public void Clear(CancellationToken cancelToken) - { - using (_lock.Lock()) - { - _isClearing = true; - for (int i = 0; i < _frameCount; i++) - Buffer.BlockCopy(_blankFrame, 0, _buffer, i * _frameCount, i++); - - _writeCursor = 0; - _readCursor = 0; - _isClearing = false; - } - } - - public void Wait(CancellationToken cancelToken) - { - while (true) - { - _notOverflowEvent.Wait(cancelToken); - if (_writeCursor == _readCursor) - break; - } - } - - private void AdvanceCursorPos(ref ushort pos) - { - pos++; - if (pos == _frameCount) - pos = 0; - } - } -} \ No newline at end of file diff --git a/src/Discord.Net.Audio/VoiceDisconnectedEventArgs.cs b/src/Discord.Net.Audio/VoiceDisconnectedEventArgs.cs deleted file mode 100644 index 4f46abde2..000000000 --- a/src/Discord.Net.Audio/VoiceDisconnectedEventArgs.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace Discord -{ - public class VoiceDisconnectedEventArgs : DisconnectedEventArgs - { - public ulong ServerId { get; } - - public VoiceDisconnectedEventArgs(ulong serverId, bool wasUnexpected, Exception ex) - : base(wasUnexpected, ex) - { - ServerId = serverId; - } - } -} diff --git a/src/Discord.Net.Audio/libsodium.dll b/src/Discord.Net.Audio/libsodium.dll deleted file mode 100644 index a9ab5078e..000000000 Binary files a/src/Discord.Net.Audio/libsodium.dll and /dev/null differ diff --git a/src/Discord.Net.Audio/opus.dll b/src/Discord.Net.Audio/opus.dll deleted file mode 100644 index a9eec802c..000000000 Binary files a/src/Discord.Net.Audio/opus.dll and /dev/null differ diff --git a/src/Discord.Net.Audio/project.json b/src/Discord.Net.Audio/project.json deleted file mode 100644 index c41a619c7..000000000 --- a/src/Discord.Net.Audio/project.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "version": "1.0.0-alpha1", - "description": "A Discord.Net extension adding voice support.", - "authors": [ "RogueException" ], - "tags": [ "discord", "discordapp" ], - "projectUrl": "https://github.com/RogueException/Discord.Net", - "licenseUrl": "http://opensource.org/licenses/MIT", - "repository": { - "type": "git", - "url": "git://github.com/RogueException/Discord.Net" - }, - "compile": [ "**/*.cs", "../Discord.Net.Shared/*.cs" ], - "contentFiles": [ "libsodium.dll", "opus.dll" ], - - "compilationOptions": { - "allowUnsafe": true, - "warningsAsErrors": true - }, - - "dependencies": { - "Discord.Net": "1.0.0-alpha1" - }, - "frameworks": { - "net45": { }, - "dotnet5.4": { } - } -} diff --git a/src/Discord.Net.Commands/Command.cs b/src/Discord.Net.Commands/Command.cs deleted file mode 100644 index ccd62798b..000000000 --- a/src/Discord.Net.Commands/Command.cs +++ /dev/null @@ -1,83 +0,0 @@ -using Discord.Commands.Permissions; -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Discord.Commands -{ - //TODO: Make this more friendly and expose it to be extendable - public class Command - { - private string[] _aliases; - internal CommandParameter[] _parameters; - private IPermissionChecker[] _checks; - private Func _runFunc; - internal readonly Dictionary _parametersByName; - - public string Text { get; } - public string Category { get; internal set; } - public bool IsHidden { get; internal set; } - public string Description { get; internal set; } - - public IEnumerable Aliases => _aliases; - public IEnumerable Parameters => _parameters; - public CommandParameter this[string name] => _parametersByName[name]; - - internal Command(string text) - { - Text = text; - IsHidden = false; - _aliases = new string[0]; - _parameters = new CommandParameter[0]; - _parametersByName = new Dictionary(); - } - - - internal void SetAliases(string[] aliases) - { - _aliases = aliases; - } - internal void SetParameters(CommandParameter[] parameters) - { - _parametersByName.Clear(); - for (int i = 0; i < parameters.Length; i++) - { - parameters[i].Id = i; - _parametersByName[parameters[i].Name] = parameters[i]; - } - _parameters = parameters; - } - internal void SetChecks(IPermissionChecker[] checks) - { - _checks = checks; - } - - internal bool CanRun(User user, ITextChannel channel, out string error) - { - for (int i = 0; i < _checks.Length; i++) - { - if (!_checks[i].CanRun(this, user, channel, out error)) - return false; - } - error = null; - return true; - } - - internal void SetRunFunc(Func func) - { - _runFunc = func; - } - internal void SetRunFunc(Action func) - { - _runFunc = TaskHelper.ToAsync(func); - } - internal Task Run(CommandEventArgs args) - { - var task = _runFunc(args); - if (task != null) - return task; - else - return TaskHelper.CompletedTask; - } - } -} diff --git a/src/Discord.Net.Commands/CommandBuilder.cs b/src/Discord.Net.Commands/CommandBuilder.cs deleted file mode 100644 index 6b945841c..000000000 --- a/src/Discord.Net.Commands/CommandBuilder.cs +++ /dev/null @@ -1,163 +0,0 @@ -using Discord.Commands.Permissions; -using System; -using System.Collections.Generic; -using System.Linq; -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; - private readonly Command _command; - private readonly List _params; - private readonly List _checks; - private readonly List _aliases; - private readonly string _prefix; - private bool _allowRequiredParams, _areParamsClosed; - - public CommandService Service => _service; - - internal CommandBuilder(CommandService service, string text, string prefix = "", string category = "", IEnumerable initialChecks = null) - { - _service = service; - _prefix = prefix; - - _command = new Command(AppendPrefix(prefix, text)); - _command.Category = category; - - if (initialChecks != null) - _checks = new List(initialChecks); - else - _checks = new List(); - - _params = new List(); - _aliases = new List(); - - _allowRequiredParams = true; - _areParamsClosed = false; - } - - public CommandBuilder Alias(params string[] aliases) - { - _aliases.AddRange(aliases); - return this; - } - /*public CommandBuilder Category(string category) - { - _command.Category = category; - return this; - }*/ - public CommandBuilder Description(string description) - { - _command.Description = description; - return this; - } - public CommandBuilder Parameter(string name, ParameterType type = ParameterType.Required) - { - if (_areParamsClosed) - throw new Exception($"No parameters may be added after a {nameof(ParameterType.Multiple)} or {nameof(ParameterType.Unparsed)} parameter."); - if (!_allowRequiredParams && type == ParameterType.Required) - throw new Exception($"{nameof(ParameterType.Required)} parameters may not be added after an optional one"); - - _params.Add(new CommandParameter(name, type)); - - if (type == ParameterType.Optional) - _allowRequiredParams = false; - if (type == ParameterType.Multiple || type == ParameterType.Unparsed) - _areParamsClosed = true; - return this; - } - public CommandBuilder Hide() - { - _command.IsHidden = true; - return this; - } - public CommandBuilder AddCheck(IPermissionChecker check) - { - _checks.Add(check); - return this; - } - public CommandBuilder AddCheck(Func checkFunc, string errorMsg = null) - { - _checks.Add(new GenericPermissionChecker(checkFunc, errorMsg)); - return this; - } - - public void Do(Func func) - { - _command.SetRunFunc(func); - Build(); - } - public void Do(Action func) - { - _command.SetRunFunc(func); - Build(); - } - private void Build() - { - _command.SetParameters(_params.ToArray()); - _command.SetChecks(_checks.ToArray()); - _command.SetAliases(_aliases.Select(x => AppendPrefix(_prefix, x)).ToArray()); - _service.AddCommand(_command); - } - - internal static string AppendPrefix(string prefix, string cmd) - { - if (cmd != "") - { - if (prefix != "") - return prefix + ' ' + cmd; - else - return cmd; - } - else - return prefix; - } - } - public class CommandGroupBuilder - { - private readonly CommandService _service; - private readonly string _prefix; - private readonly List _checks; - private string _category; - - public CommandService Service => _service; - - 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 - _checks = new List(); - } - - public CommandGroupBuilder Category(string category) - { - _category = category; - return this; - } - public void AddCheck(IPermissionChecker checker) - { - _checks.Add(checker); - } - public void AddCheck(Func checkFunc, string errorMsg = null) - { - _checks.Add(new GenericPermissionChecker(checkFunc, errorMsg)); - } - - 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) - => new CommandBuilder(_service, cmd, _prefix, _category, _checks); - } -} diff --git a/src/Discord.Net.Commands/CommandErrorEventArgs.cs b/src/Discord.Net.Commands/CommandErrorEventArgs.cs deleted file mode 100644 index 5f47c6d7c..000000000 --- a/src/Discord.Net.Commands/CommandErrorEventArgs.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; - -namespace Discord.Commands -{ - public enum CommandErrorType { Exception, UnknownCommand, BadPermissions, BadArgCount, InvalidInput } - public class CommandErrorEventArgs : CommandEventArgs - { - public CommandErrorType ErrorType { get; } - public Exception Exception { get; } - - public CommandErrorEventArgs(CommandErrorType errorType, CommandEventArgs baseArgs, Exception ex) - : base(baseArgs.Message, baseArgs.Command, baseArgs.Args) - { - Exception = ex; - ErrorType = errorType; - } - } -} diff --git a/src/Discord.Net.Commands/CommandEventArgs.cs b/src/Discord.Net.Commands/CommandEventArgs.cs deleted file mode 100644 index 70793f5e1..000000000 --- a/src/Discord.Net.Commands/CommandEventArgs.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; - -namespace Discord.Commands -{ - public class CommandEventArgs : EventArgs - { - private readonly string[] _args; - - public Message Message { get; } - public Command Command { get; } - - public User User => Message.User; - public ITextChannel Channel => Message.Channel; - - public CommandEventArgs(Message message, Command command, string[] args) - { - Message = message; - Command = command; - _args = args; - } - - public string[] Args => _args; - public string GetArg(int index) => _args[index]; - public string GetArg(string name) => _args[Command[name].Id]; - } -} diff --git a/src/Discord.Net.Commands/CommandExtensions.cs b/src/Discord.Net.Commands/CommandExtensions.cs deleted file mode 100644 index c57cf099f..000000000 --- a/src/Discord.Net.Commands/CommandExtensions.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; - -namespace Discord.Commands -{ - public static class CommandExtensions - { - public static DiscordClient UsingCommands(this DiscordClient client, CommandServiceConfig config = null) - { - client.AddService(new CommandService(config)); - return client; - } - public static DiscordClient UsingCommands(this DiscordClient client, Action configFunc = null) - { - var builder = new CommandServiceConfigBuilder(); - configFunc(builder); - client.AddService(new CommandService(builder)); - return client; - } - } -} diff --git a/src/Discord.Net.Commands/CommandMap.cs b/src/Discord.Net.Commands/CommandMap.cs deleted file mode 100644 index ad280b335..000000000 --- a/src/Discord.Net.Commands/CommandMap.cs +++ /dev/null @@ -1,141 +0,0 @@ -using System.Collections.Generic; - -namespace Discord.Commands -{ - //Represents either a single function, command group, or both - internal class CommandMap - { - private readonly CommandMap _parent; - private readonly string _name, _fullName; - - private readonly List _commands; - private readonly Dictionary _items; - private bool _isVisible, _hasNonAliases, _hasSubGroups; - - public string Name => _name; - public string FullName => _fullName; - public bool IsVisible => _isVisible; - public bool HasNonAliases => _hasNonAliases; - public bool HasSubGroups => _hasSubGroups; - public IEnumerable Commands => _commands; - public IEnumerable SubGroups => _items.Values; - - 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; - } - - public CommandMap GetItem(string text) - { - return GetItem(0, text.Split(' ')); - } - public CommandMap GetItem(int index, string[] parts) - { - if (index != parts.Length) - { - string nextPart = parts[index]; - CommandMap nextGroup; - if (_items.TryGetValue(nextPart.ToLowerInvariant(), out nextGroup)) - return nextGroup.GetItem(index + 1, parts); - else - return null; - } - return this; - } - - public IEnumerable GetCommands() - { - if (_commands.Count > 0) - return _commands; - else if (_parent != null) - return _parent.GetCommands(); - else - return null; - } - public IEnumerable GetCommands(string text) - { - return GetCommands(0, text.Split(' ')); - } - public IEnumerable GetCommands(int index, string[] parts) - { - if (index != parts.Length) - { - string nextPart = parts[index]; - CommandMap nextGroup; - if (_items.TryGetValue(nextPart.ToLowerInvariant(), out nextGroup)) - { - var cmd = nextGroup.GetCommands(index + 1, parts); - if (cmd != null) - return cmd; - } - } - - if (_commands != null) - return _commands; - return null; - } - - public void AddCommand(string text, Command command, bool isAlias) - { - AddCommand(0, text.Split(' '), command, isAlias); - } - private void AddCommand(int index, string[] parts, Command command, bool isAlias) - { - if (!command.IsHidden) - _isVisible = true; - - if (index != parts.Length) - { - CommandMap nextGroup; - string name = parts[index].ToLowerInvariant(); - string fullName = string.Join(" ", parts, 0, index + 1); - if (!_items.TryGetValue(name, out nextGroup)) - { - nextGroup = new CommandMap(this, name, fullName); - _items.Add(name, nextGroup); - _hasSubGroups = true; - } - nextGroup.AddCommand(index + 1, parts, command, isAlias); - } - else - { - _commands.Add(command); - if (!isAlias) - _hasNonAliases = true; - } - } - - public bool CanRun(User user, ITextChannel channel, out string error) - { - error = null; - if (_commands.Count > 0) - { - foreach (var cmd in _commands) - { - if (cmd.CanRun(user, channel, out error)) - return true; - } - } - if (_items.Count > 0) - { - foreach (var item in _items) - { - if (item.Value.CanRun(user, channel, out error)) - return true; - } - } - return false; - } - } -} diff --git a/src/Discord.Net.Commands/CommandParameter.cs b/src/Discord.Net.Commands/CommandParameter.cs deleted file mode 100644 index d7361bef4..000000000 --- a/src/Discord.Net.Commands/CommandParameter.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace Discord.Commands -{ - public enum ParameterType - { - /// Catches a single required parameter. - Required, - /// Catches a single optional parameter. - Optional, - /// Catches a zero or more optional parameters. - Multiple, - /// Catches all remaining text as a single optional parameter. - Unparsed - } - public class CommandParameter - { - public string Name { get; } - public int Id { get; internal set; } - public ParameterType Type { get; } - - internal CommandParameter(string name, ParameterType type) - { - Name = name; - Type = type; - } - } -} diff --git a/src/Discord.Net.Commands/CommandParser.cs b/src/Discord.Net.Commands/CommandParser.cs deleted file mode 100644 index cfdbe6903..000000000 --- a/src/Discord.Net.Commands/CommandParser.cs +++ /dev/null @@ -1,188 +0,0 @@ -using System.Collections.Generic; - -namespace Discord.Commands -{ - internal static class CommandParser - { - private enum ParserPart - { - None, - Parameter, - QuotedParameter, - DoubleQuotedParameter - } - - public static bool ParseCommand(string input, CommandMap map, out IEnumerable commands, out int endPos) - { - int startPosition = 0; - int endPosition = 0; - int inputLength = input.Length; - bool isEscaped = false; - commands = null; - endPos = 0; - - if (input == "") - return false; - - while (endPosition < inputLength) - { - char currentChar = input[endPosition++]; - if (isEscaped) - isEscaped = false; - else if (currentChar == '\\') - isEscaped = true; - - bool isWhitespace = IsWhiteSpace(currentChar); - if ((!isEscaped && isWhitespace) || endPosition >= inputLength) - { - int length = (isWhitespace ? endPosition - 1 : endPosition) - startPosition; - string temp = input.Substring(startPosition, length); - if (temp == "") - startPosition = endPosition; - else - { - var newMap = map.GetItem(temp); - if (newMap != null) - { - map = newMap; - endPos = endPosition; - } - else - break; - startPosition = endPosition; - } - } - } - commands = map.GetCommands(); //Work our way backwards to find a command that matches our input - return commands != null; - } - private static bool IsWhiteSpace(char c) => c == ' ' || c == '\n' || c == '\r' || c == '\t'; - - //TODO: Check support for escaping - public static CommandErrorType? ParseArgs(string input, int startPos, Command command, out string[] args) - { - ParserPart currentPart = ParserPart.None; - int startPosition = startPos; - int endPosition = startPos; - int inputLength = input.Length; - bool isEscaped = false; - - var expectedArgs = command._parameters; - List argList = new List(); - CommandParameter parameter = null; - - args = null; - - if (input == "") - return CommandErrorType.InvalidInput; - - while (endPosition < inputLength) - { - if (startPosition == endPosition && (parameter == null || parameter.Type != ParameterType.Multiple)) //Is first char of a new arg - { - if (argList.Count >= expectedArgs.Length) - return CommandErrorType.BadArgCount; //Too many args - parameter = expectedArgs[argList.Count]; - if (parameter.Type == ParameterType.Unparsed) - { - argList.Add(input.Substring(startPosition)); - break; - } - } - - char currentChar = input[endPosition++]; - if (isEscaped) - isEscaped = false; - else if (currentChar == '\\') - isEscaped = true; - - bool isWhitespace = IsWhiteSpace(currentChar); - if (endPosition == startPosition + 1 && isWhitespace) //Has no text yet, and is another whitespace - { - startPosition = endPosition; - continue; - } - - switch (currentPart) - { - case ParserPart.None: - if ((!isEscaped && currentChar == '\"')) - { - currentPart = ParserPart.DoubleQuotedParameter; - startPosition = endPosition; - } - else if ((!isEscaped && currentChar == '\'')) - { - currentPart = ParserPart.QuotedParameter; - startPosition = endPosition; - } - else if ((!isEscaped && isWhitespace) || endPosition >= inputLength) - { - int length = (isWhitespace ? endPosition - 1 : endPosition) - startPosition; - if (length == 0) - startPosition = endPosition; - else - { - string temp = input.Substring(startPosition, length); - argList.Add(temp); - currentPart = ParserPart.None; - startPosition = endPosition; - } - } - break; - case ParserPart.QuotedParameter: - if ((!isEscaped && currentChar == '\'')) - { - string temp = input.Substring(startPosition, endPosition - startPosition - 1); - argList.Add(temp); - currentPart = ParserPart.None; - startPosition = endPosition; - } - else if (endPosition >= inputLength) - return CommandErrorType.InvalidInput; - break; - case ParserPart.DoubleQuotedParameter: - if ((!isEscaped && currentChar == '\"')) - { - string temp = input.Substring(startPosition, endPosition - startPosition - 1); - argList.Add(temp); - currentPart = ParserPart.None; - startPosition = endPosition; - } - else if (endPosition >= inputLength) - return CommandErrorType.InvalidInput; - break; - } - } - - //Unclosed quotes - if (currentPart == ParserPart.QuotedParameter || - currentPart == ParserPart.DoubleQuotedParameter) - return CommandErrorType.InvalidInput; - - //Too few args - for (int i = argList.Count; i < expectedArgs.Length; i++) - { - var param = expectedArgs[i]; - switch (param.Type) - { - case ParameterType.Required: - return CommandErrorType.BadArgCount; - case ParameterType.Optional: - case ParameterType.Unparsed: - argList.Add(""); - break; - } - } - - /*if (argList.Count > expectedArgs.Length) - { - if (expectedArgs.Length == 0 || expectedArgs[expectedArgs.Length - 1].Type != ParameterType.Multiple) - return CommandErrorType.BadArgCount; - }*/ - - args = argList.ToArray(); - return null; - } - } -} diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs deleted file mode 100644 index ea530d27b..000000000 --- a/src/Discord.Net.Commands/CommandService.cs +++ /dev/null @@ -1,347 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Discord.Commands -{ - public partial class CommandService : IService - { - private readonly List _allCommands; - private readonly Dictionary _categories; - private readonly CommandMap _map; //Command map stores all commands by their input text, used for fast resolving and parsing - - public CommandServiceConfig Config { get; } - public CommandGroupBuilder Root { get; } - public DiscordClient Client { get; private set; } - - //AllCommands store a flattened collection of all commands - public IEnumerable AllCommands => _allCommands; - //Groups store all commands by their module, used for more informative help - internal IEnumerable Categories => _categories.Values; - - public event EventHandler CommandExecuted = delegate { }; - public event EventHandler CommandErrored = delegate { }; - - private void OnCommand(CommandEventArgs args) - => CommandExecuted(this, args); - private void OnCommandError(CommandErrorType errorType, CommandEventArgs args, Exception ex = null) - => CommandErrored(this, new CommandErrorEventArgs(errorType, args, ex)); - - public CommandService() - : this(new CommandServiceConfigBuilder()) - { - } - public CommandService(CommandServiceConfigBuilder builder) - : this(builder.Build()) - { - if (builder.ExecuteHandler != null) - CommandExecuted += builder.ExecuteHandler; - if (builder.ErrorHandler != null) - CommandErrored += builder.ErrorHandler; - } - public CommandService(CommandServiceConfig config) - { - Config = config; - - _allCommands = new List(); - _map = new CommandMap(); - _categories = new Dictionary(); - Root = new CommandGroupBuilder(this); - } - - void IService.Install(DiscordClient client) - { - Client = client; - - if (Config.HelpMode != HelpMode.Disabled) - { - CreateCommand("help") - .Parameter("command", ParameterType.Multiple) - .Hide() - .Description("Returns information about commands.") - .Do(async e => - { - ITextChannel replyChannel = Config.HelpMode == HelpMode.Public ? e.Channel : await e.User.CreatePMChannel().ConfigureAwait(false); - if (e.Args.Length > 0) //Show command help - { - var map = _map.GetItem(string.Join(" ", e.Args)); - if (map != null) - await ShowCommandHelp(map, e.User, e.Channel, replyChannel).ConfigureAwait(false); - else - await replyChannel.SendMessage("Unable to display help: Unknown command.").ConfigureAwait(false); - } - else //Show general help - await ShowGeneralHelp(e.User, e.Channel, replyChannel).ConfigureAwait(false); - }); - } - - client.MessageReceived += async (s, e) => - { - if (_allCommands.Count == 0) return; - if (e.Message.User == null || e.Message.User.Id == Client.CurrentUser.Id) return; - - string msg = e.Message.RawText; - if (msg.Length == 0) return; - - string cmdMsg = null; - - //Check for command char - if (Config.PrefixChar.HasValue) - { - if (msg[0] == Config.PrefixChar.Value) - cmdMsg = msg.Substring(1); - } - - //Check for mention - if (cmdMsg == null && Config.AllowMentionPrefix) - { - string mention = client.CurrentUser.Mention; - if (msg.StartsWith(mention) && msg.Length > mention.Length) - cmdMsg = msg.Substring(mention.Length + 1); - else - { - mention = $"@{client.CurrentUser.Name}"; - if (msg.StartsWith(mention) && msg.Length > mention.Length) - cmdMsg = msg.Substring(mention.Length + 1); - } - } - - //Check using custom activator - if (cmdMsg == null && Config.CustomPrefixHandler != null) - { - int index = Config.CustomPrefixHandler(e.Message); - if (index >= 0) - cmdMsg = msg.Substring(index); - } - - if (cmdMsg == null) return; - - //Parse command - IEnumerable commands; - int argPos; - CommandParser.ParseCommand(cmdMsg, _map, out commands, out argPos); - if (commands == null) - { - CommandEventArgs errorArgs = new CommandEventArgs(e.Message, null, null); - OnCommandError(CommandErrorType.UnknownCommand, errorArgs); - return; - } - else - { - foreach (var command in commands) - { - //Parse arguments - string[] args; - var error = CommandParser.ParseArgs(cmdMsg, argPos, command, out args); - if (error != null) - { - if (error == CommandErrorType.BadArgCount) - continue; - else - { - var errorArgs = new CommandEventArgs(e.Message, command, null); - OnCommandError(error.Value, errorArgs); - return; - } - } - - var eventArgs = new CommandEventArgs(e.Message, command, args); - - // Check permissions - string errorText; - if (!command.CanRun(eventArgs.User, eventArgs.Channel, out errorText)) - { - OnCommandError(CommandErrorType.BadPermissions, eventArgs, errorText != null ? new Exception(errorText) : null); - return; - } - - // Run the command - try - { - OnCommand(eventArgs); - await command.Run(eventArgs).ConfigureAwait(false); - } - catch (Exception ex) - { - OnCommandError(CommandErrorType.Exception, eventArgs, ex); - } - return; - } - var errorArgs2 = new CommandEventArgs(e.Message, null, null); - OnCommandError(CommandErrorType.BadArgCount, errorArgs2); - } - }; - } - - public Task ShowGeneralHelp(User user, ITextChannel channel, ITextChannel replyChannel = null) - { - StringBuilder output = new StringBuilder(); - bool isFirstCategory = true; - foreach (var category in _categories) - { - bool isFirstItem = true; - foreach (var group in category.Value.SubGroups) - { - string error; - if (group.IsVisible && (group.HasSubGroups || group.HasNonAliases) && group.CanRun(user, channel, out error)) - { - if (isFirstItem) - { - isFirstItem = false; - //This is called for the first item in each category. If we never get here, we dont bother writing the header for a category type (since it's empty) - if (isFirstCategory) - { - isFirstCategory = false; - //Called for the first non-empty category - output.AppendLine("These are the commands you can use:"); - } - else - output.AppendLine(); - if (category.Key != "") - { - output.Append(Format.Bold(category.Key)); - output.Append(": "); - } - } - else - output.Append(", "); - output.Append('`'); - output.Append(group.Name); - if (group.HasSubGroups) - output.Append("*"); - output.Append('`'); - } - } - } - - if (output.Length == 0) - output.Append("There are no commands you have permission to run."); - else - output.AppendLine("\n\nRun `help ` for more information."); - - return (replyChannel ?? channel).SendMessage(output.ToString()); - } - - private Task ShowCommandHelp(CommandMap map, User user, ITextChannel channel, ITextChannel replyChannel = null) - { - StringBuilder output = new StringBuilder(); - - IEnumerable cmds = map.Commands; - bool isFirstCmd = true; - string error; - if (cmds.Any()) - { - foreach (var cmd in cmds) - { - if (cmd.CanRun(user, channel, out error)) - { - if (isFirstCmd) - isFirstCmd = false; - else - output.AppendLine(); - ShowCommandHelpInternal(cmd, user, channel, output); - } - } - } - else - { - output.Append('`'); - output.Append(map.FullName); - output.Append("`\n"); - } - - bool isFirstSubCmd = true; - foreach (var subCmd in map.SubGroups.Where(x => x.CanRun(user, channel, out error) && x.IsVisible)) - { - if (isFirstSubCmd) - { - isFirstSubCmd = false; - output.AppendLine("Sub Commands: "); - } - else - output.Append(", "); - output.Append('`'); - output.Append(subCmd.Name); - if (subCmd.SubGroups.Any()) - output.Append("*"); - output.Append('`'); - } - - if (isFirstCmd && isFirstSubCmd) //Had no commands and no subcommands - { - output.Clear(); - output.AppendLine("There are no commands you have permission to run."); - } - - return (replyChannel ?? channel).SendMessage(output.ToString()); - } - public Task ShowCommandHelp(Command command, User user, ITextChannel channel, ITextChannel replyChannel = null) - { - StringBuilder output = new StringBuilder(); - string error; - if (!command.CanRun(user, channel, out error)) - output.AppendLine(error ?? "You do not have permission to access this command."); - else - ShowCommandHelpInternal(command, user, channel, output); - return (replyChannel ?? channel).SendMessage(output.ToString()); - } - private void ShowCommandHelpInternal(Command command, User user, ITextChannel channel, StringBuilder output) - { - output.Append('`'); - output.Append(command.Text); - foreach (var param in command.Parameters) - { - switch (param.Type) - { - case ParameterType.Required: - output.Append($" <{param.Name}>"); - break; - case ParameterType.Optional: - output.Append($" [{param.Name}]"); - break; - case ParameterType.Multiple: - output.Append($" [{param.Name}...]"); - break; - case ParameterType.Unparsed: - output.Append($" [-]"); - break; - } - } - output.AppendLine("`"); - output.AppendLine($"{command.Description ?? "No description."}"); - - if (command.Aliases.Any()) - output.AppendLine($"Aliases: `" + string.Join("`, `", command.Aliases) + '`'); - } - - public void CreateGroup(string cmd, Action config = null) => Root.CreateGroup(cmd, config); - public CommandBuilder CreateCommand(string cmd) => Root.CreateCommand(cmd); - - internal void AddCommand(Command command) - { - _allCommands.Add(command); - - //Get category - CommandMap category; - string categoryName = command.Category ?? ""; - if (!_categories.TryGetValue(categoryName, out category)) - { - category = new CommandMap(); - _categories.Add(categoryName, category); - } - - //Add main command - category.AddCommand(command.Text, command, false); - _map.AddCommand(command.Text, command, false); - - //Add aliases - foreach (var alias in command.Aliases) - { - category.AddCommand(alias, command, true); - _map.AddCommand(alias, command, true); - } - } - } -} diff --git a/src/Discord.Net.Commands/CommandServiceConfig.cs b/src/Discord.Net.Commands/CommandServiceConfig.cs deleted file mode 100644 index f43c838fb..000000000 --- a/src/Discord.Net.Commands/CommandServiceConfig.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; - -namespace Discord.Commands -{ - public class CommandServiceConfigBuilder - { - /// Gets or sets the prefix character used to trigger commands, if ActivationMode has the Char flag set. - public char? PrefixChar { get; set; } = null; - /// Gets or sets whether a message beginning with a mention to the logged-in user should be treated as a command. - public bool AllowMentionPrefix { get; set; } = true; - /// - /// Gets or sets a custom function used to detect messages that should be treated as commands. - /// This function should a positive one indicating the index of where the in the message's RawText the command begins, - /// and a negative value if the message should be ignored. - /// - public Func CustomPrefixHandler { get; set; } = null; - - /// Gets or sets whether a help function should be automatically generated. - public HelpMode HelpMode { get; set; } = HelpMode.Disabled; - - - /// Gets or sets a handler that is called on any successful command execution. - public EventHandler ExecuteHandler { get; set; } - /// Gets or sets a handler that is called on any error during command parsing or execution. - public EventHandler ErrorHandler { get; set; } - - public CommandServiceConfig Build() => new CommandServiceConfig(this); - } - public class CommandServiceConfig - { - public char? PrefixChar { get; } - public bool AllowMentionPrefix { get; } - public Func CustomPrefixHandler { get; } - - /// Gets or sets whether a help function should be automatically generated. - public HelpMode HelpMode { get; set; } = HelpMode.Disabled; - - internal CommandServiceConfig(CommandServiceConfigBuilder builder) - { - PrefixChar = builder.PrefixChar; - AllowMentionPrefix = builder.AllowMentionPrefix; - CustomPrefixHandler = builder.CustomPrefixHandler; - HelpMode = builder.HelpMode; - } - } -} diff --git a/src/Discord.Net.Commands/Discord.Net.Commands.xproj b/src/Discord.Net.Commands/Discord.Net.Commands.xproj deleted file mode 100644 index 6c0d0ca91..000000000 --- a/src/Discord.Net.Commands/Discord.Net.Commands.xproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - 19793545-ef89-48f4-8100-3ebaad0a9141 - Discord.Commands - ..\..\artifacts\obj\$(MSBuildProjectName) - ..\..\artifacts\bin\$(MSBuildProjectName)\ - - - 2.0 - - - True - - - \ No newline at end of file diff --git a/src/Discord.Net.Commands/GenericPermissionChecker.cs b/src/Discord.Net.Commands/GenericPermissionChecker.cs deleted file mode 100644 index 10d665811..000000000 --- a/src/Discord.Net.Commands/GenericPermissionChecker.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; - -namespace Discord.Commands.Permissions -{ - internal class GenericPermissionChecker : IPermissionChecker - { - private readonly Func _checkFunc; - private readonly string _error; - - public GenericPermissionChecker(Func checkFunc, string error = null) - { - _checkFunc = checkFunc; - _error = error; - } - - public bool CanRun(Command command, User user, ITextChannel channel, out string error) - { - error = _error; - return _checkFunc(command, user, channel); - } - } -} diff --git a/src/Discord.Net.Commands/HelpMode.cs b/src/Discord.Net.Commands/HelpMode.cs deleted file mode 100644 index 272403f42..000000000 --- a/src/Discord.Net.Commands/HelpMode.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Discord.Commands -{ - public enum HelpMode - { - /// Disable the automatic help command. - Disabled, - /// Use the automatic help command and respond in the channel the command is used. - Public, - /// Use the automatic help command and respond in a private message. - Private - } -} diff --git a/src/Discord.Net.Commands/IPermissionChecker.cs b/src/Discord.Net.Commands/IPermissionChecker.cs deleted file mode 100644 index 0f317ffef..000000000 --- a/src/Discord.Net.Commands/IPermissionChecker.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Discord.Commands.Permissions -{ - public interface IPermissionChecker - { - bool CanRun(Command command, User user, ITextChannel channel, out string error); - } -} diff --git a/src/Discord.Net.Commands/project.json b/src/Discord.Net.Commands/project.json deleted file mode 100644 index 124a29dfe..000000000 --- a/src/Discord.Net.Commands/project.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "version": "1.0.0-alpha1", - "description": "A Discord.Net extension adding basic command support.", - "authors": [ "RogueException" ], - "tags": [ "discord", "discordapp" ], - "projectUrl": "https://github.com/RogueException/Discord.Net", - "licenseUrl": "http://opensource.org/licenses/MIT", - "repository": { - "type": "git", - "url": "git://github.com/RogueException/Discord.Net" - }, - "compile": [ "**/*.cs", "../Discord.Net.Shared/*.cs" ], - - "compilationOptions": { - "warningsAsErrors": true - }, - - "dependencies": { - "Discord.Net": "1.0.0-alpha1" - }, - "frameworks": { - "net45": { }, - "dotnet5.4": { } - } -} diff --git a/src/Discord.Net.Modules/Discord.Net.Modules.xproj b/src/Discord.Net.Modules/Discord.Net.Modules.xproj deleted file mode 100644 index 77112cd5d..000000000 --- a/src/Discord.Net.Modules/Discord.Net.Modules.xproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - 01584e8a-78da-486f-9ef9-a894e435841b - Discord.Modules - ..\..\artifacts\obj\$(MSBuildProjectName) - ..\..\artifacts\bin\$(MSBuildProjectName)\ - - - 2.0 - - - True - - - \ No newline at end of file diff --git a/src/Discord.Net.Modules/IModule.cs b/src/Discord.Net.Modules/IModule.cs deleted file mode 100644 index 48a594eef..000000000 --- a/src/Discord.Net.Modules/IModule.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Discord.Modules -{ - public interface IModule - { - void Install(ModuleManager manager); - } -} diff --git a/src/Discord.Net.Modules/ModuleChecker.cs b/src/Discord.Net.Modules/ModuleChecker.cs deleted file mode 100644 index 5f9b8e116..000000000 --- a/src/Discord.Net.Modules/ModuleChecker.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Discord.Commands; -using Discord.Commands.Permissions; - -namespace Discord.Modules -{ - public class ModuleChecker : IPermissionChecker - { - private readonly ModuleManager _manager; - private readonly ModuleFilter _filterType; - - internal ModuleChecker(ModuleManager manager) - { - _manager = manager; - _filterType = manager.FilterType; - } - - public bool CanRun(Command command, User user, ITextChannel channel, out string error) - { - if (_filterType == ModuleFilter.None || - _filterType == ModuleFilter.AlwaysAllowPrivate || - (channel.IsPublic && _manager.HasChannel(channel))) - { - error = null; - return true; - } - else - { - error = "This module is currently disabled."; - return false; - } - } - } -} diff --git a/src/Discord.Net.Modules/ModuleExtensions.cs b/src/Discord.Net.Modules/ModuleExtensions.cs deleted file mode 100644 index a96517c06..000000000 --- a/src/Discord.Net.Modules/ModuleExtensions.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace Discord.Modules -{ - public static class ModuleExtensions - { - public static DiscordClient UsingModules(this DiscordClient client) - { - client.AddService(new ModuleService()); - return client; - } - - public static void AddModule(this DiscordClient client, IModule instance, string name = null, ModuleFilter filter = ModuleFilter.None) - { - client.GetService().Add(instance, name, filter); - } - public static void AddModule(this DiscordClient client, string name = null, ModuleFilter filter = ModuleFilter.None) - where T : class, IModule, new() - { - client.GetService().Add(name, filter); - } - public static void AddModule(this DiscordClient client, T instance, string name = null, ModuleFilter filter = ModuleFilter.None) - where T : class, IModule - { - client.GetService().Add(instance, name, filter); - } - public static ModuleManager GetModule(this DiscordClient client) - where T : class, IModule - => client.GetService().Get(); - } -} diff --git a/src/Discord.Net.Modules/ModuleFilter.cs b/src/Discord.Net.Modules/ModuleFilter.cs deleted file mode 100644 index 08fa09a5d..000000000 --- a/src/Discord.Net.Modules/ModuleFilter.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; - -namespace Discord.Modules -{ - [Flags] - public enum ModuleFilter - { - /// Disables the event and command filters. - None = 0x0, - /// Uses the server whitelist to filter events and commands. - ServerWhitelist = 0x1, - /// Uses the channel whitelist to filter events and commands. - ChannelWhitelist = 0x2, - /// Enables this module in all private messages. - AlwaysAllowPrivate = 0x4 - } -} diff --git a/src/Discord.Net.Modules/ModuleManager.cs b/src/Discord.Net.Modules/ModuleManager.cs deleted file mode 100644 index 71b5b0c08..000000000 --- a/src/Discord.Net.Modules/ModuleManager.cs +++ /dev/null @@ -1,278 +0,0 @@ -using Discord.Commands; -using Nito.AsyncEx; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; - -namespace Discord.Modules -{ - public class ModuleManager : ModuleManager - where T : class, IModule - { - public new T Instance => base.Instance as T; - - internal ModuleManager(DiscordClient client, T instance, string name, ModuleFilter filterType) - : base(client, instance, name, filterType) - { - } - } - - public class ModuleManager - { - public event EventHandler JoinedServer = delegate { }; - public event EventHandler LeftServer = 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 RoleCreated = 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 UserUnbanned = 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 MessageReadRemotely = delegate { }; - - private readonly bool _useServerWhitelist, _useChannelWhitelist, _allowAll, _allowPrivate; - private readonly ConcurrentDictionary _enabledServers; - private readonly ConcurrentDictionary _enabledChannels; - private readonly ConcurrentDictionary _indirectServers; - private readonly AsyncLock _lock; - - public DiscordClient Client { get; } - public IModule Instance { get; } - public string Name { get; } - public string Id { get; } - public ModuleFilter FilterType { get; } - - public IEnumerable EnabledServers => _enabledServers.Select(x => x.Value); - public IEnumerable EnabledChannels => _enabledChannels.Select(x => x.Value); - - internal ModuleManager(DiscordClient client, IModule instance, string name, ModuleFilter filterType) - { - Client = client; - Instance = instance; - Name = name; - FilterType = filterType; - - Id = name.ToLowerInvariant(); - _lock = new AsyncLock(); - - _allowAll = filterType == ModuleFilter.None; - _useServerWhitelist = filterType.HasFlag(ModuleFilter.ServerWhitelist); - _useChannelWhitelist = filterType.HasFlag(ModuleFilter.ChannelWhitelist); - _allowPrivate = filterType.HasFlag(ModuleFilter.AlwaysAllowPrivate); - - _enabledServers = new ConcurrentDictionary(); - _enabledChannels = new ConcurrentDictionary(); - _indirectServers = new ConcurrentDictionary(); - - if (_allowAll || _useServerWhitelist) //Server-only events - { - client.ChannelCreated += (s, e) => - { - var server = (e.Channel as PublicChannel)?.Server; - if (HasServer(server)) - ChannelCreated(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.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); }; - client.MessageDeleted += (s, e) => { if (HasChannel(e.Channel)) MessageDeleted(s, e); }; - client.MessageUpdated += (s, e) => { if (HasChannel(e.Channel)) MessageUpdated(s, e); }; - client.MessageAcknowledged += (s, e) => { if (HasChannel(e.Channel)) MessageReadRemotely(s, e); }; - - client.RoleCreated += (s, e) => { if (HasIndirectServer(e.Server)) RoleCreated(s, e); }; - client.RoleUpdated += (s, e) => { if (HasIndirectServer(e.Server)) RoleUpdated(s, e); }; - client.RoleDeleted += (s, e) => { if (HasIndirectServer(e.Server)) RoleDeleted(s, e); }; - - client.JoinedServer += (s, e) => { if (_allowAll) JoinedServer(s, e); }; - client.LeftServer += (s, e) => { if (HasIndirectServer(e.Server)) LeftServer(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); }; - //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.UserUpdated += (s, e) => { if (HasIndirectServer(e.Server)) UserUpdated(s, e); }; - client.UserIsTyping += (s, e) => { if (HasChannel(e.Channel)) UserIsTyping(s, e); }; - client.UserBanned += (s, e) => { if (HasIndirectServer(e.Server)) UserBanned(s, e); }; - client.UserUnbanned += (s, e) => { if (HasIndirectServer(e.Server)) UserUnbanned(s, e); }; - } - - public void CreateCommands(string prefix, Action config) - { - var commandService = Client.GetService(); - commandService.CreateGroup(prefix, x => - { - x.Category(Name); - x.AddCheck(new ModuleChecker(this)); - config(x); - }); - - } - public bool EnableServer(Server server) - { - if (server == null) throw new ArgumentNullException(nameof(server)); - if (!_useServerWhitelist) throw new InvalidOperationException("This module is not configured to use a server whitelist."); - - using (_lock.Lock()) - return EnableServerInternal(server); - } - public void EnableServers(IEnumerable servers) - { - if (servers == null) throw new ArgumentNullException(nameof(servers)); - if (servers.Contains(null)) throw new ArgumentException("Collection cannot contain null.", nameof(servers)); - if (!_useServerWhitelist) throw new InvalidOperationException("This module is not configured to use a server whitelist."); - - using (_lock.Lock()) - { - foreach (var server in servers) - EnableServerInternal(server); - } - } - private bool EnableServerInternal(Server server) => _enabledServers.TryAdd(server.Id, server); - - public bool DisableServer(Server server) - { - if (server == null) throw new ArgumentNullException(nameof(server)); - if (!_useServerWhitelist) return false; - - using (_lock.Lock()) - return _enabledServers.TryRemove(server.Id, out server); - } - public void DisableAllServers() - { - if (!_useServerWhitelist) throw new InvalidOperationException("This module is not configured to use a server whitelist."); - if (!_useServerWhitelist) return; - - using (_lock.Lock()) - _enabledServers.Clear(); - } - - public bool EnableChannel(ITextChannel channel) - { - if (channel == null) throw new ArgumentNullException(nameof(channel)); - if (!_useChannelWhitelist) throw new InvalidOperationException("This module is not configured to use a channel whitelist."); - - using (_lock.Lock()) - return EnableChannelInternal(channel); - } - public void EnableChannels(IEnumerable channels) - { - if (channels == null) throw new ArgumentNullException(nameof(channels)); - if (channels.Contains(null)) throw new ArgumentException("Collection cannot contain null.", nameof(channels)); - if (!_useChannelWhitelist) throw new InvalidOperationException("This module is not configured to use a channel whitelist."); - - using (_lock.Lock()) - { - foreach (var channel in channels) - EnableChannelInternal(channel); - } - } - private bool EnableChannelInternal(ITextChannel channel) - { - if (_enabledChannels.TryAdd(channel.Id, channel)) - { - if (channel.Type != ChannelType.Private) - { - var server = (channel as PublicChannel)?.Server; - int value = 0; - _indirectServers.TryGetValue(server.Id, out value); - value++; - _indirectServers[server.Id] = value; - } - return true; - } - return false; - } - - public bool DisableChannel(IChannel channel) - { - if (channel == null) throw new ArgumentNullException(nameof(channel)); - if (!_useChannelWhitelist) return false; - - IChannel ignored; - if (_enabledChannels.TryRemove(channel.Id, out ignored)) - { - using (_lock.Lock()) - { - if (channel.Type != ChannelType.Private) - { - var server = (channel as PublicChannel)?.Server; - int value = 0; - _indirectServers.TryGetValue(server.Id, out value); - value--; - if (value <= 0) - _indirectServers.TryRemove(server.Id, out value); - else - _indirectServers[server.Id] = value; - } - return true; - } - } - return false; - } - public void DisableAllChannels() - { - if (!_useChannelWhitelist) return; - - using (_lock.Lock()) - { - _enabledChannels.Clear(); - _indirectServers.Clear(); - } - } - - public void DisableAll() - { - if (_useServerWhitelist) - DisableAllServers(); - if (_useChannelWhitelist) - DisableAllChannels(); - } - - internal bool HasServer(Server server) => - _allowAll || - (_useServerWhitelist && _enabledServers.ContainsKey(server.Id)); - internal bool HasIndirectServer(Server server) => - _allowAll || - (_useServerWhitelist && _enabledServers.ContainsKey(server.Id)) || - (_useChannelWhitelist && _indirectServers.ContainsKey(server.Id)); - internal bool HasChannel(IChannel channel) - { - if (_allowAll) return true; - if (channel.Type == ChannelType.Private) return _allowPrivate; - - if (_useChannelWhitelist && _enabledChannels.ContainsKey(channel.Id)) return true; - if (_useServerWhitelist && channel.IsPublic) - { - var server = (channel as PublicChannel).Server; - if (server == null) return false; - if (_enabledServers.ContainsKey(server.Id)) return true; - } - return false; - } - } -} diff --git a/src/Discord.Net.Modules/ModuleService.cs b/src/Discord.Net.Modules/ModuleService.cs deleted file mode 100644 index 1f405a222..000000000 --- a/src/Discord.Net.Modules/ModuleService.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; - -namespace Discord.Modules -{ - public class ModuleService : IService - { - public DiscordClient Client { get; private set; } - - private static readonly MethodInfo addMethod = typeof(ModuleService).GetTypeInfo().GetDeclaredMethods(nameof(Add)) - .Single(x => x.IsGenericMethodDefinition && x.GetParameters().Length == 3); - - public IEnumerable Modules => _modules.Values; - private readonly Dictionary _modules; - - public ModuleService() - { - _modules = new Dictionary(); - } - - void IService.Install(DiscordClient client) - { - Client = client; - } - - public void Add(IModule instance, string name, ModuleFilter filter) - { - Type type = instance.GetType(); - addMethod.MakeGenericMethod(type).Invoke(this, new object[] { instance, name, filter }); - } - public void Add(string name, ModuleFilter filter) - where T : class, IModule, new() - => Add(new T(), name, filter); - public void Add(T instance, string name, ModuleFilter filter) - where T : class, IModule - { - if (instance == null) throw new ArgumentNullException(nameof(instance)); - if (Client == null) - throw new InvalidOperationException("Service needs to be added to a DiscordClient before modules can be installed."); - - Type type = typeof(T); - if (name == null) name = type.Name; - if (_modules.ContainsKey(type)) - throw new InvalidOperationException("This module has already been added."); - - var manager = new ModuleManager(Client, instance, name, filter); - _modules.Add(type, manager); - instance.Install(manager); - } - public ModuleManager Get() - where T : class, IModule - { - ModuleManager manager; - if (_modules.TryGetValue(typeof(T), out manager)) - return manager as ModuleManager; - return null; - } - } -} diff --git a/src/Discord.Net.Modules/project.json b/src/Discord.Net.Modules/project.json deleted file mode 100644 index e8e897cac..000000000 --- a/src/Discord.Net.Modules/project.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "version": "1.0.0-alpha1", - "description": "A Discord.Net extension adding basic plugin support.", - "authors": [ "RogueException" ], - "tags": [ "discord", "discordapp" ], - "projectUrl": "https://github.com/RogueException/Discord.Net", - "licenseUrl": "http://opensource.org/licenses/MIT", - "repository": { - "type": "git", - "url": "git://github.com/RogueException/Discord.Net" - }, - "compile": [ "**/*.cs", "../Discord.Net.Shared/*.cs" ], - - "compilationOptions": { - "warningsAsErrors": true - }, - - "dependencies": { - "Discord.Net": "1.0.0-alpha1", - "Discord.Net.Commands": "1.0.0-alpha1" - }, - "frameworks": { - "net45": { }, - "dotnet5.4": { } - } -} diff --git a/src/Discord.Net.Shared/Discord.Net.Shared.projitems b/src/Discord.Net.Shared/Discord.Net.Shared.projitems deleted file mode 100644 index 38e4eafc8..000000000 --- a/src/Discord.Net.Shared/Discord.Net.Shared.projitems +++ /dev/null @@ -1,16 +0,0 @@ - - - - $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - true - 2875deb5-f248-4105-8ea2-5141e3de8025 - - - Discord.Net.Shared - - - - - - - \ No newline at end of file diff --git a/src/Discord.Net.Shared/Discord.Net.Shared.shproj b/src/Discord.Net.Shared/Discord.Net.Shared.shproj deleted file mode 100644 index 5d4d53951..000000000 --- a/src/Discord.Net.Shared/Discord.Net.Shared.shproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - 2875deb5-f248-4105-8ea2-5141e3de8025 - 14.0 - - - - - - - - diff --git a/src/Discord.Net.Shared/EpochTime.cs b/src/Discord.Net.Shared/EpochTime.cs deleted file mode 100644 index b4dd03fe9..000000000 --- a/src/Discord.Net.Shared/EpochTime.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace Discord -{ - internal class EpochTime - { - private const long epoch = 621355968000000000L; //1/1/1970 in Ticks - - public static long GetMilliseconds() => (DateTime.UtcNow.Ticks - epoch) / TimeSpan.TicksPerMillisecond; - } -} diff --git a/src/Discord.Net.Shared/TaskExtensions.cs b/src/Discord.Net.Shared/TaskExtensions.cs deleted file mode 100644 index 520114f02..000000000 --- a/src/Discord.Net.Shared/TaskExtensions.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Discord -{ - internal static class TaskExtensions - { - public static async Task Timeout(this Task task, int milliseconds) - { - Task timeoutTask = Task.Delay(milliseconds); - Task finishedTask = await Task.WhenAny(task, timeoutTask).ConfigureAwait(false); - if (finishedTask == timeoutTask) - throw new TimeoutException(); - else - await task.ConfigureAwait(false); - } - public static async Task Timeout(this Task task, int milliseconds) - { - Task timeoutTask = Task.Delay(milliseconds); - Task finishedTask = await Task.WhenAny(task, timeoutTask).ConfigureAwait(false); - if (finishedTask == timeoutTask) - throw new TimeoutException(); - else - return await task.ConfigureAwait(false); - } - public static async Task Timeout(this Task task, int milliseconds, CancellationTokenSource timeoutToken) - { - try - { - timeoutToken.CancelAfter(milliseconds); - await task.ConfigureAwait(false); - } - catch (OperationCanceledException) - { - if (timeoutToken.IsCancellationRequested) - throw new TimeoutException(); - throw; - } - } - public static async Task Timeout(this Task task, int milliseconds, CancellationTokenSource timeoutToken) - { - try - { - timeoutToken.CancelAfter(milliseconds); - return await task.ConfigureAwait(false); - } - catch (OperationCanceledException) - { - if (timeoutToken.IsCancellationRequested) - throw new TimeoutException(); - throw; - } - } - - public static async Task Wait(this CancellationTokenSource tokenSource) - { - var token = tokenSource.Token; - try { await Task.Delay(-1, token).ConfigureAwait(false); } - catch (OperationCanceledException) { } //Expected - } - public static async Task Wait(this CancellationToken token) - { - try { await Task.Delay(-1, token).ConfigureAwait(false); } - catch (OperationCanceledException) { } //Expected - } - } -} diff --git a/src/Discord.Net.Shared/TaskHelper.cs b/src/Discord.Net.Shared/TaskHelper.cs deleted file mode 100644 index 83408b8e9..000000000 --- a/src/Discord.Net.Shared/TaskHelper.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace Discord -{ - internal static class TaskHelper - { -#if DOTNET54 - public static Task CompletedTask => Task.CompletedTask; -#else - public static Task CompletedTask => Task.Delay(0); -#endif - - public static Func ToAsync(Action action) - { - return () => - { - action(); return CompletedTask; - }; - } - public static Func ToAsync(Action action) - { - return x => - { - action(x); return CompletedTask; - }; - } - } -} diff --git a/src/Discord.Net/API/Client/Common/Channel.cs b/src/Discord.Net/API/Client/Common/Channel.cs deleted file mode 100644 index 37eac8e48..000000000 --- a/src/Discord.Net/API/Client/Common/Channel.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Discord.API.Converters; -using Newtonsoft.Json; - -namespace Discord.API.Client -{ - public class Channel : ChannelReference - { - public class PermissionOverwrite - { - [JsonProperty("type")] - public string Type { get; set; } - [JsonProperty("id"), JsonConverter(typeof(LongStringConverter))] - public ulong Id { get; set; } - [JsonProperty("deny")] - public uint Deny { get; set; } - [JsonProperty("allow")] - public uint Allow { get; set; } - } - - [JsonProperty("last_message_id"), JsonConverter(typeof(NullableLongStringConverter))] - public ulong? LastMessageId { get; set; } - [JsonProperty("is_private")] - public bool? IsPrivate { get; set; } - [JsonProperty("position")] - public int? Position { get; set; } - [JsonProperty("topic")] - public string Topic { get; set; } - [JsonProperty("permission_overwrites")] - public PermissionOverwrite[] PermissionOverwrites { get; set; } - [JsonProperty("recipient")] - public UserReference Recipient { get; set; } - [JsonProperty("bitrate")] - public int Bitrate { get; set; } - } -} diff --git a/src/Discord.Net/API/Client/Common/ChannelReference.cs b/src/Discord.Net/API/Client/Common/ChannelReference.cs deleted file mode 100644 index a243e0f49..000000000 --- a/src/Discord.Net/API/Client/Common/ChannelReference.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Discord.API.Converters; -using Newtonsoft.Json; - -namespace Discord.API.Client -{ - public class ChannelReference - { - [JsonProperty("id"), JsonConverter(typeof(LongStringConverter))] - public ulong Id { get; set; } - [JsonProperty("guild_id"), JsonConverter(typeof(NullableLongStringConverter))] - public ulong? GuildId { get; set; } - [JsonProperty("name")] - public string Name { get; set; } - [JsonProperty("type")] - public string Type { get; set; } - } -} diff --git a/src/Discord.Net/API/Client/Common/ExtendedMember.cs b/src/Discord.Net/API/Client/Common/ExtendedMember.cs deleted file mode 100644 index 890ec9de5..000000000 --- a/src/Discord.Net/API/Client/Common/ExtendedMember.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Client -{ - public class ExtendedMember : Member - { - [JsonProperty("mute")] - public bool? IsServerMuted { get; set; } - [JsonProperty("deaf")] - public bool? IsServerDeafened { get; set; } - } -} diff --git a/src/Discord.Net/API/Client/Common/Guild.cs b/src/Discord.Net/API/Client/Common/Guild.cs deleted file mode 100644 index 1ee4a0b51..000000000 --- a/src/Discord.Net/API/Client/Common/Guild.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Discord.API.Converters; -using Newtonsoft.Json; -using System; - -namespace Discord.API.Client -{ - public class Guild : GuildReference - { - public class EmojiData - { - [JsonProperty("id")] - public string Id { get; set; } - [JsonProperty("name")] - public string Name { get; set; } - [JsonProperty("roles"), JsonConverter(typeof(LongStringArrayConverter))] - public ulong[] RoleIds { get; set; } - [JsonProperty("require_colons")] - public bool RequireColons { get; set; } - [JsonProperty("managed")] - public bool IsManaged { get; set; } - } - - [JsonProperty("afk_channel_id"), JsonConverter(typeof(NullableLongStringConverter))] - public ulong? AFKChannelId { get; set; } - [JsonProperty("afk_timeout")] - public int? AFKTimeout { get; set; } - [JsonProperty("embed_channel_id"), JsonConverter(typeof(NullableLongStringConverter))] - public ulong? EmbedChannelId { get; set; } - [JsonProperty("embed_enabled")] - public bool EmbedEnabled { get; set; } - [JsonProperty("icon")] - public string Icon { get; set; } - [JsonProperty("joined_at")] - public DateTime? JoinedAt { get; set; } - [JsonProperty("owner_id"), JsonConverter(typeof(NullableLongStringConverter))] - public ulong? OwnerId { get; set; } - [JsonProperty("region")] - public string Region { get; set; } - [JsonProperty("roles")] - public Role[] Roles { get; set; } - [JsonProperty("features")] - public string[] Features { get; set; } - [JsonProperty("emojis")] - public EmojiData[] Emojis { get; set; } - [JsonProperty("splash")] - public string Splash { get; set; } - [JsonProperty("verification_level")] - public int VerificationLevel { get; set; } - } -} diff --git a/src/Discord.Net/API/Client/Common/GuildReference.cs b/src/Discord.Net/API/Client/Common/GuildReference.cs deleted file mode 100644 index 5b87c3cc2..000000000 --- a/src/Discord.Net/API/Client/Common/GuildReference.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Discord.API.Converters; -using Newtonsoft.Json; - -namespace Discord.API.Client -{ - public class GuildReference - { - [JsonProperty("id"), JsonConverter(typeof(LongStringConverter))] - public ulong Id { get; set; } - [JsonProperty("name")] - public string Name { get; set; } - } -} diff --git a/src/Discord.Net/API/Client/Common/Invite.cs b/src/Discord.Net/API/Client/Common/Invite.cs deleted file mode 100644 index 571f551ee..000000000 --- a/src/Discord.Net/API/Client/Common/Invite.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Newtonsoft.Json; -using System; - -namespace Discord.API.Client -{ - public class Invite : InviteReference - { - [JsonProperty("max_age")] - public int? MaxAge { get; set; } - [JsonProperty("max_uses")] - public int? MaxUses { get; set; } - [JsonProperty("revoked")] - public bool? IsRevoked { get; set; } - [JsonProperty("temporary")] - public bool? IsTemporary { get; set; } - [JsonProperty("uses")] - public int? Uses { get; set; } - [JsonProperty("created_at")] - public DateTime? CreatedAt { get; set; } - [JsonProperty("inviter")] - public UserReference Inviter { get; set; } - } -} diff --git a/src/Discord.Net/API/Client/Common/InviteReference.cs b/src/Discord.Net/API/Client/Common/InviteReference.cs deleted file mode 100644 index 194165173..000000000 --- a/src/Discord.Net/API/Client/Common/InviteReference.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Client -{ - public class InviteReference - { - public class GuildData : GuildReference - { - [JsonProperty("splash_hash")] - public string Splash { get; set; } - } - - [JsonProperty("guild")] - public GuildData Guild { get; set; } - [JsonProperty("channel")] - public ChannelReference Channel { get; set; } - [JsonProperty("code")] - public string Code { get; set; } - [JsonProperty("xkcdpass")] - public string XkcdPass { get; set; } - } -} diff --git a/src/Discord.Net/API/Client/Common/Member.cs b/src/Discord.Net/API/Client/Common/Member.cs deleted file mode 100644 index 1af23c207..000000000 --- a/src/Discord.Net/API/Client/Common/Member.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Discord.API.Converters; -using Newtonsoft.Json; -using System; - -namespace Discord.API.Client -{ - public class Member : MemberReference - { - [JsonProperty("joined_at")] - public DateTime? JoinedAt { get; set; } - [JsonProperty("roles"), JsonConverter(typeof(LongStringArrayConverter))] - public ulong[] Roles { get; set; } - } -} diff --git a/src/Discord.Net/API/Client/Common/MemberPresence.cs b/src/Discord.Net/API/Client/Common/MemberPresence.cs deleted file mode 100644 index 589ad46c1..000000000 --- a/src/Discord.Net/API/Client/Common/MemberPresence.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Discord.API.Converters; -using Newtonsoft.Json; - -namespace Discord.API.Client -{ - public class MemberPresence : MemberReference - { - public class GameInfo - { - [JsonProperty("name")] - public string Name { get; set; } - } - [JsonProperty("game")] - public GameInfo Game { get; set; } - [JsonProperty("status")] - public string Status { get; set; } - [JsonProperty("roles"), JsonConverter(typeof(LongStringArrayConverter))] - public ulong[] Roles { get; set; } //TODO: Might be temporary - } -} diff --git a/src/Discord.Net/API/Client/Common/MemberReference.cs b/src/Discord.Net/API/Client/Common/MemberReference.cs deleted file mode 100644 index ba6f37762..000000000 --- a/src/Discord.Net/API/Client/Common/MemberReference.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Discord.API.Converters; -using Newtonsoft.Json; - -namespace Discord.API.Client -{ - public class MemberReference - { - [JsonProperty("guild_id"), JsonConverter(typeof(NullableLongStringConverter))] - public ulong? GuildId { get; set; } - [JsonProperty("user")] - public UserReference User { get; set; } - } -} diff --git a/src/Discord.Net/API/Client/Common/Message.cs b/src/Discord.Net/API/Client/Common/Message.cs deleted file mode 100644 index 7e9271dc9..000000000 --- a/src/Discord.Net/API/Client/Common/Message.cs +++ /dev/null @@ -1,96 +0,0 @@ -using Newtonsoft.Json; -using System; - -namespace Discord.API.Client -{ - public class Message : MessageReference - { - public class Attachment - { - [JsonProperty("id")] - public string Id { get; set; } - [JsonProperty("url")] - public string Url { get; set; } - [JsonProperty("proxy_url")] - public string ProxyUrl { get; set; } - [JsonProperty("size")] - public int Size { get; set; } - [JsonProperty("filename")] - public string Filename { get; set; } - [JsonProperty("width")] - public int? Width { get; set; } - [JsonProperty("height")] - public int? Height { get; set; } - } - - public class Embed - { - public class Reference - { - [JsonProperty("url")] - public string Url { get; set; } - [JsonProperty("name")] - public string Name { get; set; } - } - - 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; } - [JsonProperty("height")] - public int? Height { get; set; } - } - public class VideoInfo - { - [JsonProperty("url")] - public string Url { get; set; } - [JsonProperty("width")] - public int? Width { get; set; } - [JsonProperty("height")] - public int? Height { get; set; } - } - - [JsonProperty("url")] - public string Url { get; set; } - [JsonProperty("type")] - public string Type { get; set; } - [JsonProperty("title")] - public string Title { get; set; } - [JsonProperty("description")] - public string Description { get; set; } - [JsonProperty("author")] - public Reference Author { get; set; } - [JsonProperty("provider")] - public Reference Provider { get; set; } - [JsonProperty("thumbnail")] - public ThumbnailInfo Thumbnail { get; set; } - [JsonProperty("video")] - public VideoInfo Video { get; set; } - } - - [JsonProperty("tts")] - public bool? IsTextToSpeech { get; set; } - [JsonProperty("mention_everyone")] - public bool? IsMentioningEveryone { get; set; } - [JsonProperty("timestamp")] - public DateTime? Timestamp { get; set; } - [JsonProperty("edited_timestamp")] - public DateTime? EditedTimestamp { get; set; } - [JsonProperty("mentions")] - public UserReference[] Mentions { get; set; } - [JsonProperty("embeds")] - public Embed[] Embeds { get; set; } //TODO: Parse this - [JsonProperty("attachments")] - public Attachment[] Attachments { get; set; } - [JsonProperty("content")] - public string Content { get; set; } - [JsonProperty("author")] - public UserReference Author { get; set; } - [JsonProperty("nonce")] - public string Nonce { get; set; } - } -} diff --git a/src/Discord.Net/API/Client/Common/MessageReference.cs b/src/Discord.Net/API/Client/Common/MessageReference.cs deleted file mode 100644 index c2afa6cd5..000000000 --- a/src/Discord.Net/API/Client/Common/MessageReference.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Discord.API.Converters; -using Newtonsoft.Json; - -namespace Discord.API.Client -{ - public class MessageReference - { - [JsonProperty("id"), JsonConverter(typeof(LongStringConverter))] - public ulong Id { get; set; } - [JsonProperty("message_id"), JsonConverter(typeof(LongStringConverter))] //Only used in MESSAGE_ACK - public ulong MessageId { get { return Id; } set { Id = value; } } - [JsonProperty("channel_id"), JsonConverter(typeof(LongStringConverter))] - public ulong ChannelId { get; set; } - } -} diff --git a/src/Discord.Net/API/Client/Common/RoleReference.cs b/src/Discord.Net/API/Client/Common/RoleReference.cs deleted file mode 100644 index ce7d25fcc..000000000 --- a/src/Discord.Net/API/Client/Common/RoleReference.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Discord.API.Converters; -using Newtonsoft.Json; - -namespace Discord.API.Client -{ - public class RoleReference - { - [JsonProperty("guild_id"), JsonConverter(typeof(LongStringConverter))] - public ulong GuildId { get; set; } - [JsonProperty("role_id"), JsonConverter(typeof(LongStringConverter))] - public ulong RoleId { get; set; } - } -} diff --git a/src/Discord.Net/API/Client/Common/User.cs b/src/Discord.Net/API/Client/Common/User.cs deleted file mode 100644 index 86c6f22f6..000000000 --- a/src/Discord.Net/API/Client/Common/User.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Client -{ - public class User : UserReference - { - [JsonProperty("email")] - public string Email { get; set; } - [JsonProperty("verified")] - public bool? IsVerified { get; set; } - } -} diff --git a/src/Discord.Net/API/Client/Common/UserReference.cs b/src/Discord.Net/API/Client/Common/UserReference.cs deleted file mode 100644 index f2223df87..000000000 --- a/src/Discord.Net/API/Client/Common/UserReference.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Discord.API.Converters; -using Newtonsoft.Json; - -namespace Discord.API.Client -{ - public class UserReference - { - [JsonProperty("username")] - public string Username { get; set; } - [JsonProperty("id"), JsonConverter(typeof(LongStringConverter))] - public ulong Id { get; set; } - [JsonProperty("discriminator")] - public ushort? Discriminator { get; set; } - [JsonProperty("avatar")] - public string Avatar { get; set; } - } -} diff --git a/src/Discord.Net/API/Client/GatewaySocket/Commands/Heartbeat.cs b/src/Discord.Net/API/Client/GatewaySocket/Commands/Heartbeat.cs deleted file mode 100644 index 9f3f9cefb..000000000 --- a/src/Discord.Net/API/Client/GatewaySocket/Commands/Heartbeat.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Client.GatewaySocket -{ - [JsonObject(MemberSerialization.OptIn)] - public class HeartbeatCommand : IWebSocketMessage - { - int IWebSocketMessage.OpCode => (int)OpCodes.Heartbeat; - object IWebSocketMessage.Payload => EpochTime.GetMilliseconds(); - bool IWebSocketMessage.IsPrivate => false; - } -} diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildEmojisUpdate.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/GuildEmojisUpdate.cs deleted file mode 100644 index 06255bdcf..000000000 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildEmojisUpdate.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace Discord.API.Client.GatewaySocket.Events -{ - //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 deleted file mode 100644 index 0767b2f8f..000000000 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildIntegrationsUpdate.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace Discord.API.Client.GatewaySocket -{ - //public class GuildIntegrationsUpdateEvent { } -} diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildMemberRemove.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/GuildMemberRemove.cs deleted file mode 100644 index 311186b11..000000000 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildMemberRemove.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace Discord.API.Client.GatewaySocket -{ - 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 deleted file mode 100644 index 9b56a95b0..000000000 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildMemberUpdate.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace Discord.API.Client.GatewaySocket -{ - public class GuildMemberUpdateEvent : Member { } -} diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildRoleCreate.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/GuildRoleCreate.cs deleted file mode 100644 index 3d8e2f459..000000000 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildRoleCreate.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Discord.API.Converters; -using Newtonsoft.Json; - -namespace Discord.API.Client.GatewaySocket -{ - public class GuildRoleCreateEvent - { - [JsonProperty("guild_id"), JsonConverter(typeof(LongStringConverter))] - public ulong GuildId { get; set; } - [JsonProperty("role")] - public Role Data { get; set; } - } -} diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildRoleUpdate.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/GuildRoleUpdate.cs deleted file mode 100644 index e26b65c4d..000000000 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildRoleUpdate.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Discord.API.Converters; -using Newtonsoft.Json; - -namespace Discord.API.Client.GatewaySocket -{ - public class GuildRoleUpdateEvent - { - [JsonProperty("guild_id"), JsonConverter(typeof(LongStringConverter))] - public ulong GuildId { get; set; } - [JsonProperty("role")] - public Role Data { get; set; } - } -} diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/TypingStart.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/TypingStart.cs deleted file mode 100644 index 484cec1bc..000000000 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/TypingStart.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Discord.API.Converters; -using Newtonsoft.Json; - -namespace Discord.API.Client.GatewaySocket -{ - public class TypingStartEvent - { - [JsonProperty("user_id"), JsonConverter(typeof(LongStringConverter))] - public ulong UserId { get; set; } - [JsonProperty("channel_id"), JsonConverter(typeof(LongStringConverter))] - public ulong ChannelId { get; set; } - [JsonProperty("timestamp")] - public int Timestamp { get; set; } - } -} diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/UserSettingsUpdate.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/UserSettingsUpdate.cs deleted file mode 100644 index aad938157..000000000 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/UserSettingsUpdate.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace Discord.API.Client.GatewaySocket -{ - //public class UserSettingsUpdateEvent { } -} diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/VoiceServerUpdate.cs b/src/Discord.Net/API/Client/GatewaySocket/Events/VoiceServerUpdate.cs deleted file mode 100644 index d305642a1..000000000 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/VoiceServerUpdate.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Discord.API.Converters; -using Newtonsoft.Json; - -namespace Discord.API.Client.GatewaySocket -{ - public class VoiceServerUpdateEvent - { - [JsonProperty("guild_id"), JsonConverter(typeof(LongStringConverter))] - public ulong GuildId { get; set; } - [JsonProperty("endpoint")] - public string Endpoint { get; set; } - [JsonProperty("token")] - public string Token { get; set; } - } -} diff --git a/src/Discord.Net/API/Client/ISerializable.cs b/src/Discord.Net/API/Client/ISerializable.cs deleted file mode 100644 index d23dc3c6c..000000000 --- a/src/Discord.Net/API/Client/ISerializable.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.IO; - -namespace Discord.API.Client -{ - public interface ISerializable - { - void Write(BinaryWriter writer); - } -} diff --git a/src/Discord.Net/API/Client/Rest/AcceptInvite.cs b/src/Discord.Net/API/Client/Rest/AcceptInvite.cs deleted file mode 100644 index 2940c98ac..000000000 --- a/src/Discord.Net/API/Client/Rest/AcceptInvite.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Client.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class AcceptInviteRequest : IRestRequest - { - string IRestRequest.Method => "POST"; - string IRestRequest.Endpoint => $"invite/{InviteId}"; - object IRestRequest.Payload => null; - - public string InviteId { get; set; } - - public AcceptInviteRequest(string inviteId) - { - InviteId = inviteId; - } - } -} diff --git a/src/Discord.Net/API/Client/Rest/AddGuildBan.cs b/src/Discord.Net/API/Client/Rest/AddGuildBan.cs deleted file mode 100644 index 3e0b165f5..000000000 --- a/src/Discord.Net/API/Client/Rest/AddGuildBan.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Client.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class AddGuildBanRequest : IRestRequest - { - string IRestRequest.Method => "PUT"; - string IRestRequest.Endpoint => $"guilds/{GuildId}/bans/{UserId}?delete-message-days={PruneDays}"; - object IRestRequest.Payload => null; - - public ulong GuildId { get; set; } - public ulong UserId { get; set; } - - public int PruneDays { get; set; } = 0; - - public AddGuildBanRequest(ulong guildId, ulong userId) - { - GuildId = guildId; - UserId = userId; - } - } -} diff --git a/src/Discord.Net/API/Client/Rest/DeleteRole.cs b/src/Discord.Net/API/Client/Rest/DeleteRole.cs deleted file mode 100644 index 56faf3d33..000000000 --- a/src/Discord.Net/API/Client/Rest/DeleteRole.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Client.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class DeleteRoleRequest : IRestRequest - { - string IRestRequest.Method => "DELETE"; - string IRestRequest.Endpoint => $"guilds/{GuildId}/roles/{RoleId}"; - object IRestRequest.Payload => null; - - public ulong GuildId { get; set; } - public ulong RoleId { get; set; } - - public DeleteRoleRequest(ulong guildId, ulong roleId) - { - GuildId = guildId; - RoleId = roleId; - } - } -} diff --git a/src/Discord.Net/API/Client/Rest/Gateway.cs b/src/Discord.Net/API/Client/Rest/Gateway.cs deleted file mode 100644 index 02dd71008..000000000 --- a/src/Discord.Net/API/Client/Rest/Gateway.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Client.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class GatewayRequest : IRestRequest - { - string IRestRequest.Method => "GET"; - string IRestRequest.Endpoint => $"gateway"; - object IRestRequest.Payload => null; - } - - 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 deleted file mode 100644 index 714cdbaf8..000000000 --- a/src/Discord.Net/API/Client/Rest/GetBans.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Client.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class GetBansRequest : IRestRequest - { - string IRestRequest.Method => "GET"; - string IRestRequest.Endpoint => $"guilds/{GuildId}/bans"; - object IRestRequest.Payload => null; - - public ulong GuildId { get; set; } - - public GetBansRequest(ulong guildId) - { - GuildId = guildId; - } - } -} diff --git a/src/Discord.Net/API/Client/Rest/GetInvite.cs b/src/Discord.Net/API/Client/Rest/GetInvite.cs deleted file mode 100644 index 2531ac26a..000000000 --- a/src/Discord.Net/API/Client/Rest/GetInvite.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Client.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class GetInviteRequest : IRestRequest - { - string IRestRequest.Method => "GET"; - string IRestRequest.Endpoint => $"invite/{InviteCode}"; - object IRestRequest.Payload => null; - - public string InviteCode { get; set; } - - public GetInviteRequest(string inviteCode) - { - InviteCode = inviteCode; - } - } -} diff --git a/src/Discord.Net/API/Client/Rest/GetInvites.cs b/src/Discord.Net/API/Client/Rest/GetInvites.cs deleted file mode 100644 index 2b4f2f5fe..000000000 --- a/src/Discord.Net/API/Client/Rest/GetInvites.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Client.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class GetInvitesRequest : IRestRequest - { - string IRestRequest.Method => "GET"; - string IRestRequest.Endpoint => $"guilds/{GuildId}/invites"; - object IRestRequest.Payload => null; - - public ulong GuildId { get; set; } - - public GetInvitesRequest(ulong guildId) - { - GuildId = guildId; - } - } -} diff --git a/src/Discord.Net/API/Client/Rest/GetMessages.cs b/src/Discord.Net/API/Client/Rest/GetMessages.cs deleted file mode 100644 index 1beadb9a9..000000000 --- a/src/Discord.Net/API/Client/Rest/GetMessages.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Newtonsoft.Json; -using System.Text; - -namespace Discord.API.Client.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class GetMessagesRequest : IRestRequest - { - string IRestRequest.Method => "GET"; - string IRestRequest.Endpoint - { - get - { - StringBuilder query = new StringBuilder(); - this.AddQueryParam(query, "limit", Limit.ToString()); - if (RelativeDir != null) - this.AddQueryParam(query, RelativeDir, RelativeId.ToString()); - return $"channels/{ChannelId}/messages{query}"; - } - } - object IRestRequest.Payload => null; - - public ulong ChannelId { get; set; } - - public int Limit { get; set; } = 100; - public string RelativeDir { get; set; } = null; - public ulong RelativeId { get; set; } = 0; - - public GetMessagesRequest(ulong channelId) - { - ChannelId = channelId; - } - } -} diff --git a/src/Discord.Net/API/Client/Rest/GetWidget.cs b/src/Discord.Net/API/Client/Rest/GetWidget.cs deleted file mode 100644 index 0437a8b6b..000000000 --- a/src/Discord.Net/API/Client/Rest/GetWidget.cs +++ /dev/null @@ -1,60 +0,0 @@ -using Discord.API.Converters; -using Newtonsoft.Json; - -namespace Discord.API.Client.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class GetWidgetRequest : IRestRequest - { - string IRestRequest.Method => "GET"; - string IRestRequest.Endpoint => $"servers/{GuildId}/widget.json"; - object IRestRequest.Payload => null; - - public ulong GuildId { get; set; } - - public GetWidgetRequest(ulong guildId) - { - GuildId = guildId; - } - } - - public class GetWidgetResponse - { - public class Channel - { - [JsonProperty("id"), JsonConverter(typeof(LongStringConverter))] - public ulong Id { get; set; } - [JsonProperty("name")] - public string Name { get; set; } - [JsonProperty("position")] - public int Position { get; set; } - } - public class User : UserReference - { - [JsonProperty("avatar_url")] - public string AvatarUrl { get; set; } - [JsonProperty("status")] - public string Status { get; set; } - [JsonProperty("game")] - public UserGame Game { get; set; } - } - public class UserGame - { - [JsonProperty("id")] - public int Id { get; set; } - [JsonProperty("name")] - public string Name { get; set; } - } - - [JsonProperty("id"), JsonConverter(typeof(LongStringConverter))] - public ulong Id { get; set; } - [JsonProperty("channels")] - public Channel[] Channels { get; set; } - [JsonProperty("members")] - public MemberReference[] Members { get; set; } - [JsonProperty("instant_invite")] - public string InstantInviteUrl { get; set; } - [JsonProperty("name")] - public string Name { get; set; } - } -} diff --git a/src/Discord.Net/API/Client/Rest/KickMember.cs b/src/Discord.Net/API/Client/Rest/KickMember.cs deleted file mode 100644 index 4808f8543..000000000 --- a/src/Discord.Net/API/Client/Rest/KickMember.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Client.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class KickMemberRequest : IRestRequest - { - string IRestRequest.Method => "DELETE"; - string IRestRequest.Endpoint => $"guilds/{GuildId}/members/{UserId}"; - object IRestRequest.Payload => null; - - public ulong GuildId { get; set; } - public ulong UserId { get; set; } - - public KickMemberRequest(ulong guildId, ulong userId) - { - GuildId = guildId; - UserId = userId; - } - } -} diff --git a/src/Discord.Net/API/Client/Rest/Login.cs b/src/Discord.Net/API/Client/Rest/Login.cs deleted file mode 100644 index f9c89c717..000000000 --- a/src/Discord.Net/API/Client/Rest/Login.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Client.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class LoginRequest : IRestRequest - { - string IRestRequest.Method => Email != null ? "POST" : "GET"; - string IRestRequest.Endpoint => $"auth/login"; - object IRestRequest.Payload => this; - - [JsonProperty("email", NullValueHandling = NullValueHandling.Ignore)] - public string Email { get; set; } - [JsonProperty("password", NullValueHandling = NullValueHandling.Ignore)] - public string Password { get; set; } - } - - 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 deleted file mode 100644 index 9f4443c51..000000000 --- a/src/Discord.Net/API/Client/Rest/Logout.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Client.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class LogoutRequest : IRestRequest - { - string IRestRequest.Method => "POST"; - string IRestRequest.Endpoint => $"auth/logout"; - object IRestRequest.Payload => null; - } -} diff --git a/src/Discord.Net/API/Client/Rest/PruneMembers.cs b/src/Discord.Net/API/Client/Rest/PruneMembers.cs deleted file mode 100644 index e80498bb1..000000000 --- a/src/Discord.Net/API/Client/Rest/PruneMembers.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Client.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class PruneMembersRequest : IRestRequest - { - string IRestRequest.Method => IsSimulation ? "GET" : "POST"; - string IRestRequest.Endpoint => $"guilds/{GuildId}/prune?days={Days}"; - object IRestRequest.Payload => null; - - public ulong GuildId { get; set; } - - public int Days { get; set; } = 30; - public bool IsSimulation { get; set; } = false; - - public PruneMembersRequest(ulong guildId) - { - GuildId = guildId; - } - } - - 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 deleted file mode 100644 index b453cba49..000000000 --- a/src/Discord.Net/API/Client/Rest/RemoveChannelPermission.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Client.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class RemoveChannelPermissionsRequest : IRestRequest - { - string IRestRequest.Method => "DELETE"; - string IRestRequest.Endpoint => $"channels/{ChannelId}/permissions/{TargetId}"; - object IRestRequest.Payload => null; - - public ulong ChannelId { get; set; } - public ulong TargetId { get; set; } - - public RemoveChannelPermissionsRequest(ulong channelId, ulong targetId) - { - ChannelId = channelId; - TargetId = targetId; - } - } -} diff --git a/src/Discord.Net/API/Client/Rest/ReorderChannels.cs b/src/Discord.Net/API/Client/Rest/ReorderChannels.cs deleted file mode 100644 index c13f8b21c..000000000 --- a/src/Discord.Net/API/Client/Rest/ReorderChannels.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Discord.API.Converters; -using Newtonsoft.Json; -using System.Linq; - -namespace Discord.API.Client.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class ReorderChannelsRequest : IRestRequest - { - string IRestRequest.Method => "PATCH"; - string IRestRequest.Endpoint => $"guilds/{GuildId}/channels"; - object IRestRequest.Payload - { - get - { - int pos = StartPos; - return ChannelIds.Select(x => new Channel(x, pos++)); - } - } - - public class Channel - { - [JsonProperty("id"), JsonConverter(typeof(LongStringConverter))] - public ulong Id { get; set; } - [JsonProperty("position")] - public int Position { get; set; } - - public Channel(ulong id, int position) - { - Id = id; - Position = position; - } - } - - public ulong GuildId { get; set; } - - public ulong[] ChannelIds { get; set; } = new ulong[0]; - public int StartPos { get; set; } = 0; - - public ReorderChannelsRequest(ulong guildId) - { - GuildId = guildId; - } - } -} diff --git a/src/Discord.Net/API/Client/Rest/ReorderRoles.cs b/src/Discord.Net/API/Client/Rest/ReorderRoles.cs deleted file mode 100644 index 300176a76..000000000 --- a/src/Discord.Net/API/Client/Rest/ReorderRoles.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Discord.API.Converters; -using Newtonsoft.Json; -using System.Linq; - -namespace Discord.API.Client.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class ReorderRolesRequest : IRestRequest - { - string IRestRequest.Method => "PATCH"; - string IRestRequest.Endpoint => $"guilds/{GuildId}/roles"; - object IRestRequest.Payload - { - get - { - int pos = StartPos; - return RoleIds.Select(x => new Role(x, pos++)); - } - } - - public class Role - { - [JsonProperty("id"), JsonConverter(typeof(LongStringConverter))] - public ulong Id { get; set; } - [JsonProperty("position")] - public int Position { get; set; } - - public Role(ulong id, int pos) - { - Id = id; - Position = pos; - } - } - - public ulong GuildId { get; set; } - - public ulong[] RoleIds { get; set; } = new ulong[0]; - public int StartPos { get; set; } = 0; - - public ReorderRolesRequest(ulong guildId) - { - GuildId = guildId; - } - } -} diff --git a/src/Discord.Net/API/Client/Rest/SendIsTyping.cs b/src/Discord.Net/API/Client/Rest/SendIsTyping.cs deleted file mode 100644 index 4c56da0be..000000000 --- a/src/Discord.Net/API/Client/Rest/SendIsTyping.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Client.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class SendIsTypingRequest : IRestRequest - { - string IRestRequest.Method => "POST"; - string IRestRequest.Endpoint => $"channels/{ChannelId}/typing"; - object IRestRequest.Payload => null; - - public ulong ChannelId { get; set; } - - public SendIsTypingRequest(ulong channelId) - { - ChannelId = channelId; - } - } -} diff --git a/src/Discord.Net/API/Client/Rest/UpdateGuild.cs b/src/Discord.Net/API/Client/Rest/UpdateGuild.cs deleted file mode 100644 index f36b18d9f..000000000 --- a/src/Discord.Net/API/Client/Rest/UpdateGuild.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Discord.API.Converters; -using Newtonsoft.Json; - -namespace Discord.API.Client.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class UpdateGuildRequest : IRestRequest - { - string IRestRequest.Method => "PATCH"; - string IRestRequest.Endpoint => $"guilds/{GuildId}"; - object IRestRequest.Payload => this; - - public ulong GuildId { get; set; } - - [JsonProperty("name")] - public string Name { get; set; } - [JsonProperty("region")] - public string Region { get; set; } - [JsonProperty("icon")] - public string IconBase64 { get; set; } - [JsonProperty("afk_channel_id"), JsonConverter(typeof(NullableLongStringConverter))] - public ulong? AFKChannelId { get; set; } - [JsonProperty("afk_timeout")] - public int AFKTimeout { get; set; } - [JsonProperty("splash")] - public object Splash { get; set; } - - public UpdateGuildRequest(ulong guildId) - { - GuildId = guildId; - } - } -} diff --git a/src/Discord.Net/API/Client/Rest/UpdateRole.cs b/src/Discord.Net/API/Client/Rest/UpdateRole.cs deleted file mode 100644 index 4bea0b52b..000000000 --- a/src/Discord.Net/API/Client/Rest/UpdateRole.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Client.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class UpdateRoleRequest : IRestRequest - { - string IRestRequest.Method => "PATCH"; - string IRestRequest.Endpoint => $"guilds/{GuildId}/roles/{RoleId}"; - object IRestRequest.Payload => this; - - public ulong GuildId { get; set; } - public ulong RoleId { get; set; } - - [JsonProperty("name")] - public string Name { get; set; } - [JsonProperty("permissions")] - public uint Permissions { get; set; } - [JsonProperty("hoist")] - public bool IsHoisted { get; set; } - [JsonProperty("color")] - public uint Color { get; set; } - - public UpdateRoleRequest(ulong guildId, ulong roleId) - { - GuildId = guildId; - RoleId = roleId; - } - } -} diff --git a/src/Discord.Net/API/Client/VoiceSocket/Commands/Heartbeat.cs b/src/Discord.Net/API/Client/VoiceSocket/Commands/Heartbeat.cs deleted file mode 100644 index 349a8a28b..000000000 --- a/src/Discord.Net/API/Client/VoiceSocket/Commands/Heartbeat.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Discord.API.Client.VoiceSocket -{ - public class HeartbeatCommand : IWebSocketMessage - { - int IWebSocketMessage.OpCode => (int)OpCodes.Heartbeat; - object IWebSocketMessage.Payload => EpochTime.GetMilliseconds(); - bool IWebSocketMessage.IsPrivate => false; - } -} diff --git a/src/Discord.Net/API/Client/VoiceSocket/Commands/Identify.cs b/src/Discord.Net/API/Client/VoiceSocket/Commands/Identify.cs deleted file mode 100644 index fbb38b9d0..000000000 --- a/src/Discord.Net/API/Client/VoiceSocket/Commands/Identify.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Discord.API.Converters; -using Newtonsoft.Json; - -namespace Discord.API.Client.VoiceSocket -{ - public class IdentifyCommand : IWebSocketMessage - { - int IWebSocketMessage.OpCode => (int)OpCodes.Identify; - object IWebSocketMessage.Payload => this; - bool IWebSocketMessage.IsPrivate => true; - - [JsonProperty("server_id"), JsonConverter(typeof(LongStringConverter))] - public ulong GuildId { get; set; } - [JsonProperty("user_id"), JsonConverter(typeof(LongStringConverter))] - public ulong UserId { get; set; } - [JsonProperty("session_id")] - public string SessionId { get; set; } - [JsonProperty("token")] - public string Token { get; set; } - } -} diff --git a/src/Discord.Net/API/Client/VoiceSocket/Commands/SelectProtocol.cs b/src/Discord.Net/API/Client/VoiceSocket/Commands/SelectProtocol.cs deleted file mode 100644 index d860efe45..000000000 --- a/src/Discord.Net/API/Client/VoiceSocket/Commands/SelectProtocol.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Client.VoiceSocket -{ - public class SelectProtocolCommand : IWebSocketMessage - { - int IWebSocketMessage.OpCode => (int)OpCodes.SelectProtocol; - object IWebSocketMessage.Payload => this; - bool IWebSocketMessage.IsPrivate => false; - - public class Data - { - [JsonProperty("address")] - public string Address { get; set; } - [JsonProperty("port")] - public int Port { get; set; } - [JsonProperty("mode")] - public string Mode { get; set; } - } - [JsonProperty("protocol")] - public string Protocol { get; set; } = "udp"; - [JsonProperty("data")] - private Data ProtocolData { get; } = new Data(); - - public string ExternalAddress { get { return ProtocolData.Address; } set { ProtocolData.Address = value; } } - public int ExternalPort { get { return ProtocolData.Port; } set { ProtocolData.Port = value; } } - public string EncryptionMode { get { return ProtocolData.Mode; } set { ProtocolData.Mode = value; } } - } -} diff --git a/src/Discord.Net/API/Common/Attachment.cs b/src/Discord.Net/API/Common/Attachment.cs new file mode 100644 index 000000000..1f2c4b8b7 --- /dev/null +++ b/src/Discord.Net/API/Common/Attachment.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class Attachment + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("filename")] + public string Filename { get; set; } + [JsonProperty("size")] + public int Size { get; set; } + [JsonProperty("url")] + public string Url { get; set; } + [JsonProperty("proxy_url")] + public string ProxyUrl { get; set; } + [JsonProperty("height")] + public int? Height { get; set; } + [JsonProperty("width")] + public int? Width { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/Channel.cs b/src/Discord.Net/API/Common/Channel.cs new file mode 100644 index 000000000..df71979a8 --- /dev/null +++ b/src/Discord.Net/API/Common/Channel.cs @@ -0,0 +1,37 @@ +#pragma warning disable CA1721 + +using Newtonsoft.Json; + +namespace Discord.API +{ + public class Channel + { + //Shared + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("is_private")] + public bool IsPrivate { get; set; } + [JsonProperty("last_message_id")] + public ulong LastMessageId { get; set; } + + //GuildChannel + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("type")] + public string Type { get; set; } + [JsonProperty("position")] + public int Position { get; set; } + [JsonProperty("permission_overwrites")] + public Overwrite[] PermissionOverwrites { get; set; } + [JsonProperty("topic")] + public string Topic { get; set; } + [JsonProperty("bitrate")] + public int Bitrate { get; set; } + + //DMChannel + [JsonProperty("recipient")] + public User Recipient { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/Embed.cs b/src/Discord.Net/API/Common/Embed.cs new file mode 100644 index 000000000..a75bdb636 --- /dev/null +++ b/src/Discord.Net/API/Common/Embed.cs @@ -0,0 +1,21 @@ +#pragma warning disable CA1721 +using Newtonsoft.Json; + +namespace Discord.API +{ + public class Embed + { + [JsonProperty("title")] + public string Title { get; set; } + [JsonProperty("type")] + public string Type { get; set; } + [JsonProperty("description")] + public string Description { get; set; } + [JsonProperty("url")] + public string Url { get; set; } + [JsonProperty("thumbnail")] + public EmbedThumbnail Thumbnail { get; set; } + [JsonProperty("provider")] + public EmbedProvider Provider { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/EmbedProvider.cs b/src/Discord.Net/API/Common/EmbedProvider.cs new file mode 100644 index 000000000..22c9cbaeb --- /dev/null +++ b/src/Discord.Net/API/Common/EmbedProvider.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class EmbedProvider + { + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("url")] + public string Url { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/EmbedThumbnail.cs b/src/Discord.Net/API/Common/EmbedThumbnail.cs new file mode 100644 index 000000000..73fe3472d --- /dev/null +++ b/src/Discord.Net/API/Common/EmbedThumbnail.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class EmbedThumbnail + { + [JsonProperty("url")] + public string Url { get; set; } + [JsonProperty("proxy_url")] + public string ProxyUrl { get; set; } + [JsonProperty("height")] + public int? Height { get; set; } + [JsonProperty("width")] + public int? Width { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/Emoji.cs b/src/Discord.Net/API/Common/Emoji.cs new file mode 100644 index 000000000..1787c430c --- /dev/null +++ b/src/Discord.Net/API/Common/Emoji.cs @@ -0,0 +1,18 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class Emoji + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("roles")] + public ulong[] Roles { get; set; } + [JsonProperty("require_colons")] + public bool RequireColons { get; set; } + [JsonProperty("managed")] + public bool Managed { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/Guild.cs b/src/Discord.Net/API/Common/Guild.cs new file mode 100644 index 000000000..cbd50e390 --- /dev/null +++ b/src/Discord.Net/API/Common/Guild.cs @@ -0,0 +1,36 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class Guild + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("icon")] + public string Icon { get; set; } + [JsonProperty("splash")] + public string Splash { get; set; } + [JsonProperty("owner_id")] + public ulong OwnerId { get; set; } + [JsonProperty("region")] + public string Region { get; set; } + [JsonProperty("afk_channel_id")] + public ulong? AFKChannelId { get; set; } + [JsonProperty("afk_timeout")] + public int AFKTimeout { get; set; } + [JsonProperty("embed_enabled")] + public bool EmbedEnabled { get; set; } + [JsonProperty("embed_channel_id")] + public ulong? EmbedChannelId { get; set; } + [JsonProperty("verification_level")] + public int VerificationLevel { get; set; } + [JsonProperty("roles")] + public Role[] Roles { get; set; } + [JsonProperty("emojis")] + public Emoji[] Emojis { get; set; } + [JsonProperty("features")] + public string[] Features { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/GuildEmbed.cs b/src/Discord.Net/API/Common/GuildEmbed.cs new file mode 100644 index 000000000..9aceaa472 --- /dev/null +++ b/src/Discord.Net/API/Common/GuildEmbed.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class GuildEmbed + { + [JsonProperty("enabled")] + public bool Enabled { get; set; } + [JsonProperty("channel_id")] + public ulong? ChannelId { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/GuildMember.cs b/src/Discord.Net/API/Common/GuildMember.cs new file mode 100644 index 000000000..c28d47d34 --- /dev/null +++ b/src/Discord.Net/API/Common/GuildMember.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; +using System; + +namespace Discord.API +{ + public class GuildMember + { + [JsonProperty("user")] + public User User { get; set; } + [JsonProperty("roles")] + public ulong[] Roles { get; set; } + [JsonProperty("joined_at")] + public DateTime?JoinedAt { get; set; } + [JsonProperty("deaf")] + public bool Deaf { get; set; } + [JsonProperty("mute")] + public bool Mute { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/Integration.cs b/src/Discord.Net/API/Common/Integration.cs new file mode 100644 index 000000000..9b14a0cd4 --- /dev/null +++ b/src/Discord.Net/API/Common/Integration.cs @@ -0,0 +1,32 @@ +#pragma warning disable CA1721 +using Newtonsoft.Json; +using System; + +namespace Discord.API +{ + public class Integration + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("type")] + public string Type { get; set; } + [JsonProperty("enabled")] + public bool Enabled { get; set; } + [JsonProperty("syncing")] + public bool Syncing { get; set; } + [JsonProperty("role_id")] + public ulong RoleId { get; set; } + [JsonProperty("expire_behavior")] + public ulong ExpireBehavior { get; set; } + [JsonProperty("expire_grace_period")] + public ulong ExpireGracePeriod { get; set; } + [JsonProperty("user")] + public User User { get; set; } + [JsonProperty("account")] + public IntegrationAccount Account { get; set; } + [JsonProperty("synced_at")] + public DateTime SyncedAt { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/IntegrationAccount.cs b/src/Discord.Net/API/Common/IntegrationAccount.cs new file mode 100644 index 000000000..77645caaa --- /dev/null +++ b/src/Discord.Net/API/Common/IntegrationAccount.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class IntegrationAccount + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/Invite.cs b/src/Discord.Net/API/Common/Invite.cs new file mode 100644 index 000000000..276314560 --- /dev/null +++ b/src/Discord.Net/API/Common/Invite.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class Invite + { + [JsonProperty("code")] + public string Code { get; set; } + [JsonProperty("guild")] + public InviteGuild Guild { get; set; } + [JsonProperty("channel")] + public InviteChannel Channel { get; set; } + [JsonProperty("xkcdpass")] + public string XkcdPass { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/InviteChannel.cs b/src/Discord.Net/API/Common/InviteChannel.cs new file mode 100644 index 000000000..545d5fecd --- /dev/null +++ b/src/Discord.Net/API/Common/InviteChannel.cs @@ -0,0 +1,15 @@ +#pragma warning disable CA1721 +using Newtonsoft.Json; + +namespace Discord.API +{ + public class InviteChannel + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("type")] + public string Type { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/InviteGuild.cs b/src/Discord.Net/API/Common/InviteGuild.cs new file mode 100644 index 000000000..7800a71ea --- /dev/null +++ b/src/Discord.Net/API/Common/InviteGuild.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class InviteGuild + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("splash_hash")] + public string SplashHash { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/InviteMetadata.cs b/src/Discord.Net/API/Common/InviteMetadata.cs new file mode 100644 index 000000000..55eeebeee --- /dev/null +++ b/src/Discord.Net/API/Common/InviteMetadata.cs @@ -0,0 +1,23 @@ +using Newtonsoft.Json; +using System; + +namespace Discord.API +{ + public class InviteMetadata : Invite + { + [JsonProperty("inviter")] + public User Inviter { get; set; } + [JsonProperty("uses")] + public int Uses { get; set; } + [JsonProperty("max_uses")] + public int MaxUses { get; set; } + [JsonProperty("max_age")] + public int MaxAge { get; set; } + [JsonProperty("temporary")] + public bool Temporary { get; set; } + [JsonProperty("created_at")] + public DateTime CreatedAt { get; set; } + [JsonProperty("revoked")] + public bool Revoked { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/Message.cs b/src/Discord.Net/API/Common/Message.cs new file mode 100644 index 000000000..666c73652 --- /dev/null +++ b/src/Discord.Net/API/Common/Message.cs @@ -0,0 +1,31 @@ +using Newtonsoft.Json; +using System; + +namespace Discord.API +{ + public class Message + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + [JsonProperty("author")] + public User Author { get; set; } + [JsonProperty("content")] + public string Content { get; set; } + [JsonProperty("timestamp")] + public DateTime Timestamp { get; set; } + [JsonProperty("edited_timestamp")] + public DateTime? EditedTimestamp { get; set; } + [JsonProperty("tts")] + public bool IsTextToSpeech { get; set; } + [JsonProperty("mention_everyone")] + public bool IsMentioningEveryone { get; set; } + [JsonProperty("mentions")] + public User[] Mentions { get; set; } + [JsonProperty("attachments")] + public Attachment[] Attachments { get; set; } + [JsonProperty("embeds")] + public Embed[] Embeds { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/ReadState.cs b/src/Discord.Net/API/Common/ReadState.cs new file mode 100644 index 000000000..6fa0c9b6e --- /dev/null +++ b/src/Discord.Net/API/Common/ReadState.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class ReadState + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("mention_count")] + public int MentionCount { get; set; } + [JsonProperty("last_message_id")] + public ulong LastMentionId { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/Unconfirmed/Connection.cs b/src/Discord.Net/API/Common/Unconfirmed/Connection.cs new file mode 100644 index 000000000..2c82d6cf2 --- /dev/null +++ b/src/Discord.Net/API/Common/Unconfirmed/Connection.cs @@ -0,0 +1,19 @@ +#pragma warning disable CA1721 +using Newtonsoft.Json; + +namespace Discord.API +{ + public class Connection + { + [JsonProperty("integrations")] + public Integration[] Integrations { get; set; } + [JsonProperty("revoked")] + public bool Revoked { get; set; } + [JsonProperty("type")] + public string Type { get; set; } + [JsonProperty("id")] + public string Id { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + } +} diff --git a/src/Discord.Net/API/Client/Common/ExtendedGuild.cs b/src/Discord.Net/API/Common/Unconfirmed/ExtendedGuild.cs similarity index 95% rename from src/Discord.Net/API/Client/Common/ExtendedGuild.cs rename to src/Discord.Net/API/Common/Unconfirmed/ExtendedGuild.cs index 63c55eddb..00aaeb7b9 100644 --- a/src/Discord.Net/API/Client/Common/ExtendedGuild.cs +++ b/src/Discord.Net/API/Common/Unconfirmed/ExtendedGuild.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace Discord.API.Client +namespace Discord.API { public class ExtendedGuild : Guild { diff --git a/src/Discord.Net/API/Common/Unconfirmed/ExtendedMember.cs b/src/Discord.Net/API/Common/Unconfirmed/ExtendedMember.cs new file mode 100644 index 000000000..f09c12e0c --- /dev/null +++ b/src/Discord.Net/API/Common/Unconfirmed/ExtendedMember.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class ExtendedMember : GuildMember + { + [JsonProperty("mute")] + public bool? IsMuted { get; set; } + [JsonProperty("deaf")] + public bool? IsDeafened { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/Unconfirmed/MemberPresence.cs b/src/Discord.Net/API/Common/Unconfirmed/MemberPresence.cs new file mode 100644 index 000000000..a57630ed1 --- /dev/null +++ b/src/Discord.Net/API/Common/Unconfirmed/MemberPresence.cs @@ -0,0 +1,15 @@ +#pragma warning disable CA1721 +using Newtonsoft.Json; + +namespace Discord.API +{ + public class MemberPresence : MemberReference + { + [JsonProperty("game")] + public MemberPresenceGame Game { get; set; } + [JsonProperty("status")] + public UserStatus Status { get; set; } + [JsonProperty("roles")] + public ulong[] Roles { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/Unconfirmed/MemberPresenceGame.cs b/src/Discord.Net/API/Common/Unconfirmed/MemberPresenceGame.cs new file mode 100644 index 000000000..acd805548 --- /dev/null +++ b/src/Discord.Net/API/Common/Unconfirmed/MemberPresenceGame.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class MemberPresenceGame + { + [JsonProperty("name")] + public string Name { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/Unconfirmed/MemberReference.cs b/src/Discord.Net/API/Common/Unconfirmed/MemberReference.cs new file mode 100644 index 000000000..edc41f688 --- /dev/null +++ b/src/Discord.Net/API/Common/Unconfirmed/MemberReference.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class MemberReference + { + [JsonProperty("guild_id")] + public ulong? GuildId { get; set; } + [JsonProperty("user")] + public User User { get; set; } + } +} diff --git a/src/Discord.Net/API/Client/Common/MemberVoiceState.cs b/src/Discord.Net/API/Common/Unconfirmed/MemberVoiceState.cs similarity index 55% rename from src/Discord.Net/API/Client/Common/MemberVoiceState.cs rename to src/Discord.Net/API/Common/Unconfirmed/MemberVoiceState.cs index 4aab1774c..b79df1790 100644 --- a/src/Discord.Net/API/Client/Common/MemberVoiceState.cs +++ b/src/Discord.Net/API/Common/Unconfirmed/MemberVoiceState.cs @@ -1,16 +1,15 @@ -using Discord.API.Converters; -using Newtonsoft.Json; +using Newtonsoft.Json; -namespace Discord.API.Client +namespace Discord.API { public class MemberVoiceState { - [JsonProperty("guild_id"), JsonConverter(typeof(LongStringConverter))] + [JsonProperty("guild_id")] public ulong GuildId { get; set; } - [JsonProperty("user_id"), JsonConverter(typeof(LongStringConverter))] + [JsonProperty("user_id")] public ulong UserId { get; set; } - [JsonProperty("channel_id"), JsonConverter(typeof(NullableLongStringConverter))] + [JsonProperty("channel_id")] public ulong? ChannelId { get; set; } [JsonProperty("session_id")] public string SessionId { get; set; } @@ -22,10 +21,10 @@ namespace Discord.API.Client [JsonProperty("self_deaf")] public bool? IsSelfDeafened { get; set; } [JsonProperty("mute")] - public bool? IsServerMuted { get; set; } + public bool? IsMuted { get; set; } [JsonProperty("deaf")] - public bool? IsServerDeafened { get; set; } + public bool? IsDeafened { get; set; } [JsonProperty("suppress")] - public bool? IsServerSuppressed { get; set; } + public bool? IsSuppressed { get; set; } } } diff --git a/src/Discord.Net/API/Common/Unconfirmed/MessageReference.cs b/src/Discord.Net/API/Common/Unconfirmed/MessageReference.cs new file mode 100644 index 000000000..d2c1dd268 --- /dev/null +++ b/src/Discord.Net/API/Common/Unconfirmed/MessageReference.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class MessageReference + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("message_id")] //Only used in MESSAGE_ACK + public ulong MessageId { get { return Id; } set { Id = value; } } + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/Unconfirmed/Overwrite.cs b/src/Discord.Net/API/Common/Unconfirmed/Overwrite.cs new file mode 100644 index 000000000..f1da83b9e --- /dev/null +++ b/src/Discord.Net/API/Common/Unconfirmed/Overwrite.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class Overwrite + { + [JsonProperty("id")] + public ulong TargetId { get; set; } + [JsonProperty("type")] + public PermissionTarget TargetType { get; set; } + [JsonProperty("deny")] + public uint Deny { get; set; } + [JsonProperty("allow")] + public uint Allow { get; set; } + } +} diff --git a/src/Discord.Net/API/Client/Common/Role.cs b/src/Discord.Net/API/Common/Unconfirmed/Role.cs similarity index 77% rename from src/Discord.Net/API/Client/Common/Role.cs rename to src/Discord.Net/API/Common/Unconfirmed/Role.cs index 59431989a..e561ab355 100644 --- a/src/Discord.Net/API/Client/Common/Role.cs +++ b/src/Discord.Net/API/Common/Unconfirmed/Role.cs @@ -1,11 +1,10 @@ -using Discord.API.Converters; -using Newtonsoft.Json; +using Newtonsoft.Json; -namespace Discord.API.Client +namespace Discord.API { public class Role { - [JsonProperty("id"), JsonConverter(typeof(LongStringConverter))] + [JsonProperty("id")] public ulong Id { get; set; } [JsonProperty("permissions")] public uint? Permissions { get; set; } diff --git a/src/Discord.Net/API/Common/Unconfirmed/RoleReference.cs b/src/Discord.Net/API/Common/Unconfirmed/RoleReference.cs new file mode 100644 index 000000000..bf516faaa --- /dev/null +++ b/src/Discord.Net/API/Common/Unconfirmed/RoleReference.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class RoleReference + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + [JsonProperty("role_id")] + public ulong RoleId { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/User.cs b/src/Discord.Net/API/Common/User.cs new file mode 100644 index 000000000..c8e566711 --- /dev/null +++ b/src/Discord.Net/API/Common/User.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class User + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("username")] + public string Username { get; set; } + [JsonProperty("discriminator")] + public ushort Discriminator { get; set; } + [JsonProperty("avatar")] + public string Avatar { get; set; } + [JsonProperty("verified")] + public bool IsVerified { get; set; } + [JsonProperty("email")] + public string Email { get; set; } + [JsonProperty("bot")] + public bool Bot { get; set; } + } +} diff --git a/src/Discord.Net/API/Common/UserGuild.cs b/src/Discord.Net/API/Common/UserGuild.cs new file mode 100644 index 000000000..9b0819395 --- /dev/null +++ b/src/Discord.Net/API/Common/UserGuild.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class UserGuild + { + [JsonProperty("id")] + public ulong Id { get; set; } + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("icon")] + public string Icon { get; set; } + [JsonProperty("owner")] + public bool Owner { get; set; } + } +} diff --git a/src/Discord.Net/API/Converters.cs b/src/Discord.Net/API/Converters.cs deleted file mode 100644 index 5d80ca99f..000000000 --- a/src/Discord.Net/API/Converters.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System; -using Newtonsoft.Json; -using System.Collections.Generic; - -namespace Discord.API.Converters -{ - public class LongStringConverter : JsonConverter - { - public override bool CanConvert(Type objectType) - => objectType == typeof(ulong); - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - => ((string)reader.Value).ToId(); - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - => writer.WriteValue(((ulong)value).ToIdString()); - } - - public class NullableLongStringConverter : JsonConverter - { - public override bool CanConvert(Type objectType) - => objectType == typeof(ulong?); - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - => ((string)reader.Value).ToNullableId(); - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - => writer.WriteValue(((ulong?)value).ToIdString()); - } - - /*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) - { - List result = new List(); - if (reader.TokenType == JsonToken.StartArray) - { - reader.Read(); - while (reader.TokenType != JsonToken.EndArray) - { - result.Add(IdConvert.ToLong((string)reader.Value)); - reader.Read(); - } - } - return result; - } - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - if (value == null) - writer.WriteNull(); - else - { - writer.WriteStartArray(); - foreach (var v in (IEnumerable)value) - writer.WriteValue(IdConvert.ToString(v)); - writer.WriteEndArray(); - } - } - }*/ - - 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) - { - var result = new List(); - if (reader.TokenType == JsonToken.StartArray) - { - reader.Read(); - while (reader.TokenType != JsonToken.EndArray) - { - result.Add(((string)reader.Value).ToId()); - reader.Read(); - } - } - return result.ToArray(); - } - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - if (value == null) - writer.WriteNull(); - else - { - writer.WriteStartArray(); - var a = (ulong[])value; - for (int i = 0; i < a.Length; i++) - writer.WriteValue(a[i].ToIdString()); - writer.WriteEndArray(); - } - } - } -} diff --git a/src/Discord.Net/API/Extensions.cs b/src/Discord.Net/API/Extensions.cs deleted file mode 100644 index 77d25fe4e..000000000 --- a/src/Discord.Net/API/Extensions.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Text; - -namespace Discord.API -{ - internal static class RestRequestExtensions - { - public static void AddQueryParam(this IRestRequest request, StringBuilder builder, string name, string value) - { - if (builder.Length == 0) - builder.Append('?'); - else - builder.Append('&'); - builder.Append(Uri.EscapeDataString(name)); - builder.Append('='); - builder.Append(Uri.EscapeDataString(value)); - } - } -} diff --git a/src/Discord.Net/API/Client/GatewaySocket/OpCodes.cs b/src/Discord.Net/API/GatewaySocket/OpCode.cs similarity index 67% rename from src/Discord.Net/API/Client/GatewaySocket/OpCodes.cs rename to src/Discord.Net/API/GatewaySocket/OpCode.cs index 9942c670e..cf8e142ef 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/OpCodes.cs +++ b/src/Discord.Net/API/GatewaySocket/OpCode.cs @@ -1,6 +1,6 @@ -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { - public enum OpCodes : byte + public enum OpCode : byte { /// C←S - Used to send most events. Dispatch = 0, @@ -12,13 +12,15 @@ StatusUpdate = 3, /// C→S - Used to join a particular voice channel. VoiceStateUpdate = 4, - /// C→S - Used to ensure the server's voice server is alive. Only send this if voice connection fails or suddenly drops. + /// C→S - Used to ensure the guild's voice server is alive. Only send this if voice connection fails or suddenly drops. VoiceServerPing = 5, /// C→S - Used to resume a connection after a redirect occurs. Resume = 6, /// C←S - Used to notify a client that they must reconnect to another gateway. - Redirect = 7, - /// C→S - Used to request all members that were withheld by large_threshold - RequestGuildMembers = 8 + Reconnect = 7, + /// C→S - Used to request all members that were withheld by large_threshold. + RequestGuildMembers = 8, + /// C←S - Used to notify the client of an invalid session id. + InvalidSession = 9 } } diff --git a/src/Discord.Net/API/GatewaySocket/Unconfirmed/Commands/Heartbeat.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Commands/Heartbeat.cs new file mode 100644 index 000000000..5dac73ec5 --- /dev/null +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Commands/Heartbeat.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; +using System; + +namespace Discord.API.GatewaySocket +{ + [JsonObject(MemberSerialization.OptIn)] + public class HeartbeatCommand : IWebSocketMessage + { + int IWebSocketMessage.OpCode => (int)OpCode.Heartbeat; + object IWebSocketMessage.Payload => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + } +} diff --git a/src/Discord.Net/API/Client/GatewaySocket/Commands/Identify.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Commands/Identify.cs similarity index 73% rename from src/Discord.Net/API/Client/GatewaySocket/Commands/Identify.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Commands/Identify.cs index 8437f595c..235f615ef 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Commands/Identify.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Commands/Identify.cs @@ -1,21 +1,20 @@ using Newtonsoft.Json; using System.Collections.Generic; -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { [JsonObject(MemberSerialization.OptIn)] public class IdentifyCommand : IWebSocketMessage { - int IWebSocketMessage.OpCode => (int)OpCodes.Identify; + int IWebSocketMessage.OpCode => (int)OpCode.Identify; object IWebSocketMessage.Payload => this; - bool IWebSocketMessage.IsPrivate => false; [JsonProperty("v")] public int Version { get; set; } [JsonProperty("token")] public string Token { get; set; } [JsonProperty("properties")] - public Dictionary Properties { get; set; } + public IReadOnlyDictionary Properties { get; set; } [JsonProperty("large_threshold", NullValueHandling = NullValueHandling.Ignore)] public int LargeThreshold { get; set; } [JsonProperty("compress")] diff --git a/src/Discord.Net/API/Client/GatewaySocket/Commands/RequestMembers.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Commands/RequestMembers.cs similarity index 53% rename from src/Discord.Net/API/Client/GatewaySocket/Commands/RequestMembers.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Commands/RequestMembers.cs index cc3c93176..c4614284d 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Commands/RequestMembers.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Commands/RequestMembers.cs @@ -1,16 +1,14 @@ -using Discord.API.Converters; -using Newtonsoft.Json; +using Newtonsoft.Json; -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { [JsonObject(MemberSerialization.OptIn)] public class RequestMembersCommand : IWebSocketMessage { - int IWebSocketMessage.OpCode => (int)OpCodes.RequestGuildMembers; + int IWebSocketMessage.OpCode => (int)OpCode.RequestGuildMembers; object IWebSocketMessage.Payload => this; - bool IWebSocketMessage.IsPrivate => false; - [JsonProperty("guild_id"), JsonConverter(typeof(LongStringArrayConverter))] + [JsonProperty("guild_id")] public ulong[] GuildId { get; set; } [JsonProperty("query")] public string Query { get; set; } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Commands/Resume.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Commands/Resume.cs similarity index 69% rename from src/Discord.Net/API/Client/GatewaySocket/Commands/Resume.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Commands/Resume.cs index 15486e577..1525c6b6a 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Commands/Resume.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Commands/Resume.cs @@ -1,13 +1,12 @@ using Newtonsoft.Json; -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { [JsonObject(MemberSerialization.OptIn)] public class ResumeCommand : IWebSocketMessage { - int IWebSocketMessage.OpCode => (int)OpCodes.Resume; + int IWebSocketMessage.OpCode => (int)OpCode.Resume; object IWebSocketMessage.Payload => this; - bool IWebSocketMessage.IsPrivate => false; [JsonProperty("session_id")] public string SessionId { get; set; } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Commands/UpdateStatus.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Commands/UpdateStatus.cs similarity index 74% rename from src/Discord.Net/API/Client/GatewaySocket/Commands/UpdateStatus.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Commands/UpdateStatus.cs index dff18b08c..8aa22ede5 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Commands/UpdateStatus.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Commands/UpdateStatus.cs @@ -1,13 +1,12 @@ using Newtonsoft.Json; -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { [JsonObject(MemberSerialization.OptIn)] public class UpdateStatusCommand : IWebSocketMessage { - int IWebSocketMessage.OpCode => (int)OpCodes.StatusUpdate; + int IWebSocketMessage.OpCode => (int)OpCode.StatusUpdate; object IWebSocketMessage.Payload => this; - bool IWebSocketMessage.IsPrivate => false; public class GameInfo { diff --git a/src/Discord.Net/API/Client/GatewaySocket/Commands/UpdateVoice.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Commands/UpdateVoice.cs similarity index 51% rename from src/Discord.Net/API/Client/GatewaySocket/Commands/UpdateVoice.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Commands/UpdateVoice.cs index 3ccf92c65..d7befa41e 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Commands/UpdateVoice.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Commands/UpdateVoice.cs @@ -1,18 +1,16 @@ -using Discord.API.Converters; -using Newtonsoft.Json; +using Newtonsoft.Json; -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { [JsonObject(MemberSerialization.OptIn)] public class UpdateVoiceCommand : IWebSocketMessage { - int IWebSocketMessage.OpCode => (int)OpCodes.VoiceStateUpdate; + int IWebSocketMessage.OpCode => (int)OpCode.VoiceStateUpdate; object IWebSocketMessage.Payload => this; - bool IWebSocketMessage.IsPrivate => false; - [JsonProperty("guild_id"), JsonConverter(typeof(NullableLongStringConverter))] + [JsonProperty("guild_id")] public ulong? GuildId { get; set; } - [JsonProperty("channel_id"), JsonConverter(typeof(NullableLongStringConverter))] + [JsonProperty("channel_id")] public ulong? ChannelId { get; set; } [JsonProperty("self_mute")] public bool IsSelfMuted { get; set; } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/ChannelCreate.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/ChannelCreate.cs similarity index 52% rename from src/Discord.Net/API/Client/GatewaySocket/Events/ChannelCreate.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/ChannelCreate.cs index ca26fecc7..1e2769036 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/ChannelCreate.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/ChannelCreate.cs @@ -1,4 +1,4 @@ -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { public class ChannelCreateEvent : Channel { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/ChannelDelete.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/ChannelDelete.cs similarity index 54% rename from src/Discord.Net/API/Client/GatewaySocket/Events/ChannelDelete.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/ChannelDelete.cs index 2b61a7d78..91c57d640 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/ChannelDelete.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/ChannelDelete.cs @@ -1,4 +1,4 @@ -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { public class ChannelDeleteEvent : Channel { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/ChannelUpdate.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/ChannelUpdate.cs similarity index 54% rename from src/Discord.Net/API/Client/GatewaySocket/Events/ChannelUpdate.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/ChannelUpdate.cs index 4565ce1bc..227124291 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/ChannelUpdate.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/ChannelUpdate.cs @@ -1,4 +1,4 @@ -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { public class ChannelUpdateEvent : Channel { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildBanAdd.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildBanAdd.cs similarity index 56% rename from src/Discord.Net/API/Client/GatewaySocket/Events/GuildBanAdd.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildBanAdd.cs index 7ba24473a..c1149ee15 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildBanAdd.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildBanAdd.cs @@ -1,4 +1,4 @@ -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { public class GuildBanAddEvent : MemberReference { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildBanRemove.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildBanRemove.cs similarity index 57% rename from src/Discord.Net/API/Client/GatewaySocket/Events/GuildBanRemove.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildBanRemove.cs index a56a98494..5474146a7 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildBanRemove.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildBanRemove.cs @@ -1,4 +1,4 @@ -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { public class GuildBanRemoveEvent : MemberReference { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildCreate.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildCreate.cs similarity index 55% rename from src/Discord.Net/API/Client/GatewaySocket/Events/GuildCreate.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildCreate.cs index 41c1c71c7..f07adaed3 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildCreate.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildCreate.cs @@ -1,4 +1,4 @@ -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { public class GuildCreateEvent : ExtendedGuild { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildDelete.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildDelete.cs similarity index 55% rename from src/Discord.Net/API/Client/GatewaySocket/Events/GuildDelete.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildDelete.cs index cf824c40e..3408183ad 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildDelete.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildDelete.cs @@ -1,4 +1,4 @@ -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { public class GuildDeleteEvent : ExtendedGuild { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildMemberAdd.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildMemberAdd.cs similarity index 57% rename from src/Discord.Net/API/Client/GatewaySocket/Events/GuildMemberAdd.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildMemberAdd.cs index a2ce6ddb2..098a2ea3b 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildMemberAdd.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildMemberAdd.cs @@ -1,4 +1,4 @@ -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { public class GuildMemberAddEvent : ExtendedMember { } } diff --git a/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildMemberRemove.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildMemberRemove.cs new file mode 100644 index 000000000..686af9511 --- /dev/null +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildMemberRemove.cs @@ -0,0 +1,4 @@ +namespace Discord.API.GatewaySocket +{ + public class GuildMemberRemoveEvent : MemberReference { } +} diff --git a/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildMemberUpdate.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildMemberUpdate.cs new file mode 100644 index 000000000..339489f76 --- /dev/null +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildMemberUpdate.cs @@ -0,0 +1,4 @@ +namespace Discord.API.GatewaySocket +{ + public class GuildMemberUpdateEvent : GuildMember { } +} diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildMembersChunk.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildMembersChunk.cs similarity index 51% rename from src/Discord.Net/API/Client/GatewaySocket/Events/GuildMembersChunk.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildMembersChunk.cs index 4f2d36b8a..72936dd16 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildMembersChunk.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildMembersChunk.cs @@ -1,11 +1,10 @@ -using Discord.API.Converters; -using Newtonsoft.Json; +using Newtonsoft.Json; -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { public class GuildMembersChunkEvent { - [JsonProperty("guild_id"), JsonConverter(typeof(LongStringConverter))] + [JsonProperty("guild_id")] public ulong GuildId { get; set; } [JsonProperty("members")] public ExtendedMember[] Members { get; set; } diff --git a/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildRoleCreate.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildRoleCreate.cs new file mode 100644 index 000000000..2740546dc --- /dev/null +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildRoleCreate.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API.GatewaySocket +{ + public class GuildRoleCreateEvent + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + [JsonProperty("role")] + public Role Data { get; set; } + } +} diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildRoleDelete.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildRoleDelete.cs similarity index 57% rename from src/Discord.Net/API/Client/GatewaySocket/Events/GuildRoleDelete.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildRoleDelete.cs index 2ecd2edc5..4986f6193 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildRoleDelete.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildRoleDelete.cs @@ -1,4 +1,4 @@ -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { public class GuildRoleDeleteEvent : RoleReference { } } diff --git a/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildRoleUpdate.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildRoleUpdate.cs new file mode 100644 index 000000000..56c232d06 --- /dev/null +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildRoleUpdate.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API.GatewaySocket +{ + public class GuildRoleUpdateEvent + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + [JsonProperty("role")] + public Role Data { get; set; } + } +} diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildUpdate.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildUpdate.cs similarity index 52% rename from src/Discord.Net/API/Client/GatewaySocket/Events/GuildUpdate.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildUpdate.cs index 8fc0f1350..b292c54e6 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/GuildUpdate.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/GuildUpdate.cs @@ -1,4 +1,4 @@ -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { public class GuildUpdateEvent : Guild { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/MessageAck.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/MessageAck.cs similarity index 56% rename from src/Discord.Net/API/Client/GatewaySocket/Events/MessageAck.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/MessageAck.cs index 64c106ef5..c8379d369 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/MessageAck.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/MessageAck.cs @@ -1,4 +1,4 @@ -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { public class MessageAckEvent : MessageReference { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/MessageCreate.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/MessageCreate.cs similarity index 54% rename from src/Discord.Net/API/Client/GatewaySocket/Events/MessageCreate.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/MessageCreate.cs index d6d2ec1cc..6b0e1e024 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/MessageCreate.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/MessageCreate.cs @@ -1,4 +1,4 @@ -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { public class MessageCreateEvent : Message { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/MessageDelete.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/MessageDelete.cs similarity index 57% rename from src/Discord.Net/API/Client/GatewaySocket/Events/MessageDelete.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/MessageDelete.cs index cfc2df7ff..d932960ba 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/MessageDelete.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/MessageDelete.cs @@ -1,4 +1,4 @@ -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { public class MessageDeleteEvent : MessageReference { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/MessageUpdate.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/MessageUpdate.cs similarity index 54% rename from src/Discord.Net/API/Client/GatewaySocket/Events/MessageUpdate.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/MessageUpdate.cs index 23521fd93..39ef7e1ba 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/MessageUpdate.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/MessageUpdate.cs @@ -1,4 +1,4 @@ -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { public class MessageUpdateEvent : Message { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/PresenceUpdate.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/PresenceUpdate.cs similarity index 57% rename from src/Discord.Net/API/Client/GatewaySocket/Events/PresenceUpdate.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/PresenceUpdate.cs index c40853336..59d915354 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/PresenceUpdate.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/PresenceUpdate.cs @@ -1,4 +1,4 @@ -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { public class PresenceUpdateEvent : MemberPresence { } } diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/Ready.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/Ready.cs similarity index 96% rename from src/Discord.Net/API/Client/GatewaySocket/Events/Ready.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/Ready.cs index 744e5b4b5..51e0f3c8c 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/Ready.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/Ready.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { public class ReadyEvent { diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/Redirect.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/Redirect.cs similarity index 77% rename from src/Discord.Net/API/Client/GatewaySocket/Events/Redirect.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/Redirect.cs index fe9d644d4..180bf574f 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/Redirect.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/Redirect.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { public class RedirectEvent { diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/Resumed.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/Resumed.cs similarity index 79% rename from src/Discord.Net/API/Client/GatewaySocket/Events/Resumed.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/Resumed.cs index 6a50fbe32..3f98b1f35 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/Resumed.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/Resumed.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { public class ResumedEvent { diff --git a/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/TypingStart.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/TypingStart.cs new file mode 100644 index 000000000..063011bbb --- /dev/null +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/TypingStart.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Discord.API.GatewaySocket +{ + public class TypingStartEvent + { + [JsonProperty("user_id")] + public ulong UserId { get; set; } + [JsonProperty("channel_id")] + public ulong ChannelId { get; set; } + [JsonProperty("timestamp")] + public int Timestamp { get; set; } + } +} diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/UserUpdate.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/UserUpdate.cs similarity index 51% rename from src/Discord.Net/API/Client/GatewaySocket/Events/UserUpdate.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/UserUpdate.cs index 3c366310a..e49f8b292 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/UserUpdate.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/UserUpdate.cs @@ -1,4 +1,4 @@ -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { public class UserUpdateEvent : User { } } diff --git a/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/VoiceServerUpdate.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/VoiceServerUpdate.cs new file mode 100644 index 000000000..75936bb93 --- /dev/null +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/VoiceServerUpdate.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Discord.API.GatewaySocket +{ + public class VoiceServerUpdateEvent + { + [JsonProperty("guild_id")] + public ulong GuildId { get; set; } + [JsonProperty("endpoint")] + public string Endpoint { get; set; } + [JsonProperty("token")] + public string Token { get; set; } + } +} diff --git a/src/Discord.Net/API/Client/GatewaySocket/Events/VoiceStateUpdate.cs b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/VoiceStateUpdate.cs similarity index 58% rename from src/Discord.Net/API/Client/GatewaySocket/Events/VoiceStateUpdate.cs rename to src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/VoiceStateUpdate.cs index f3ba96b17..c0b99c710 100644 --- a/src/Discord.Net/API/Client/GatewaySocket/Events/VoiceStateUpdate.cs +++ b/src/Discord.Net/API/GatewaySocket/Unconfirmed/Events/VoiceStateUpdate.cs @@ -1,4 +1,4 @@ -namespace Discord.API.Client.GatewaySocket +namespace Discord.API.GatewaySocket { public class VoiceStateUpdateEvent : MemberVoiceState { } } diff --git a/src/Discord.Net/API/IRestRequest.cs b/src/Discord.Net/API/IRestRequest.cs deleted file mode 100644 index af520370d..000000000 --- a/src/Discord.Net/API/IRestRequest.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.IO; - -namespace Discord.API -{ - public interface IRestRequest - { - string Method { get; } - string Endpoint { get; } - object Payload { get; } - } - public interface IRestRequest : IRestRequest - where ResponseT : class - { - } - - public interface IRestFileRequest : IRestRequest - { - string Filename { get; } - Stream Stream { get; } - } - public interface IRestFileRequest : IRestFileRequest, IRestRequest - where ResponseT : class - { - } -} diff --git a/src/Discord.Net/API/Client/IWebSocketMessage.cs b/src/Discord.Net/API/IWebSocketMessage.cs similarity index 92% rename from src/Discord.Net/API/Client/IWebSocketMessage.cs rename to src/Discord.Net/API/IWebSocketMessage.cs index 6f6de535a..06c51bf77 100644 --- a/src/Discord.Net/API/Client/IWebSocketMessage.cs +++ b/src/Discord.Net/API/IWebSocketMessage.cs @@ -1,12 +1,11 @@ using Newtonsoft.Json; -namespace Discord.API.Client +namespace Discord.API { public interface IWebSocketMessage { int OpCode { get; } object Payload { get; } - bool IsPrivate { get; } } public class WebSocketMessage { diff --git a/src/Discord.Net/API/Rest/AcceptInvite.cs b/src/Discord.Net/API/Rest/AcceptInvite.cs new file mode 100644 index 000000000..72350253f --- /dev/null +++ b/src/Discord.Net/API/Rest/AcceptInvite.cs @@ -0,0 +1,18 @@ +using Discord.Net.Rest; + +namespace Discord.API.Rest +{ + public class AcceptInviteRequest : IRestRequest + { + string IRestRequest.Method => "POST"; + string IRestRequest.Endpoint => $"invites/{InviteCode}"; + object IRestRequest.Payload => null; + + public string InviteCode { get; } + + public AcceptInviteRequest(string inviteCode) + { + InviteCode = inviteCode; + } + } +} diff --git a/src/Discord.Net/API/Client/Rest/AckMessage.cs b/src/Discord.Net/API/Rest/AckMessage.cs similarity index 59% rename from src/Discord.Net/API/Client/Rest/AckMessage.cs rename to src/Discord.Net/API/Rest/AckMessage.cs index 4cf238b72..387215624 100644 --- a/src/Discord.Net/API/Client/Rest/AckMessage.cs +++ b/src/Discord.Net/API/Rest/AckMessage.cs @@ -1,19 +1,15 @@ -using Newtonsoft.Json; +using Discord.Net.Rest; -namespace Discord.API.Client.Rest +namespace Discord.API.Rest { - [JsonObject(MemberSerialization.OptIn)] public class AckMessageRequest : IRestRequest { string IRestRequest.Method => "POST"; string IRestRequest.Endpoint => $"channels/{ChannelId}/messages/{MessageId}/ack"; object IRestRequest.Payload => null; - public ulong ChannelId { get; set; } - public ulong MessageId { get; set; } - - /*[JsonProperty("manual")] - public bool Manual { get; set; }*/ + public ulong ChannelId { get; } + public ulong MessageId { get; } public AckMessageRequest(ulong channelId, ulong messageId) { diff --git a/src/Discord.Net/API/Rest/BeginGuildPrune.cs b/src/Discord.Net/API/Rest/BeginGuildPrune.cs new file mode 100644 index 000000000..0a35bdc80 --- /dev/null +++ b/src/Discord.Net/API/Rest/BeginGuildPrune.cs @@ -0,0 +1,23 @@ +using Discord.Net.Rest; +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization.OptIn)] + public class BeginGuildPruneRequest : IRestRequest + { + string IRestRequest.Method => "POST"; + string IRestRequest.Endpoint => $"guilds/{GuildId}/prune"; + object IRestRequest.Payload => this; + + public ulong GuildId { get; } + + [JsonProperty("days")] + public int Days { get; set; } = 30; + + public BeginGuildPruneRequest(ulong guildId) + { + GuildId = guildId; + } + } +} diff --git a/src/Discord.Net/API/Client/Rest/CreateInvite.cs b/src/Discord.Net/API/Rest/CreateChannelInvite.cs similarity index 62% rename from src/Discord.Net/API/Client/Rest/CreateInvite.cs rename to src/Discord.Net/API/Rest/CreateChannelInvite.cs index 73f15c248..f60232f48 100644 --- a/src/Discord.Net/API/Client/Rest/CreateInvite.cs +++ b/src/Discord.Net/API/Rest/CreateChannelInvite.cs @@ -1,28 +1,27 @@ -using Newtonsoft.Json; +using Discord.Net.Rest; +using Newtonsoft.Json; -namespace Discord.API.Client.Rest +namespace Discord.API.Rest { [JsonObject(MemberSerialization.OptIn)] - public class CreateInviteRequest : IRestRequest + public class CreateChannelInviteRequest : IRestRequest { string IRestRequest.Method => "POST"; string IRestRequest.Endpoint => $"channels/{ChannelId}/invites"; object IRestRequest.Payload => this; - public ulong ChannelId { get; set; } + public ulong ChannelId { get; } [JsonProperty("max_age")] - public int MaxAge { get; set; } = 1800; + public int MaxAge { get; set; } = 86400; //24 Hours [JsonProperty("max_uses")] public int MaxUses { get; set; } = 0; [JsonProperty("temporary")] public bool IsTemporary { get; set; } = false; [JsonProperty("xkcdpass")] public bool WithXkcdPass { get; set; } = false; - /*[JsonProperty("validate")] - public bool Validate { get; set; }*/ - public CreateInviteRequest(ulong channelId) + public CreateChannelInviteRequest(ulong channelId) { ChannelId = channelId; } diff --git a/src/Discord.Net/API/Client/Rest/CreatePrivateChannel.cs b/src/Discord.Net/API/Rest/CreateDMChannel.cs similarity index 56% rename from src/Discord.Net/API/Client/Rest/CreatePrivateChannel.cs rename to src/Discord.Net/API/Rest/CreateDMChannel.cs index e1087dc36..473ecc8e2 100644 --- a/src/Discord.Net/API/Client/Rest/CreatePrivateChannel.cs +++ b/src/Discord.Net/API/Rest/CreateDMChannel.cs @@ -1,16 +1,16 @@ -using Discord.API.Converters; +using Discord.Net.Rest; using Newtonsoft.Json; -namespace Discord.API.Client.Rest +namespace Discord.API.Rest { [JsonObject(MemberSerialization.OptIn)] - public class CreatePrivateChannelRequest : IRestRequest + public class CreateDMChannelRequest : IRestRequest { string IRestRequest.Method => "POST"; string IRestRequest.Endpoint => $"users/@me/channels"; object IRestRequest.Payload => this; - [JsonProperty("recipient_id"), JsonConverter(typeof(LongStringConverter))] + [JsonProperty("recipient_id")] public ulong RecipientId { get; set; } } } diff --git a/src/Discord.Net/API/Client/Rest/CreateGuild.cs b/src/Discord.Net/API/Rest/CreateGuild.cs similarity index 63% rename from src/Discord.Net/API/Client/Rest/CreateGuild.cs rename to src/Discord.Net/API/Rest/CreateGuild.cs index a18d2bee9..c2c9532e0 100644 --- a/src/Discord.Net/API/Client/Rest/CreateGuild.cs +++ b/src/Discord.Net/API/Rest/CreateGuild.cs @@ -1,6 +1,9 @@ -using Newtonsoft.Json; +using Discord.Net.JsonConverters; +using Discord.Net.Rest; +using Newtonsoft.Json; +using System.IO; -namespace Discord.API.Client.Rest +namespace Discord.API.Rest { [JsonObject(MemberSerialization.OptIn)] public class CreateGuildRequest : IRestRequest @@ -13,7 +16,7 @@ namespace Discord.API.Client.Rest public string Name { get; set; } [JsonProperty("region")] public string Region { get; set; } - [JsonProperty("icon")] - public string IconBase64 { get; set; } + [JsonProperty("icon"), JsonConverter(typeof(ImageConverter))] + public Stream Icon { get; set; } } } diff --git a/src/Discord.Net/API/Rest/CreateGuildBan.cs b/src/Discord.Net/API/Rest/CreateGuildBan.cs new file mode 100644 index 000000000..c76122c5f --- /dev/null +++ b/src/Discord.Net/API/Rest/CreateGuildBan.cs @@ -0,0 +1,24 @@ +using Discord.Net.Rest; +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + public class CreateGuildBanRequest : IRestRequest + { + string IRestRequest.Method => "PUT"; + string IRestRequest.Endpoint => $"guilds/{GuildId}/bans/{UserId}"; + object IRestRequest.Payload => null; + + public ulong GuildId { get; } + public ulong UserId { get; } + + [JsonProperty("delete-message-days")] + public int PruneDays { get; set; } = 0; + + public CreateGuildBanRequest(ulong guildId, ulong userId) + { + GuildId = guildId; + UserId = userId; + } + } +} diff --git a/src/Discord.Net/API/Client/Rest/CreateChannel.cs b/src/Discord.Net/API/Rest/CreateGuildChannel.cs similarity index 73% rename from src/Discord.Net/API/Client/Rest/CreateChannel.cs rename to src/Discord.Net/API/Rest/CreateGuildChannel.cs index 90d9afec0..839f3cc1d 100644 --- a/src/Discord.Net/API/Client/Rest/CreateChannel.cs +++ b/src/Discord.Net/API/Rest/CreateGuildChannel.cs @@ -1,6 +1,7 @@ -using Newtonsoft.Json; +using Discord.Net.Rest; +using Newtonsoft.Json; -namespace Discord.API.Client.Rest +namespace Discord.API.Rest { [JsonObject(MemberSerialization.OptIn)] public class CreateChannelRequest : IRestRequest @@ -9,12 +10,14 @@ namespace Discord.API.Client.Rest string IRestRequest.Endpoint => $"guilds/{GuildId}/channels"; object IRestRequest.Payload => this; - public ulong GuildId { get; set; } + public ulong GuildId { get; } [JsonProperty("name")] public string Name { get; set; } [JsonProperty("type")] public ChannelType Type { get; set; } + [JsonProperty("bitrate")] + public int Bitrate { get; set; } public CreateChannelRequest(ulong guildId) { diff --git a/src/Discord.Net/API/Rest/CreateGuildIntegration.cs b/src/Discord.Net/API/Rest/CreateGuildIntegration.cs new file mode 100644 index 000000000..7cf48397d --- /dev/null +++ b/src/Discord.Net/API/Rest/CreateGuildIntegration.cs @@ -0,0 +1,25 @@ +using Discord.Net.Rest; +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization.OptIn)] + public class CreateGuildIntegrationRequest : IRestRequest + { + string IRestRequest.Method => "POST"; + string IRestRequest.Endpoint => $"guilds/{GuildId}/integrations"; + object IRestRequest.Payload => this; + + public ulong GuildId { get; } + + [JsonProperty("id")] + public ulong IntegrationId { get; set; } + [JsonProperty("type")] + public string Type { get; set; } + + public CreateGuildIntegrationRequest(ulong guildId) + { + GuildId = guildId; + } + } +} diff --git a/src/Discord.Net/API/Client/Rest/CreateRole.cs b/src/Discord.Net/API/Rest/CreateGuildRole.cs similarity index 69% rename from src/Discord.Net/API/Client/Rest/CreateRole.cs rename to src/Discord.Net/API/Rest/CreateGuildRole.cs index 3978c6aaa..0d21805d8 100644 --- a/src/Discord.Net/API/Client/Rest/CreateRole.cs +++ b/src/Discord.Net/API/Rest/CreateGuildRole.cs @@ -1,15 +1,14 @@ -using Newtonsoft.Json; +using Discord.Net.Rest; -namespace Discord.API.Client.Rest +namespace Discord.API.Rest { - [JsonObject(MemberSerialization.OptIn)] public class CreateRoleRequest : IRestRequest { string IRestRequest.Method => "POST"; string IRestRequest.Endpoint => $"guilds/{GuildId}/roles"; object IRestRequest.Payload => null; - public ulong GuildId { get; set; } + public ulong GuildId { get; } public CreateRoleRequest(ulong guildId) { diff --git a/src/Discord.Net/API/Client/Rest/SendMessage.cs b/src/Discord.Net/API/Rest/CreateMessage.cs similarity index 51% rename from src/Discord.Net/API/Client/Rest/SendMessage.cs rename to src/Discord.Net/API/Rest/CreateMessage.cs index 9caca991d..1018a8c66 100644 --- a/src/Discord.Net/API/Client/Rest/SendMessage.cs +++ b/src/Discord.Net/API/Rest/CreateMessage.cs @@ -1,24 +1,25 @@ -using Newtonsoft.Json; +using Discord.Net.Rest; +using Newtonsoft.Json; -namespace Discord.API.Client.Rest +namespace Discord.API.Rest { [JsonObject(MemberSerialization.OptIn)] - public class SendMessageRequest : IRestRequest + public class CreateMessageRequest : IRestRequest { string IRestRequest.Method => "POST"; string IRestRequest.Endpoint => $"channels/{ChannelId}/messages"; object IRestRequest.Payload => this; - public ulong ChannelId { get; set; } + public ulong ChannelId { get; } [JsonProperty("content")] public string Content { get; set; } [JsonProperty("nonce", NullValueHandling = NullValueHandling.Ignore)] - public string Nonce { get; set; } - [JsonProperty("tts")] - public bool IsTTS { get; set; } + public string Nonce { get; set; } = null; + [JsonProperty("tts", DefaultValueHandling = DefaultValueHandling.Ignore)] + public bool IsTTS { get; set; } = false; - public SendMessageRequest(ulong channelId) + public CreateMessageRequest(ulong channelId) { ChannelId = channelId; } diff --git a/src/Discord.Net/API/Client/Rest/DeleteChannel.cs b/src/Discord.Net/API/Rest/DeleteChannel.cs similarity index 69% rename from src/Discord.Net/API/Client/Rest/DeleteChannel.cs rename to src/Discord.Net/API/Rest/DeleteChannel.cs index ae56934b5..fa9b10c10 100644 --- a/src/Discord.Net/API/Client/Rest/DeleteChannel.cs +++ b/src/Discord.Net/API/Rest/DeleteChannel.cs @@ -1,15 +1,14 @@ -using Newtonsoft.Json; +using Discord.Net.Rest; -namespace Discord.API.Client.Rest +namespace Discord.API.Rest { - [JsonObject(MemberSerialization.OptIn)] public class DeleteChannelRequest : IRestRequest { string IRestRequest.Method => "DELETE"; string IRestRequest.Endpoint => $"channels/{ChannelId}"; object IRestRequest.Payload => null; - public ulong ChannelId { get; set; } + public ulong ChannelId { get; } public DeleteChannelRequest(ulong channelId) { diff --git a/src/Discord.Net/API/Rest/DeleteChannelPermission.cs b/src/Discord.Net/API/Rest/DeleteChannelPermission.cs new file mode 100644 index 000000000..658388641 --- /dev/null +++ b/src/Discord.Net/API/Rest/DeleteChannelPermission.cs @@ -0,0 +1,20 @@ +using Discord.Net.Rest; + +namespace Discord.API.Rest +{ + public class DeleteChannelPermissionsRequest : IRestRequest + { + string IRestRequest.Method => "DELETE"; + string IRestRequest.Endpoint => $"channels/{ChannelId}/permissions/{TargetId}"; + object IRestRequest.Payload => null; + + public ulong ChannelId { get; } + public ulong TargetId { get; } + + public DeleteChannelPermissionsRequest(ulong channelId, ulong targetId) + { + ChannelId = channelId; + TargetId = targetId; + } + } +} diff --git a/src/Discord.Net/API/Client/Rest/DeleteGuild.cs b/src/Discord.Net/API/Rest/DeleteGuild.cs similarity index 69% rename from src/Discord.Net/API/Client/Rest/DeleteGuild.cs rename to src/Discord.Net/API/Rest/DeleteGuild.cs index 44df5892e..d1a14fbc9 100644 --- a/src/Discord.Net/API/Client/Rest/DeleteGuild.cs +++ b/src/Discord.Net/API/Rest/DeleteGuild.cs @@ -1,15 +1,14 @@ -using Newtonsoft.Json; +using Discord.Net.Rest; -namespace Discord.API.Client.Rest +namespace Discord.API.Rest { - [JsonObject(MemberSerialization.OptIn)] public class DeleteGuildRequest : IRestRequest { string IRestRequest.Method => "DELETE"; string IRestRequest.Endpoint => $"guilds/{GuildId}"; object IRestRequest.Payload => null; - public ulong GuildId { get; set; } + public ulong GuildId { get; } public DeleteGuildRequest(ulong guildId) { diff --git a/src/Discord.Net/API/Rest/DeleteGuildIntegration.cs b/src/Discord.Net/API/Rest/DeleteGuildIntegration.cs new file mode 100644 index 000000000..b25418c1a --- /dev/null +++ b/src/Discord.Net/API/Rest/DeleteGuildIntegration.cs @@ -0,0 +1,20 @@ +using Discord.Net.Rest; + +namespace Discord.API.Rest +{ + public class DeleteGuildIntegrationRequest : IRestRequest + { + string IRestRequest.Method => "DELETE"; + string IRestRequest.Endpoint => $"guilds/{GuildId}/integrations/{IntegrationId}"; + object IRestRequest.Payload => null; + + public ulong GuildId { get; } + public ulong IntegrationId { get; } + + public DeleteGuildIntegrationRequest(ulong guildId, ulong integrationId) + { + GuildId = guildId; + IntegrationId = integrationId; + } + } +} diff --git a/src/Discord.Net/API/Rest/DeleteGuildRole.cs b/src/Discord.Net/API/Rest/DeleteGuildRole.cs new file mode 100644 index 000000000..cbb21eec3 --- /dev/null +++ b/src/Discord.Net/API/Rest/DeleteGuildRole.cs @@ -0,0 +1,20 @@ +using Discord.Net.Rest; + +namespace Discord.API.Rest +{ + public class DeleteGuildRoleRequest : IRestRequest + { + string IRestRequest.Method => "DELETE"; + string IRestRequest.Endpoint => $"guilds/{GuildId}/roles/{RoleId}"; + object IRestRequest.Payload => null; + + public ulong GuildId { get; } + public ulong RoleId { get; } + + public DeleteGuildRoleRequest(ulong guildId, ulong roleId) + { + GuildId = guildId; + RoleId = roleId; + } + } +} diff --git a/src/Discord.Net/API/Client/Rest/DeleteInvite.cs b/src/Discord.Net/API/Rest/DeleteInvite.cs similarity index 56% rename from src/Discord.Net/API/Client/Rest/DeleteInvite.cs rename to src/Discord.Net/API/Rest/DeleteInvite.cs index 4bfe1e0d7..388255862 100644 --- a/src/Discord.Net/API/Client/Rest/DeleteInvite.cs +++ b/src/Discord.Net/API/Rest/DeleteInvite.cs @@ -1,15 +1,14 @@ -using Newtonsoft.Json; +using Discord.Net.Rest; -namespace Discord.API.Client.Rest +namespace Discord.API.Rest { - [JsonObject(MemberSerialization.OptIn)] public class DeleteInviteRequest : IRestRequest { string IRestRequest.Method => "DELETE"; - string IRestRequest.Endpoint => $"invite/{InviteCode}"; + string IRestRequest.Endpoint => $"invites/{InviteCode}"; object IRestRequest.Payload => null; - public string InviteCode { get; set; } + public string InviteCode { get; } public DeleteInviteRequest(string inviteCode) { diff --git a/src/Discord.Net/API/Client/Rest/DeleteMessage.cs b/src/Discord.Net/API/Rest/DeleteMessage.cs similarity index 67% rename from src/Discord.Net/API/Client/Rest/DeleteMessage.cs rename to src/Discord.Net/API/Rest/DeleteMessage.cs index 3f781a756..c9d95deba 100644 --- a/src/Discord.Net/API/Client/Rest/DeleteMessage.cs +++ b/src/Discord.Net/API/Rest/DeleteMessage.cs @@ -1,16 +1,15 @@ -using Newtonsoft.Json; +using Discord.Net.Rest; -namespace Discord.API.Client.Rest +namespace Discord.API.Rest { - [JsonObject(MemberSerialization.OptIn)] public class DeleteMessageRequest : IRestRequest { string IRestRequest.Method => "DELETE"; string IRestRequest.Endpoint => $"channels/{ChannelId}/messages/{MessageId}"; object IRestRequest.Payload => null; - public ulong ChannelId { get; set; } - public ulong MessageId { get; set; } + public ulong ChannelId { get; } + public ulong MessageId { get; } public DeleteMessageRequest(ulong channelId, ulong messageId) { diff --git a/src/Discord.Net/API/Rest/GetChannel.cs b/src/Discord.Net/API/Rest/GetChannel.cs new file mode 100644 index 000000000..12d258836 --- /dev/null +++ b/src/Discord.Net/API/Rest/GetChannel.cs @@ -0,0 +1,18 @@ +using Discord.Net.Rest; + +namespace Discord.API.Rest +{ + public class GetChannelRequest : IRestRequest + { + string IRestRequest.Method => "GET"; + string IRestRequest.Endpoint => $"channels/{ChannelId}"; + object IRestRequest.Payload => null; + + public ulong ChannelId { get; } + + public GetChannelRequest(ulong channelId) + { + ChannelId = channelId; + } + } +} diff --git a/src/Discord.Net/API/Rest/GetChannelInvites.cs b/src/Discord.Net/API/Rest/GetChannelInvites.cs new file mode 100644 index 000000000..7532fb64f --- /dev/null +++ b/src/Discord.Net/API/Rest/GetChannelInvites.cs @@ -0,0 +1,20 @@ +using Discord.Net.Rest; +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization.OptIn)] + public class GetChannelInvitesRequest : IRestRequest + { + string IRestRequest.Method => "GET"; + string IRestRequest.Endpoint => $"channels/{ChannelId}/invites"; + object IRestRequest.Payload => null; + + public ulong ChannelId { get; } + + public GetChannelInvitesRequest(ulong channelId) + { + ChannelId = channelId; + } + } +} diff --git a/src/Discord.Net/API/Rest/GetChannelMessages.cs b/src/Discord.Net/API/Rest/GetChannelMessages.cs new file mode 100644 index 000000000..a0e6b5afe --- /dev/null +++ b/src/Discord.Net/API/Rest/GetChannelMessages.cs @@ -0,0 +1,34 @@ +using Discord.Net.Rest; +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization.OptIn)] + public class GetChannelMessagesRequest : IRestRequest + { + string IRestRequest.Method => "GET"; + string IRestRequest.Endpoint => $"channels/{ChannelId}/messages?limit={Limit}&{RelativeDir}={RelativeId}"; + object IRestRequest.Payload => null; + + public ulong ChannelId { get; } + + public Relative RelativeDir { get; set; } + public ulong RelativeId { get; set; } = 0; + + [JsonProperty("limit")] + public int Limit { get; set; } = 100; + + [JsonProperty("before")] + public ulong Before => RelativeId; + private bool ShouldSerializeBefore => RelativeDir == Relative.Before; + + [JsonProperty("after")] + public ulong After => RelativeId; + private bool ShouldSerializeAfter => RelativeDir == Relative.After; + + public GetChannelMessagesRequest(ulong channelId) + { + ChannelId = channelId; + } + } +} diff --git a/src/Discord.Net/API/Rest/GetCurrentUser.cs b/src/Discord.Net/API/Rest/GetCurrentUser.cs new file mode 100644 index 000000000..76bc3e838 --- /dev/null +++ b/src/Discord.Net/API/Rest/GetCurrentUser.cs @@ -0,0 +1,11 @@ +using Discord.Net.Rest; + +namespace Discord.API.Rest +{ + public class GetCurrentUserRequest : IRestRequest + { + string IRestRequest.Method => "GET"; + string IRestRequest.Endpoint => $"users/@me"; + object IRestRequest.Payload => null; + } +} diff --git a/src/Discord.Net/API/Rest/GetCurrentUserConnections.cs b/src/Discord.Net/API/Rest/GetCurrentUserConnections.cs new file mode 100644 index 000000000..102affaa7 --- /dev/null +++ b/src/Discord.Net/API/Rest/GetCurrentUserConnections.cs @@ -0,0 +1,11 @@ +using Discord.Net.Rest; + +namespace Discord.API.Rest +{ + public class GetCurrentUserConnectionsRequest : IRestRequest + { + string IRestRequest.Method => "GET"; + string IRestRequest.Endpoint => $"users/@me/connections"; + object IRestRequest.Payload => null; + } +} diff --git a/src/Discord.Net/API/Rest/GetCurrentUserDMs.cs b/src/Discord.Net/API/Rest/GetCurrentUserDMs.cs new file mode 100644 index 000000000..31cc1edeb --- /dev/null +++ b/src/Discord.Net/API/Rest/GetCurrentUserDMs.cs @@ -0,0 +1,11 @@ +using Discord.Net.Rest; + +namespace Discord.API.Rest +{ + public class GetCurrentUserDMsRequest : IRestRequest + { + string IRestRequest.Method => "GET"; + string IRestRequest.Endpoint => $"users/@me/channels"; + object IRestRequest.Payload => null; + } +} diff --git a/src/Discord.Net/API/Rest/GetCurrentUserGuilds.cs b/src/Discord.Net/API/Rest/GetCurrentUserGuilds.cs new file mode 100644 index 000000000..e3423c3f8 --- /dev/null +++ b/src/Discord.Net/API/Rest/GetCurrentUserGuilds.cs @@ -0,0 +1,11 @@ +using Discord.Net.Rest; + +namespace Discord.API.Rest +{ + public class GetCurrentUserGuildsRequest : IRestRequest + { + string IRestRequest.Method => "GET"; + string IRestRequest.Endpoint => $"users/@me/guilds"; + object IRestRequest.Payload => null; + } +} diff --git a/src/Discord.Net/API/Rest/GetGateway.cs b/src/Discord.Net/API/Rest/GetGateway.cs new file mode 100644 index 000000000..3581f9f00 --- /dev/null +++ b/src/Discord.Net/API/Rest/GetGateway.cs @@ -0,0 +1,18 @@ +using Discord.Net.Rest; +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + public class GetGatewayRequest : IRestRequest + { + string IRestRequest.Method => "GET"; + string IRestRequest.Endpoint => $"gateway"; + object IRestRequest.Payload => null; + } + + public class GetGatewayResponse + { + [JsonProperty("url")] + public string Url { get; set; } + } +} diff --git a/src/Discord.Net/API/Rest/GetGuild.cs b/src/Discord.Net/API/Rest/GetGuild.cs new file mode 100644 index 000000000..2e83d261a --- /dev/null +++ b/src/Discord.Net/API/Rest/GetGuild.cs @@ -0,0 +1,18 @@ +using Discord.Net.Rest; + +namespace Discord.API.Rest +{ + public class GetGuildRequest : IRestRequest + { + string IRestRequest.Method => "GET"; + string IRestRequest.Endpoint => $"guilds/{GuildId}"; + object IRestRequest.Payload => null; + + public ulong GuildId { get; } + + public GetGuildRequest(ulong guildId) + { + GuildId = guildId; + } + } +} diff --git a/src/Discord.Net/API/Rest/GetGuildBans.cs b/src/Discord.Net/API/Rest/GetGuildBans.cs new file mode 100644 index 000000000..73a02b43e --- /dev/null +++ b/src/Discord.Net/API/Rest/GetGuildBans.cs @@ -0,0 +1,18 @@ +using Discord.Net.Rest; + +namespace Discord.API.Rest +{ + public class GetGuildBansRequest : IRestRequest + { + string IRestRequest.Method => "GET"; + string IRestRequest.Endpoint => $"guilds/{GuildId}/bans"; + object IRestRequest.Payload => null; + + public ulong GuildId { get; } + + public GetGuildBansRequest(ulong guildId) + { + GuildId = guildId; + } + } +} diff --git a/src/Discord.Net/API/Rest/GetGuildChannels.cs b/src/Discord.Net/API/Rest/GetGuildChannels.cs new file mode 100644 index 000000000..f20e9ec4f --- /dev/null +++ b/src/Discord.Net/API/Rest/GetGuildChannels.cs @@ -0,0 +1,18 @@ +using Discord.Net.Rest; + +namespace Discord.API.Rest +{ + public class GetGuildChannelsRequest : IRestRequest + { + string IRestRequest.Method => "GET"; + string IRestRequest.Endpoint => $"guild/{GuildId}/channels"; + object IRestRequest.Payload => null; + + public ulong GuildId { get; } + + public GetGuildChannelsRequest(ulong guildId) + { + GuildId = guildId; + } + } +} diff --git a/src/Discord.Net/API/Rest/GetGuildEmbed.cs b/src/Discord.Net/API/Rest/GetGuildEmbed.cs new file mode 100644 index 000000000..0eb67e680 --- /dev/null +++ b/src/Discord.Net/API/Rest/GetGuildEmbed.cs @@ -0,0 +1,18 @@ +using Discord.Net.Rest; + +namespace Discord.API.Rest +{ + public class GetGuildEmbedRequest : IRestRequest + { + string IRestRequest.Method => "GET"; + string IRestRequest.Endpoint => $"guilds/{GuildId}/embed"; + object IRestRequest.Payload => null; + + public ulong GuildId { get; } + + public GetGuildEmbedRequest(ulong guildId) + { + GuildId = guildId; + } + } +} diff --git a/src/Discord.Net/API/Rest/GetGuildIntegrations.cs b/src/Discord.Net/API/Rest/GetGuildIntegrations.cs new file mode 100644 index 000000000..ba2b1ce2d --- /dev/null +++ b/src/Discord.Net/API/Rest/GetGuildIntegrations.cs @@ -0,0 +1,18 @@ +using Discord.Net.Rest; + +namespace Discord.API.Rest +{ + public class GetGuildIntegrationsRequest : IRestRequest + { + string IRestRequest.Method => "GET"; + string IRestRequest.Endpoint => $"guilds/{GuildId}/integrations"; + object IRestRequest.Payload => null; + + public ulong GuildId { get; } + + public GetGuildIntegrationsRequest(ulong guildId) + { + GuildId = guildId; + } + } +} diff --git a/src/Discord.Net/API/Rest/GetGuildInvites.cs b/src/Discord.Net/API/Rest/GetGuildInvites.cs new file mode 100644 index 000000000..4a20770b1 --- /dev/null +++ b/src/Discord.Net/API/Rest/GetGuildInvites.cs @@ -0,0 +1,18 @@ +using Discord.Net.Rest; + +namespace Discord.API.Rest +{ + public class GetGuildInvitesRequest : IRestRequest + { + string IRestRequest.Method => "GET"; + string IRestRequest.Endpoint => $"guilds/{GuildId}/invites"; + object IRestRequest.Payload => null; + + public ulong GuildId { get; } + + public GetGuildInvitesRequest(ulong guildId) + { + GuildId = guildId; + } + } +} diff --git a/src/Discord.Net/API/Rest/GetGuildMember.cs b/src/Discord.Net/API/Rest/GetGuildMember.cs new file mode 100644 index 000000000..0dc1d8077 --- /dev/null +++ b/src/Discord.Net/API/Rest/GetGuildMember.cs @@ -0,0 +1,20 @@ +using Discord.Net.Rest; + +namespace Discord.API.Rest +{ + public class GetGuildMemberRequest : IRestRequest + { + string IRestRequest.Method => "GET"; + string IRestRequest.Endpoint => $"guilds/{GuildId}/members/{UserId}"; + object IRestRequest.Payload => null; + + public ulong GuildId { get; } + public ulong UserId { get; } + + public GetGuildMemberRequest(ulong guildId, ulong userId) + { + GuildId = guildId; + UserId = userId; + } + } +} diff --git a/src/Discord.Net/API/Rest/GetGuildPruneCount.cs b/src/Discord.Net/API/Rest/GetGuildPruneCount.cs new file mode 100644 index 000000000..469b5fa01 --- /dev/null +++ b/src/Discord.Net/API/Rest/GetGuildPruneCount.cs @@ -0,0 +1,29 @@ +using Discord.Net.Rest; +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization.OptIn)] + public class GetGuildPruneCountRequest : IRestRequest + { + string IRestRequest.Method => "GET"; + string IRestRequest.Endpoint => $"guilds/{GuildId}/prune"; + object IRestRequest.Payload => null; + + public ulong GuildId { get; } + + [JsonProperty("days")] + public int Days { get; set; } = 30; + + public GetGuildPruneCountRequest(ulong guildId) + { + GuildId = guildId; + } + } + + public class GetGuildPruneCountResponse + { + [JsonProperty("pruned")] + public int Pruned { get; set; } + } +} diff --git a/src/Discord.Net/API/Rest/GetGuildRoles.cs b/src/Discord.Net/API/Rest/GetGuildRoles.cs new file mode 100644 index 000000000..f8ff0a9b3 --- /dev/null +++ b/src/Discord.Net/API/Rest/GetGuildRoles.cs @@ -0,0 +1,18 @@ +using Discord.Net.Rest; + +namespace Discord.API.Rest +{ + public class GetGuildRolesRequest : IRestRequest + { + string IRestRequest.Method => "GET"; + string IRestRequest.Endpoint => $"guild/{GuildId}/roles"; + object IRestRequest.Payload => null; + + public ulong GuildId { get; } + + public GetGuildRolesRequest(ulong guildId) + { + GuildId = guildId; + } + } +} diff --git a/src/Discord.Net/API/Rest/GetGuildVoiceRegions.cs b/src/Discord.Net/API/Rest/GetGuildVoiceRegions.cs new file mode 100644 index 000000000..ed631e7e2 --- /dev/null +++ b/src/Discord.Net/API/Rest/GetGuildVoiceRegions.cs @@ -0,0 +1,18 @@ +using Discord.Net.Rest; + +namespace Discord.API.Rest +{ + public class GetGuildVoiceRegionsRequest : IRestRequest + { + string IRestRequest.Method => "GET"; + string IRestRequest.Endpoint => $"guilds/{GuildId}/regions"; + object IRestRequest.Payload => null; + + public ulong GuildId { get; } + + public GetGuildVoiceRegionsRequest(ulong guildId) + { + GuildId = guildId; + } + } +} diff --git a/src/Discord.Net/API/Rest/GetInvite.cs b/src/Discord.Net/API/Rest/GetInvite.cs new file mode 100644 index 000000000..fccbc79c9 --- /dev/null +++ b/src/Discord.Net/API/Rest/GetInvite.cs @@ -0,0 +1,18 @@ +using Discord.Net.Rest; + +namespace Discord.API.Rest +{ + public class GetInviteRequest : IRestRequest + { + string IRestRequest.Method => "GET"; + string IRestRequest.Endpoint => $"invites/{InviteCode}"; + object IRestRequest.Payload => null; + + public string InviteCode { get; } + + public GetInviteRequest(string inviteCode) + { + InviteCode = inviteCode; + } + } +} diff --git a/src/Discord.Net/API/Rest/GetUser.cs b/src/Discord.Net/API/Rest/GetUser.cs new file mode 100644 index 000000000..1cfeb82be --- /dev/null +++ b/src/Discord.Net/API/Rest/GetUser.cs @@ -0,0 +1,18 @@ +using Discord.Net.Rest; + +namespace Discord.API.Rest +{ + public class GetUserRequest : IRestRequest + { + string IRestRequest.Method => "GET"; + string IRestRequest.Endpoint => $"users/{UserId}"; + object IRestRequest.Payload => null; + + public ulong UserId { get; } + + public GetUserRequest(ulong userId) + { + UserId = userId; + } + } +} diff --git a/src/Discord.Net/API/Client/Rest/GetVoiceRegions.cs b/src/Discord.Net/API/Rest/GetVoiceRegions.cs similarity index 65% rename from src/Discord.Net/API/Client/Rest/GetVoiceRegions.cs rename to src/Discord.Net/API/Rest/GetVoiceRegions.cs index df21cc203..5fdbccfd8 100644 --- a/src/Discord.Net/API/Client/Rest/GetVoiceRegions.cs +++ b/src/Discord.Net/API/Rest/GetVoiceRegions.cs @@ -1,8 +1,8 @@ -using Newtonsoft.Json; +using Discord.Net.Rest; +using Newtonsoft.Json; -namespace Discord.API.Client.Rest +namespace Discord.API.Rest { - [JsonObject(MemberSerialization.OptIn)] public class GetVoiceRegionsRequest : IRestRequest { string IRestRequest.Method => "GET"; @@ -12,15 +12,17 @@ namespace Discord.API.Client.Rest public class GetVoiceRegionsResponse { - [JsonProperty("sample_hostname")] - public string Hostname { get; set; } - [JsonProperty("sample_port")] - public int Port { get; set; } [JsonProperty("id")] public string Id { get; set; } [JsonProperty("name")] public string Name { get; set; } [JsonProperty("vip")] - public bool Vip { get; set; } + public bool IsVip { get; set; } + [JsonProperty("optimal")] + public bool IsOptimal { get; set; } + [JsonProperty("sample_hostname")] + public string SampleHostname { get; set; } + [JsonProperty("sample_port")] + public int SamplePort { get; set; } } } diff --git a/src/Discord.Net/API/Client/Rest/LeaveGuild.cs b/src/Discord.Net/API/Rest/LeaveGuild.cs similarity index 69% rename from src/Discord.Net/API/Client/Rest/LeaveGuild.cs rename to src/Discord.Net/API/Rest/LeaveGuild.cs index 99fd8cbe7..a62fdd089 100644 --- a/src/Discord.Net/API/Client/Rest/LeaveGuild.cs +++ b/src/Discord.Net/API/Rest/LeaveGuild.cs @@ -1,15 +1,14 @@ -using Newtonsoft.Json; +using Discord.Net.Rest; -namespace Discord.API.Client.Rest +namespace Discord.API.Rest { - [JsonObject(MemberSerialization.OptIn)] public class LeaveGuildRequest : IRestRequest { string IRestRequest.Method => "DELETE"; string IRestRequest.Endpoint => $"users/@me/guilds/{GuildId}"; object IRestRequest.Payload => null; - public ulong GuildId { get; set; } + public ulong GuildId { get; } public LeaveGuildRequest(ulong guildId) { diff --git a/src/Discord.Net/API/Rest/ListGuildMembers.cs b/src/Discord.Net/API/Rest/ListGuildMembers.cs new file mode 100644 index 000000000..4577b8910 --- /dev/null +++ b/src/Discord.Net/API/Rest/ListGuildMembers.cs @@ -0,0 +1,24 @@ +using Discord.Net.Rest; +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + public class ListGuildMembersRequest : IRestRequest + { + string IRestRequest.Method => "GET"; + string IRestRequest.Endpoint => $"guild/{GuildId}/members"; + object IRestRequest.Payload => null; + + public ulong GuildId { get; } + + [JsonProperty("limit")] + public int Limit { get; } = 1; + [JsonProperty("offset")] + public int Offset { get; } = 0; + + public ListGuildMembersRequest(ulong guildId) + { + GuildId = guildId; + } + } +} diff --git a/src/Discord.Net/API/Client/Rest/AddChannelPermission.cs b/src/Discord.Net/API/Rest/ModifyChannelPermission.cs similarity index 50% rename from src/Discord.Net/API/Client/Rest/AddChannelPermission.cs rename to src/Discord.Net/API/Rest/ModifyChannelPermission.cs index bf725bcaf..e38685bb7 100644 --- a/src/Discord.Net/API/Client/Rest/AddChannelPermission.cs +++ b/src/Discord.Net/API/Rest/ModifyChannelPermission.cs @@ -1,29 +1,27 @@ -using Discord.API.Converters; +using Discord.Net.Rest; using Newtonsoft.Json; -namespace Discord.API.Client.Rest +namespace Discord.API.Rest { [JsonObject(MemberSerialization.OptIn)] - public class AddOrUpdateChannelPermissionsRequest : IRestRequest + public class ModifyChannelPermissionsRequest : IRestRequest { string IRestRequest.Method => "PUT"; string IRestRequest.Endpoint => $"channels/{ChannelId}/permissions/{TargetId}"; object IRestRequest.Payload => this; - public ulong ChannelId { get; set; } + public ulong ChannelId { get; } + public ulong TargetId { get; } - [JsonProperty("id"), JsonConverter(typeof(LongStringConverter))] - public ulong TargetId { get; set; } - [JsonProperty("type")] - public string TargetType { get; set; } [JsonProperty("allow")] public uint Allow { get; set; } [JsonProperty("deny")] public uint Deny { get; set; } - public AddOrUpdateChannelPermissionsRequest(ulong channelId) + public ModifyChannelPermissionsRequest(ulong channelId, ulong targetId) { ChannelId = channelId; + TargetId = targetId; } } } diff --git a/src/Discord.Net/API/Client/Rest/UpdateProfile.cs b/src/Discord.Net/API/Rest/ModifyCurrentUser.cs similarity index 57% rename from src/Discord.Net/API/Client/Rest/UpdateProfile.cs rename to src/Discord.Net/API/Rest/ModifyCurrentUser.cs index 0f0cdb313..afc1c3b74 100644 --- a/src/Discord.Net/API/Client/Rest/UpdateProfile.cs +++ b/src/Discord.Net/API/Rest/ModifyCurrentUser.cs @@ -1,23 +1,26 @@ -using Newtonsoft.Json; +using Discord.Net.JsonConverters; +using Discord.Net.Rest; +using Newtonsoft.Json; +using System.IO; -namespace Discord.API.Client.Rest +namespace Discord.API.Rest { [JsonObject(MemberSerialization.OptIn)] - public class UpdateProfileRequest : IRestRequest + public class ModifyCurrentUserRequest : IRestRequest { string IRestRequest.Method => "PATCH"; string IRestRequest.Endpoint => $"users/@me"; object IRestRequest.Payload => this; - [JsonProperty("password")] - public string CurrentPassword { get; set; } + [JsonProperty("username")] + public string Username { get; set; } [JsonProperty("email")] public string Email { get; set; } - [JsonProperty("new_password")] + [JsonProperty("password")] public string Password { get; set; } - [JsonProperty("username")] - public string Username { get; set; } - [JsonProperty("avatar")] - public string AvatarBase64 { get; set; } + [JsonProperty("new_password")] + public string NewPassword { get; set; } + [JsonProperty("avatar"), JsonConverter(typeof(ImageConverter))] + public Stream Avatar { get; set; } } } diff --git a/src/Discord.Net/API/Rest/ModifyGuild.cs b/src/Discord.Net/API/Rest/ModifyGuild.cs new file mode 100644 index 000000000..1405a8144 --- /dev/null +++ b/src/Discord.Net/API/Rest/ModifyGuild.cs @@ -0,0 +1,39 @@ +using Discord.Net.JsonConverters; +using Discord.Net.Rest; +using Newtonsoft.Json; +using System.IO; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization.OptIn)] + public class ModifyGuildRequest : IRestRequest + { + string IRestRequest.Method => "PATCH"; + string IRestRequest.Endpoint => $"guilds/{GuildId}"; + object IRestRequest.Payload => this; + + public ulong GuildId { get; } + + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("region")] + public VoiceRegion Region { get; set; } + [JsonProperty("verification_level")] + public int VerificationLevel { get; set; } + [JsonProperty("afk_channel_id")] + public ulong? AFKChannelId { get; set; } + [JsonProperty("afk_timeout")] + public int AFKTimeout { get; set; } + [JsonProperty("icon"), JsonConverter(typeof(ImageConverter))] + public Stream Icon { get; set; } + [JsonProperty("owner_id")] + public GuildPresence Owner { get; set; } + [JsonProperty("splash"), JsonConverter(typeof(ImageConverter))] + public Stream Splash { get; set; } + + public ModifyGuildRequest(ulong guildId) + { + GuildId = guildId; + } + } +} diff --git a/src/Discord.Net/API/Client/Rest/UpdateChannel.cs b/src/Discord.Net/API/Rest/ModifyGuildChannel.cs similarity index 59% rename from src/Discord.Net/API/Client/Rest/UpdateChannel.cs rename to src/Discord.Net/API/Rest/ModifyGuildChannel.cs index 8a82caefd..eeb909e84 100644 --- a/src/Discord.Net/API/Client/Rest/UpdateChannel.cs +++ b/src/Discord.Net/API/Rest/ModifyGuildChannel.cs @@ -1,9 +1,10 @@ -using Newtonsoft.Json; +using Discord.Net.Rest; +using Newtonsoft.Json; -namespace Discord.API.Client.Rest +namespace Discord.API.Rest { [JsonObject(MemberSerialization.OptIn)] - public class UpdateChannelRequest : IRestRequest + public class ModifyGuildChannelRequest : IRestRequest { string IRestRequest.Method => "PATCH"; string IRestRequest.Endpoint => $"channels/{ChannelId}"; @@ -13,14 +14,10 @@ namespace Discord.API.Client.Rest [JsonProperty("name")] public string Name { get; set; } - [JsonProperty("topic")] - public string Topic { get; set; } [JsonProperty("position")] public int Position { get; set; } - [JsonProperty("bitrate")] - public int Bitrate { get; set; } - public UpdateChannelRequest(ulong channelId) + public ModifyGuildChannelRequest(ulong channelId) { ChannelId = channelId; } diff --git a/src/Discord.Net/API/Rest/ModifyGuildChannels.cs b/src/Discord.Net/API/Rest/ModifyGuildChannels.cs new file mode 100644 index 000000000..de9b97b88 --- /dev/null +++ b/src/Discord.Net/API/Rest/ModifyGuildChannels.cs @@ -0,0 +1,22 @@ +using Discord.Net.Rest; +using Newtonsoft.Json; +using System; + +namespace Discord.API.Rest +{ + public class ModifyGuildChannelsRequest : IRestRequest + { + string IRestRequest.Method => "PATCH"; + string IRestRequest.Endpoint => $"guilds/{GuildId}/channels"; + object IRestRequest.Payload => Requests; + + public ulong GuildId { get; } + + public ModifyGuildChannelRequest[] Requests { get; set; } = Array.Empty(); + + public ModifyGuildChannelsRequest(ulong guildId) + { + GuildId = guildId; + } + } +} diff --git a/src/Discord.Net/API/Rest/ModifyGuildEmbed.cs b/src/Discord.Net/API/Rest/ModifyGuildEmbed.cs new file mode 100644 index 000000000..c896e5a34 --- /dev/null +++ b/src/Discord.Net/API/Rest/ModifyGuildEmbed.cs @@ -0,0 +1,25 @@ +using Discord.Net.Rest; +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization.OptIn)] + public class ModifyGuildEmbedRequest : IRestRequest + { + string IRestRequest.Method => "PATCH"; + string IRestRequest.Endpoint => $"guilds/{GuildId}/embed"; + object IRestRequest.Payload => this; + + public ulong GuildId { get; } + + [JsonProperty("enabled")] + public bool Enabled { get; set; } + [JsonProperty("channel_id")] + public ulong? ChannelId { get; set; } + + public ModifyGuildEmbedRequest(ulong guildId) + { + GuildId = guildId; + } + } +} diff --git a/src/Discord.Net/API/Rest/ModifyGuildIntegration.cs b/src/Discord.Net/API/Rest/ModifyGuildIntegration.cs new file mode 100644 index 000000000..b093888da --- /dev/null +++ b/src/Discord.Net/API/Rest/ModifyGuildIntegration.cs @@ -0,0 +1,29 @@ +using Discord.Net.Rest; +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization.OptIn)] + public class ModifyGuildIntegrationRequest : IRestRequest + { + string IRestRequest.Method => "POST"; + string IRestRequest.Endpoint => $"guilds/{GuildId}/integrations/{IntegrationId}"; + object IRestRequest.Payload => this; + + public ulong GuildId { get; } + public ulong IntegrationId { get; } + + [JsonProperty("expire_behavior")] + public int ExpireBehavior { get; set; } + [JsonProperty("expire_grace_period")] + public int ExpireGracePeriod { get; set; } + [JsonProperty("enable_emoticons")] + public bool EnableEmoticons { get; set; } + + public ModifyGuildIntegrationRequest(ulong guildId, ulong integrationId) + { + GuildId = guildId; + IntegrationId = integrationId; + } + } +} diff --git a/src/Discord.Net/API/Client/Rest/UpdateMember.cs b/src/Discord.Net/API/Rest/ModifyGuildMember.cs similarity index 50% rename from src/Discord.Net/API/Client/Rest/UpdateMember.cs rename to src/Discord.Net/API/Rest/ModifyGuildMember.cs index ce1649bdd..07a662645 100644 --- a/src/Discord.Net/API/Client/Rest/UpdateMember.cs +++ b/src/Discord.Net/API/Rest/ModifyGuildMember.cs @@ -1,7 +1,7 @@ -using Discord.API.Converters; +using Discord.Net.Rest; using Newtonsoft.Json; -namespace Discord.API.Client.Rest +namespace Discord.API.Rest { [JsonObject(MemberSerialization.OptIn)] public class UpdateMemberRequest : IRestRequest @@ -10,17 +10,17 @@ namespace Discord.API.Client.Rest string IRestRequest.Endpoint => $"guilds/{GuildId}/members/{UserId}"; object IRestRequest.Payload => this; - public ulong GuildId { get; set; } - public ulong UserId { get; set; } + public ulong GuildId { get; } + public ulong UserId { get; } + [JsonProperty("roles")] + public ulong[] Roles { get; set; } [JsonProperty("mute")] - public bool IsMuted { get; set; } + public bool Mute { get; set; } [JsonProperty("deaf")] - public bool IsDeafened { get; set; } - [JsonProperty("channel_id"), JsonConverter(typeof(NullableLongStringConverter))] - public ulong? VoiceChannelId { get; set; } - [JsonProperty("roles"), JsonConverter(typeof(LongStringArrayConverter))] - public ulong[] RoleIds { get; set; } + public bool Deaf { get; set; } + [JsonProperty("channel_id")] + public ulong? ChannelId { get; set; } public UpdateMemberRequest(ulong guildId, ulong userId) { diff --git a/src/Discord.Net/API/Rest/ModifyGuildRole.cs b/src/Discord.Net/API/Rest/ModifyGuildRole.cs new file mode 100644 index 000000000..6d7e09c59 --- /dev/null +++ b/src/Discord.Net/API/Rest/ModifyGuildRole.cs @@ -0,0 +1,32 @@ +using Discord.Net.Rest; +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + public class ModifyGuildRoleRequest : IRestRequest + { + string IRestRequest.Method => "PATCH"; + string IRestRequest.Endpoint => $"guilds/{GuildId}/roles/{RoleId}"; + object IRestRequest.Payload => this; + + public ulong GuildId { get; } + public ulong RoleId { get; } + + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("permissions")] + public int Permissions { get; set; } + [JsonProperty("position")] + public int Position { get; set; } + [JsonProperty("color")] + public int Color { get; set; } + [JsonProperty("hoist")] + public bool Hoist { get; set; } + + public ModifyGuildRoleRequest(ulong guildId, ulong roleId) + { + GuildId = guildId; + RoleId = roleId; + } + } +} diff --git a/src/Discord.Net/API/Rest/ModifyGuildRoles.cs b/src/Discord.Net/API/Rest/ModifyGuildRoles.cs new file mode 100644 index 000000000..905f51b11 --- /dev/null +++ b/src/Discord.Net/API/Rest/ModifyGuildRoles.cs @@ -0,0 +1,21 @@ +using Discord.Net.Rest; +using System; + +namespace Discord.API.Rest +{ + public class ModifyGuildRolesRequest : IRestRequest + { + string IRestRequest.Method => "PATCH"; + string IRestRequest.Endpoint => $"guilds/{GuildId}/roles"; + object IRestRequest.Payload => Requests; + + public ulong GuildId { get; } + + public ModifyGuildRoleRequest[] Requests { get; set; } = Array.Empty(); + + public ModifyGuildRolesRequest(ulong guildId) + { + GuildId = guildId; + } + } +} diff --git a/src/Discord.Net/API/Client/Rest/UpdateMessage.cs b/src/Discord.Net/API/Rest/ModifyMessage.cs similarity index 77% rename from src/Discord.Net/API/Client/Rest/UpdateMessage.cs rename to src/Discord.Net/API/Rest/ModifyMessage.cs index fc055b2bc..0c99403a6 100644 --- a/src/Discord.Net/API/Client/Rest/UpdateMessage.cs +++ b/src/Discord.Net/API/Rest/ModifyMessage.cs @@ -1,6 +1,7 @@ -using Newtonsoft.Json; +using Discord.Net.Rest; +using Newtonsoft.Json; -namespace Discord.API.Client.Rest +namespace Discord.API.Rest { [JsonObject(MemberSerialization.OptIn)] public class UpdateMessageRequest : IRestRequest @@ -9,8 +10,8 @@ namespace Discord.API.Client.Rest string IRestRequest.Endpoint => $"channels/{ChannelId}/messages/{MessageId}"; object IRestRequest.Payload => this; - public ulong ChannelId { get; set; } - public ulong MessageId { get; set; } + public ulong ChannelId { get; } + public ulong MessageId { get; } [JsonProperty("content")] public string Content { get; set; } = ""; diff --git a/src/Discord.Net/API/Rest/ModifyTextChannel.cs b/src/Discord.Net/API/Rest/ModifyTextChannel.cs new file mode 100644 index 000000000..f3d672daf --- /dev/null +++ b/src/Discord.Net/API/Rest/ModifyTextChannel.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization.OptIn)] + public class ModifyTextChannelRequest : ModifyGuildChannelRequest + { + [JsonProperty("topic")] + public string Topic { get; set; } + + public ModifyTextChannelRequest(ulong channelId) + : base(channelId) + { + } + } +} diff --git a/src/Discord.Net/API/Rest/ModifyVoiceChannel.cs b/src/Discord.Net/API/Rest/ModifyVoiceChannel.cs new file mode 100644 index 000000000..e8bdfac8f --- /dev/null +++ b/src/Discord.Net/API/Rest/ModifyVoiceChannel.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + [JsonObject(MemberSerialization.OptIn)] + public class ModifyVoiceChannelRequest : ModifyGuildChannelRequest + { + [JsonProperty("bitrate")] + public int Bitrate { get; set; } + + public ModifyVoiceChannelRequest(ulong channelId) + : base(channelId) + { + } + } +} diff --git a/src/Discord.Net/API/Rest/QueryUser.cs b/src/Discord.Net/API/Rest/QueryUser.cs new file mode 100644 index 000000000..45a194805 --- /dev/null +++ b/src/Discord.Net/API/Rest/QueryUser.cs @@ -0,0 +1,19 @@ +using Discord.Net.Rest; +using System; + +namespace Discord.API.Rest +{ + public class QueryUserRequest : IRestRequest + { + string IRestRequest.Method => "GET"; + string IRestRequest.Endpoint => $"users?q={Uri.EscapeDataString(Query)}&limit={Limit}"; + object IRestRequest.Payload => null; + + public string Query { get; set; } + public int Limit { get; set; } = 25; + + public QueryUserRequest() + { + } + } +} diff --git a/src/Discord.Net/API/Client/Rest/RemoveGuildBan.cs b/src/Discord.Net/API/Rest/RemoveGuildBan.cs similarity index 67% rename from src/Discord.Net/API/Client/Rest/RemoveGuildBan.cs rename to src/Discord.Net/API/Rest/RemoveGuildBan.cs index 5a8f4f796..4f5df1243 100644 --- a/src/Discord.Net/API/Client/Rest/RemoveGuildBan.cs +++ b/src/Discord.Net/API/Rest/RemoveGuildBan.cs @@ -1,16 +1,15 @@ -using Newtonsoft.Json; +using Discord.Net.Rest; -namespace Discord.API.Client.Rest +namespace Discord.API.Rest { - [JsonObject(MemberSerialization.OptIn)] public class RemoveGuildBanRequest : IRestRequest { string IRestRequest.Method => "DELETE"; string IRestRequest.Endpoint => $"guilds/{GuildId}/bans/{UserId}"; object IRestRequest.Payload => null; - public ulong GuildId { get; set; } - public ulong UserId { get; set; } + public ulong GuildId { get; } + public ulong UserId { get; } public RemoveGuildBanRequest(ulong guildId, ulong userId) { diff --git a/src/Discord.Net/API/Rest/RemoveGuildMember.cs b/src/Discord.Net/API/Rest/RemoveGuildMember.cs new file mode 100644 index 000000000..a5b39b1db --- /dev/null +++ b/src/Discord.Net/API/Rest/RemoveGuildMember.cs @@ -0,0 +1,20 @@ +using Discord.Net.Rest; + +namespace Discord.API.Rest +{ + public class RemoveGuildMemberRequest : IRestRequest + { + string IRestRequest.Method => "DELETE"; + string IRestRequest.Endpoint => $"guilds/{GuildId}/members/{UserId}"; + object IRestRequest.Payload => null; + + public ulong GuildId { get; } + public ulong UserId { get; } + + public RemoveGuildMemberRequest(ulong guildId, ulong userId) + { + GuildId = guildId; + UserId = userId; + } + } +} diff --git a/src/Discord.Net/API/Rest/SyncGuildIntegration.cs b/src/Discord.Net/API/Rest/SyncGuildIntegration.cs new file mode 100644 index 000000000..4c7f0acfa --- /dev/null +++ b/src/Discord.Net/API/Rest/SyncGuildIntegration.cs @@ -0,0 +1,21 @@ +using Discord.Net.Rest; +using Newtonsoft.Json; + +namespace Discord.API.Rest +{ + public class SyncGuildIntegrationRequest : IRestRequest + { + string IRestRequest.Method => "POST"; + string IRestRequest.Endpoint => $"guilds/{GuildId}/integrations/{IntegrationId}/sync"; + object IRestRequest.Payload => null; + + public ulong GuildId { get; } + public ulong IntegrationId { get; } + + public SyncGuildIntegrationRequest(ulong guildId, ulong integrationId) + { + GuildId = guildId; + IntegrationId = integrationId; + } + } +} diff --git a/src/Discord.Net/API/Rest/TriggerTypingIndicator.cs b/src/Discord.Net/API/Rest/TriggerTypingIndicator.cs new file mode 100644 index 000000000..3c0baa855 --- /dev/null +++ b/src/Discord.Net/API/Rest/TriggerTypingIndicator.cs @@ -0,0 +1,18 @@ +using Discord.Net.Rest; + +namespace Discord.API.Rest +{ + public class TriggerTypingIndicatorRequest : IRestRequest + { + string IRestRequest.Method => "POST"; + string IRestRequest.Endpoint => $"channels/{ChannelId}/typing"; + object IRestRequest.Payload => null; + + public ulong ChannelId { get; } + + public TriggerTypingIndicatorRequest(ulong channelId) + { + ChannelId = channelId; + } + } +} diff --git a/src/Discord.Net/API/Client/Rest/SendFile.cs b/src/Discord.Net/API/Rest/Unconfirmed/SendFile.cs similarity index 50% rename from src/Discord.Net/API/Client/Rest/SendFile.cs rename to src/Discord.Net/API/Rest/Unconfirmed/SendFile.cs index 4b59e1a11..d7252e7d4 100644 --- a/src/Discord.Net/API/Client/Rest/SendFile.cs +++ b/src/Discord.Net/API/Rest/Unconfirmed/SendFile.cs @@ -1,21 +1,31 @@ -using Newtonsoft.Json; +using Discord.Net.Rest; +using System.Collections.Generic; +using System.Collections.Immutable; using System.IO; -namespace Discord.API.Client.Rest +namespace Discord.API.Rest { - [JsonObject(MemberSerialization.OptIn)] public class SendFileRequest : IRestFileRequest { string IRestRequest.Method => "POST"; string IRestRequest.Endpoint => $"channels/{ChannelId}/messages"; object IRestRequest.Payload => null; + string IRestFileRequest.Filename => Filename; Stream IRestFileRequest.Stream => Stream; + IReadOnlyList IRestFileRequest.MultipartParameters => ImmutableArray.Create( + new RestParameter("content", Content), + new RestParameter("nonce", Nonce), + new RestParameter("tts", IsTTS) + ); - public ulong ChannelId { get; set; } + public ulong ChannelId { get; } public string Filename { get; set; } public Stream Stream { get; set; } + public string Content { get; set; } + public string Nonce { get; set; } + public bool IsTTS { get; set; } public SendFileRequest(ulong channelId) { diff --git a/src/Discord.Net/API/Status/Common/StatusResult.cs b/src/Discord.Net/API/Status/Common/StatusResult.cs deleted file mode 100644 index 74728c578..000000000 --- a/src/Discord.Net/API/Status/Common/StatusResult.cs +++ /dev/null @@ -1,80 +0,0 @@ -using Newtonsoft.Json; -using System; - -namespace Discord.API.Status -{ - public class StatusResult - { - public class PageData - { - [JsonProperty("id")] - public string Id { get; set; } - [JsonProperty("name")] - public string Name { get; set; } - [JsonProperty("url")] - public string Url { get; set; } - [JsonProperty("updated_at")] - public DateTime? UpdatedAt { get; set; } - } - - public class IncidentData - { - [JsonProperty("id")] - public string Id { get; set; } - [JsonProperty("page_id")] - public string PageId { get; set; } - - [JsonProperty("name")] - public string Name { get; set; } - [JsonProperty("status")] - public string Status { get; set; } - [JsonProperty("shortlink")] - public string Shortlink { get; set; } - [JsonProperty("impact")] - public string Impact { get; set; } - - [JsonProperty("created_at")] - public DateTime CreatedAt { get; set; } - [JsonProperty("updated_at")] - public DateTime UpdatedAt { get; set; } - [JsonProperty("monitoring_at")] - public DateTime? MonitoringAt { get; set; } - [JsonProperty("resolved_at")] - public DateTime? ResolvedAt { get; set; } - [JsonProperty("scheduled_for")] - public DateTime StartTime { get; set; } - [JsonProperty("scheduled_until")] - public DateTime EndTime { get; set; } - - [JsonProperty("incident_updates")] - public IncidentUpdateData[] Updates { get; set; } - } - - public class IncidentUpdateData - { - [JsonProperty("id")] - public string Id { get; set; } - [JsonProperty("incident_id")] - public string IncidentId { get; set; } - [JsonProperty("status")] - public string Status { get; set; } - [JsonProperty("body")] - public string Body { get; set; } - - [JsonProperty("created_at")] - public DateTime CreatedAt { get; set; } - [JsonProperty("updated_at")] - public DateTime? UpdatedAt { get; set; } - [JsonProperty("display_at")] - public DateTime? DisplayAt { get; set; } - - } - - [JsonProperty("page")] - public PageData Page { get; set; } - [JsonProperty("scheduled_maintenances")] - public IncidentData[] ScheduledMaintenances { get; set; } - [JsonProperty("incidents")] - public IncidentData[] Incidents { get; set; } - } -} diff --git a/src/Discord.Net/API/Status/Rest/ActiveMaintenances.cs b/src/Discord.Net/API/Status/Rest/ActiveMaintenances.cs deleted file mode 100644 index 639f85f08..000000000 --- a/src/Discord.Net/API/Status/Rest/ActiveMaintenances.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Status.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class GetActiveMaintenancesRequest : IRestRequest - { - string IRestRequest.Method => "GET"; - string IRestRequest.Endpoint => $"scheduled-maintenances/active.json"; - object IRestRequest.Payload => null; - } -} diff --git a/src/Discord.Net/API/Status/Rest/AllIncidents.cs b/src/Discord.Net/API/Status/Rest/AllIncidents.cs deleted file mode 100644 index 9575bbd43..000000000 --- a/src/Discord.Net/API/Status/Rest/AllIncidents.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Status.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class GetAllIncidentsRequest : IRestRequest - { - string IRestRequest.Method => "GET"; - string IRestRequest.Endpoint => $"incidents.json"; - object IRestRequest.Payload => null; - } -} diff --git a/src/Discord.Net/API/Status/Rest/UnresolvedIncidents.cs b/src/Discord.Net/API/Status/Rest/UnresolvedIncidents.cs deleted file mode 100644 index 3cff11c23..000000000 --- a/src/Discord.Net/API/Status/Rest/UnresolvedIncidents.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Status.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class GetUnresolvedIncidentsRequest : IRestRequest - { - string IRestRequest.Method => "GET"; - string IRestRequest.Endpoint => $"incidents/unresolved.json"; - object IRestRequest.Payload => null; - } -} diff --git a/src/Discord.Net/API/Status/Rest/UpcomingMaintenances.cs b/src/Discord.Net/API/Status/Rest/UpcomingMaintenances.cs deleted file mode 100644 index 803a8a630..000000000 --- a/src/Discord.Net/API/Status/Rest/UpcomingMaintenances.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Newtonsoft.Json; - -namespace Discord.API.Status.Rest -{ - [JsonObject(MemberSerialization.OptIn)] - public class GetUpcomingMaintenancesRequest : IRestRequest - { - string IRestRequest.Method => "GET"; - string IRestRequest.Endpoint => $"scheduled-maintenances/upcoming.json"; - object IRestRequest.Payload => null; - } -} diff --git a/src/Discord.Net/API/Client/VoiceSocket/OpCodes.cs b/src/Discord.Net/API/VoiceSocket/OpCode.cs similarity index 81% rename from src/Discord.Net/API/Client/VoiceSocket/OpCodes.cs rename to src/Discord.Net/API/VoiceSocket/OpCode.cs index e82ab5286..d9174be22 100644 --- a/src/Discord.Net/API/Client/VoiceSocket/OpCodes.cs +++ b/src/Discord.Net/API/VoiceSocket/OpCode.cs @@ -1,10 +1,10 @@ -namespace Discord.API.Client.VoiceSocket +namespace Discord.API.VoiceSocket { - public enum OpCodes : byte + public enum OpCode : byte { /// C→S - Used to associate a connection with a token. Identify = 0, - /// C→S - Used to specify configuration. + /// C→S - Used to specify protocol configuration. SelectProtocol = 1, /// C←S - Used to notify that the voice connection was successful and informs the client of available protocols. Ready = 2, diff --git a/src/Discord.Net/API/VoiceSocket/Unconfirmed/Commands/Heartbeat.cs b/src/Discord.Net/API/VoiceSocket/Unconfirmed/Commands/Heartbeat.cs new file mode 100644 index 000000000..f2f356854 --- /dev/null +++ b/src/Discord.Net/API/VoiceSocket/Unconfirmed/Commands/Heartbeat.cs @@ -0,0 +1,10 @@ +using System; + +namespace Discord.API.VoiceSocket +{ + public class HeartbeatCommand : IWebSocketMessage + { + int IWebSocketMessage.OpCode => (int)OpCode.Heartbeat; + object IWebSocketMessage.Payload => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + } +} diff --git a/src/Discord.Net/API/VoiceSocket/Unconfirmed/Commands/Identify.cs b/src/Discord.Net/API/VoiceSocket/Unconfirmed/Commands/Identify.cs new file mode 100644 index 000000000..06a63e3c2 --- /dev/null +++ b/src/Discord.Net/API/VoiceSocket/Unconfirmed/Commands/Identify.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; + +namespace Discord.API.VoiceSocket +{ + public class IdentifyCommand : IWebSocketMessage + { + int IWebSocketMessage.OpCode => (int)OpCode.Identify; + object IWebSocketMessage.Payload => this; + + [JsonProperty("server_id")] + public ulong GuildId { get; set; } + [JsonProperty("user_id")] + public ulong UserId { get; set; } + [JsonProperty("session_id")] + public string SessionId { get; set; } + [JsonProperty("token")] + public string Token { get; set; } + } +} diff --git a/src/Discord.Net/API/VoiceSocket/Unconfirmed/Commands/SelectProtocol.cs b/src/Discord.Net/API/VoiceSocket/Unconfirmed/Commands/SelectProtocol.cs new file mode 100644 index 000000000..4aa71b4f3 --- /dev/null +++ b/src/Discord.Net/API/VoiceSocket/Unconfirmed/Commands/SelectProtocol.cs @@ -0,0 +1,28 @@ +using Newtonsoft.Json; + +namespace Discord.API.VoiceSocket +{ + public class SelectProtocolCommand : IWebSocketMessage + { + int IWebSocketMessage.OpCode => (int)OpCode.SelectProtocol; + object IWebSocketMessage.Payload => this; + + public class ProtocolData + { + [JsonProperty("address")] + public string Address { get; set; } + [JsonProperty("port")] + public int Port { get; set; } + [JsonProperty("mode")] + public string Mode { get; set; } + } + [JsonProperty("protocol")] + public string Protocol { get; set; } = "udp"; + [JsonProperty("data")] + private ProtocolData Data { get; } = new ProtocolData(); + + public string ExternalAddress { get { return Data.Address; } set { Data.Address = value; } } + public int ExternalPort { get { return Data.Port; } set { Data.Port = value; } } + public string EncryptionMode { get { return Data.Mode; } set { Data.Mode = value; } } + } +} diff --git a/src/Discord.Net/API/Client/VoiceSocket/Commands/SetSpeaking.cs b/src/Discord.Net/API/VoiceSocket/Unconfirmed/Commands/SetSpeaking.cs similarity index 66% rename from src/Discord.Net/API/Client/VoiceSocket/Commands/SetSpeaking.cs rename to src/Discord.Net/API/VoiceSocket/Unconfirmed/Commands/SetSpeaking.cs index 6022c4d58..0476b9a7d 100644 --- a/src/Discord.Net/API/Client/VoiceSocket/Commands/SetSpeaking.cs +++ b/src/Discord.Net/API/VoiceSocket/Unconfirmed/Commands/SetSpeaking.cs @@ -1,12 +1,11 @@ using Newtonsoft.Json; -namespace Discord.API.Client.VoiceSocket +namespace Discord.API.VoiceSocket { public class SetSpeakingCommand : IWebSocketMessage { - int IWebSocketMessage.OpCode => (int)OpCodes.Speaking; + int IWebSocketMessage.OpCode => (int)OpCode.Speaking; object IWebSocketMessage.Payload => this; - bool IWebSocketMessage.IsPrivate => false; [JsonProperty("speaking")] public bool IsSpeaking { get; set; } diff --git a/src/Discord.Net/API/Client/VoiceSocket/Events/Ready.cs b/src/Discord.Net/API/VoiceSocket/Unconfirmed/Events/Ready.cs similarity index 90% rename from src/Discord.Net/API/Client/VoiceSocket/Events/Ready.cs rename to src/Discord.Net/API/VoiceSocket/Unconfirmed/Events/Ready.cs index 6fdced897..7ba700d96 100644 --- a/src/Discord.Net/API/Client/VoiceSocket/Events/Ready.cs +++ b/src/Discord.Net/API/VoiceSocket/Unconfirmed/Events/Ready.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace Discord.API.Client.VoiceSocket +namespace Discord.API.VoiceSocket { public class ReadyEvent { diff --git a/src/Discord.Net/API/Client/VoiceSocket/Events/SessionDescription.cs b/src/Discord.Net/API/VoiceSocket/Unconfirmed/Events/SessionDescription.cs similarity index 85% rename from src/Discord.Net/API/Client/VoiceSocket/Events/SessionDescription.cs rename to src/Discord.Net/API/VoiceSocket/Unconfirmed/Events/SessionDescription.cs index 042c5278d..09bda01c1 100644 --- a/src/Discord.Net/API/Client/VoiceSocket/Events/SessionDescription.cs +++ b/src/Discord.Net/API/VoiceSocket/Unconfirmed/Events/SessionDescription.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace Discord.API.Client.VoiceSocket +namespace Discord.API.VoiceSocket { public class SessionDescriptionEvent { diff --git a/src/Discord.Net/API/Client/VoiceSocket/Events/Speaking.cs b/src/Discord.Net/API/VoiceSocket/Unconfirmed/Events/Speaking.cs similarity index 57% rename from src/Discord.Net/API/Client/VoiceSocket/Events/Speaking.cs rename to src/Discord.Net/API/VoiceSocket/Unconfirmed/Events/Speaking.cs index 59268c4e6..0e1271f98 100644 --- a/src/Discord.Net/API/Client/VoiceSocket/Events/Speaking.cs +++ b/src/Discord.Net/API/VoiceSocket/Unconfirmed/Events/Speaking.cs @@ -1,11 +1,10 @@ -using Discord.API.Converters; -using Newtonsoft.Json; +using Newtonsoft.Json; -namespace Discord.API.Client.VoiceSocket +namespace Discord.API.VoiceSocket { public class SpeakingEvent { - [JsonProperty("user_id"), JsonConverter(typeof(LongStringConverter))] + [JsonProperty("user_id")] public ulong UserId { get; set; } [JsonProperty("ssrc")] public uint SSRC { get; set; } diff --git a/src/Discord.Net/CDN.cs b/src/Discord.Net/CDN.cs new file mode 100644 index 000000000..11786436b --- /dev/null +++ b/src/Discord.Net/CDN.cs @@ -0,0 +1,12 @@ +namespace Discord +{ + internal static class CDN + { + public static string GetUserAvatarUrl(ulong userId, string avatarId) + => avatarId != null ? $"{DiscordConfig.ClientAPIUrl}users/{userId}/avatars/{avatarId}.jpg" : null; + public static string GetGuildIconUrl(ulong guildId, string iconId) + => iconId != null ? $"{DiscordConfig.ClientAPIUrl}guilds/{guildId}/icons/{iconId}.jpg" : null; + public static string GetGuildSplashUrl(ulong guildId, string splashId) + => splashId != null ? $"{DiscordConfig.ClientAPIUrl}guilds/{guildId}/splashes/{splashId}.jpg" : null; + } +} diff --git a/src/Discord.Net/Discord.Net.Net45.csproj b/src/Discord.Net/Discord.Net.Net45.csproj new file mode 100644 index 000000000..f5d12fb3f --- /dev/null +++ b/src/Discord.Net/Discord.Net.Net45.csproj @@ -0,0 +1,305 @@ + + + + + + Debug + AnyCPU + {C6A50D24-CBD3-4E76-852C-4DCA60BBD608} + Library + Properties + Discord + Discord.Net + v4.6.1 + 512 + + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + MinimumRecommendedRules.ruleset + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/src/Discord.Net/Discord.Net.Net45.project.json b/src/Discord.Net/Discord.Net.Net45.project.json new file mode 100644 index 000000000..0d36751ed --- /dev/null +++ b/src/Discord.Net/Discord.Net.Net45.project.json @@ -0,0 +1,12 @@ +{ + "dependencies": { + "Newtonsoft.Json": "8.0.3", + "System.Collections.Immutable": "1.2.0-rc2-23608" + }, + "frameworks": { + "net461": {} + }, + "runtimes": { + "win": {} + } +} \ No newline at end of file diff --git a/src/Discord.Net/Discord.Net.Net45.project.lock.json b/src/Discord.Net/Discord.Net.Net45.project.lock.json new file mode 100644 index 000000000..aad5884f0 --- /dev/null +++ b/src/Discord.Net/Discord.Net.Net45.project.lock.json @@ -0,0 +1,88 @@ +{ + "locked": false, + "version": 2, + "targets": { + ".NETFramework,Version=v4.6.1": { + "Newtonsoft.Json/8.0.3": { + "type": "package", + "compile": { + "lib/net45/Newtonsoft.Json.dll": {} + }, + "runtime": { + "lib/net45/Newtonsoft.Json.dll": {} + } + }, + "System.Collections.Immutable/1.2.0-rc2-23608": { + "type": "package", + "compile": { + "lib/dotnet5.1/System.Collections.Immutable.dll": {} + }, + "runtime": { + "lib/dotnet5.1/System.Collections.Immutable.dll": {} + } + } + }, + ".NETFramework,Version=v4.6.1/win": { + "Newtonsoft.Json/8.0.3": { + "type": "package", + "compile": { + "lib/net45/Newtonsoft.Json.dll": {} + }, + "runtime": { + "lib/net45/Newtonsoft.Json.dll": {} + } + }, + "System.Collections.Immutable/1.2.0-rc2-23608": { + "type": "package", + "compile": { + "lib/dotnet5.1/System.Collections.Immutable.dll": {} + }, + "runtime": { + "lib/dotnet5.1/System.Collections.Immutable.dll": {} + } + } + } + }, + "libraries": { + "Newtonsoft.Json/8.0.3": { + "sha512": "KGsYQdS2zLH+H8x2cZaSI7e+YZ4SFIbyy1YJQYl6GYBWjf5o4H1A68nxyq+WTyVSOJQ4GqS/DiPE+UseUizgMg==", + "type": "package", + "files": [ + "Newtonsoft.Json.8.0.3.nupkg.sha512", + "Newtonsoft.Json.nuspec", + "lib/net20/Newtonsoft.Json.dll", + "lib/net20/Newtonsoft.Json.xml", + "lib/net35/Newtonsoft.Json.dll", + "lib/net35/Newtonsoft.Json.xml", + "lib/net40/Newtonsoft.Json.dll", + "lib/net40/Newtonsoft.Json.xml", + "lib/net45/Newtonsoft.Json.dll", + "lib/net45/Newtonsoft.Json.xml", + "lib/portable-net40+sl5+wp80+win8+wpa81/Newtonsoft.Json.dll", + "lib/portable-net40+sl5+wp80+win8+wpa81/Newtonsoft.Json.xml", + "lib/portable-net45+wp80+win8+wpa81+dnxcore50/Newtonsoft.Json.dll", + "lib/portable-net45+wp80+win8+wpa81+dnxcore50/Newtonsoft.Json.xml", + "tools/install.ps1" + ] + }, + "System.Collections.Immutable/1.2.0-rc2-23608": { + "sha512": "LIodNcjmeDMzZ0P0nadxBAiZcxwTNXmiHMJoyj1xO2vvahd617xLnO8tJrWNCKgPcwDimuAC9twqsQRFiDOuDQ==", + "type": "package", + "files": [ + "System.Collections.Immutable.1.2.0-rc2-23608.nupkg.sha512", + "System.Collections.Immutable.nuspec", + "lib/dotnet5.1/System.Collections.Immutable.dll", + "lib/dotnet5.1/System.Collections.Immutable.xml", + "lib/portable-net45+win8+wp8+wpa81/System.Collections.Immutable.dll", + "lib/portable-net45+win8+wp8+wpa81/System.Collections.Immutable.xml" + ] + } + }, + "projectFileDependencyGroups": { + "": [ + "Newtonsoft.Json >= 8.0.3", + "System.Collections.Immutable >= 1.2.0-rc2-23608" + ], + ".NETFramework,Version=v4.6.1": [] + } +} \ No newline at end of file diff --git a/src/Discord.Net/Discord.Net.xproj b/src/Discord.Net/Discord.Net.xproj index be1dbc400..e5ec681b5 100644 --- a/src/Discord.Net/Discord.Net.xproj +++ b/src/Discord.Net/Discord.Net.xproj @@ -1,20 +1,20 @@  - + - 14.0 + 14.0.25123 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - acfb060b-ec8a-4926-b293-04c01e17ee23 + 2c91bdd7-621d-460f-b768-ead106d9ba62 Discord - ..\..\artifacts\obj\$(MSBuildProjectName) - ..\..\artifacts\bin\$(MSBuildProjectName)\ + ..\..\..\TestBot\artifacts\obj\$(MSBuildProjectName) + ..\..\..\TestBot\artifacts\bin\$(MSBuildProjectName)\ 2.0 - + True diff --git a/src/Discord.Net/DiscordClient.Events.cs b/src/Discord.Net/DiscordClient.Events.cs deleted file mode 100644 index b05725f9b..000000000 --- a/src/Discord.Net/DiscordClient.Events.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System; -using System.Runtime.CompilerServices; - -namespace Discord -{ - public partial class DiscordClient - { - public event EventHandler Ready = delegate { }; - //public event EventHandler LoggedOut = delegate { }; - public event EventHandler ChannelCreated = delegate { }; - public event EventHandler ChannelDestroyed = 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 RoleCreated = 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 ServerUnavailable = delegate { }; - public event EventHandler UserBanned = delegate { }; - public event EventHandler UserIsTyping = delegate { }; - public event EventHandler UserJoined = delegate { }; - public event EventHandler UserLeft = delegate { }; - public event EventHandler UserUpdated = delegate { }; - public event EventHandler UserUnbanned = delegate { }; - - private void OnReady() - => OnEvent(Ready); - /*private void OnLoggedOut(bool wasUnexpected, Exception ex) - => OnEvent(LoggedOut, new DisconnectedEventArgs(wasUnexpected, ex));*/ - - private void OnChannelCreated(IChannel channel) - => OnEvent(ChannelCreated, new ChannelEventArgs(channel)); - private void OnChannelDestroyed(IChannel channel) - => OnEvent(ChannelDestroyed, new ChannelEventArgs(channel)); - private void OnChannelUpdated(IChannel before, IChannel after) - => OnEvent(ChannelUpdated, new ChannelUpdatedEventArgs(before, after)); - - private void OnMessageAcknowledged(Message msg) - => OnEvent(MessageAcknowledged, new MessageEventArgs(msg)); - private void OnMessageDeleted(Message msg) - => OnEvent(MessageDeleted, new MessageEventArgs(msg)); - private void OnMessageReceived(Message msg) - => OnEvent(MessageReceived, new MessageEventArgs(msg)); - internal void OnMessageSent(Message msg) - => OnEvent(MessageSent, new MessageEventArgs(msg)); - private void OnMessageUpdated(Message before, Message after) - => OnEvent(MessageUpdated, new MessageUpdatedEventArgs(before, after)); - - 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 before, Role after) - => OnEvent(RoleUpdated, new RoleUpdatedEventArgs(before, after)); - - private void OnJoinedServer(Server server) - => OnEvent(JoinedServer, new ServerEventArgs(server)); - private void OnLeftServer(Server server) - => OnEvent(LeftServer, new ServerEventArgs(server)); - private void OnServerAvailable(Server server) - => OnEvent(ServerAvailable, 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(ITextChannel channel, User user) - => OnEvent(UserIsTyping, new TypingEventArgs(channel, user)); - private void OnUserJoined(User user) - => OnEvent(UserJoined, new UserEventArgs(user)); - private void OnUserLeft(User user) - => OnEvent(UserLeft, new UserEventArgs(user)); - private void OnUserUnbanned(User user) - => OnEvent(UserUnbanned, 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) - { - try { handler(this, eventArgs); } - catch (Exception ex) - { - Logger.Error($"{callerName.Substring(2)}'s handler encountered an error", ex); - } - } - private void OnEvent(EventHandler handler, [CallerMemberName] string callerName = null) - { - try { handler(this, EventArgs.Empty); } - catch (Exception ex) - { - Logger.Error($"{callerName.Substring(2)}'s handler encountered an error", ex); - } - } - } -} diff --git a/src/Discord.Net/DiscordClient.cs b/src/Discord.Net/DiscordClient.cs index 5fa94bbc2..d59f7fec1 100644 --- a/src/Discord.Net/DiscordClient.cs +++ b/src/Discord.Net/DiscordClient.cs @@ -1,398 +1,136 @@ -using APIChannel = Discord.API.Client.Channel; -using Discord.API.Client.GatewaySocket; -using Discord.API.Client.Rest; +using Discord.API.Rest; using Discord.Logging; using Discord.Net; using Discord.Net.Rest; -using Discord.Net.WebSockets; -using Newtonsoft.Json; -using Nito.AsyncEx; using System; -using System.Collections.Concurrent; using System.Collections.Generic; -using System.Diagnostics; +using System.Collections.Immutable; using System.IO; using System.Linq; using System.Net; -using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; namespace Discord { - /// Provides a connection to the DiscordApp service. - public partial class DiscordClient : IDisposable + public class DiscordClient : IDisposable { - private readonly AsyncLock _connectionLock; - private readonly ManualResetEvent _disconnectedEvent; - private readonly ManualResetEventSlim _connectedEvent; - private readonly TaskManager _taskManager; - private readonly ServiceCollection _services; - private ConcurrentDictionary _servers; - private ConcurrentDictionary _channels; - private ConcurrentDictionary _privateChannels; //Key = RecipientId - private Dictionary _regions; - private Stopwatch _connectionStopwatch; - - internal Logger Logger { get; } - - /// Gets the configuration object used to make this client. - public DiscordConfig Config { get; } - /// Gets the log manager. - public LogManager Log { get; } - /// Gets the internal RestClient for the Client API endpoint. - public RestClient ClientAPI { get; } - /// Gets the internal RestClient for the Status API endpoint. - public RestClient StatusAPI { get; } - /// Gets the internal WebSocket for the Gateway event stream. - public GatewaySocket GatewaySocket { get; } + public event EventHandler Log; + public event EventHandler LoggedIn, LoggedOut; + + protected readonly RestClientProvider _restClientProvider; + protected readonly string _token; + protected readonly LogManager _logManager; + protected readonly SemaphoreSlim _connectionLock; + protected readonly Logger _restLogger; + protected CancellationTokenSource _cancelToken; + protected bool _isDisposed; + + public string UserAgent { get; } + public IReadOnlyList VoiceRegions { get; private set; } + /// Gets the internal RestClient. + public RestClient RestClient { get; protected set; } /// Gets the queue used for outgoing messages, if enabled. - public MessageQueue MessageQueue { get; } - /// Gets the JSON serializer used by this client. - public JsonSerializer Serializer { get; } - - /// Gets the current connection state of this client. - public ConnectionState State { get; private set; } - /// Gets a cancellation token that triggers when the client is manually disconnected. - public CancellationToken CancelToken { get; private set; } - /// Gets the current logged-in user used in private channels. - internal User PrivateUser { get; private set; } - /// Gets information about the current logged-in account. - public Profile CurrentUser { get; private set; } - /// Gets the session id for the current connection. - public string SessionId { get; private set; } - /// Gets the status of the current user. - public UserStatus Status { get; private set; } - /// Gets the game the current user is displayed as playing. - public string CurrentGame { get; private set; } + public MessageQueue MessageQueue { get; protected set; } + public SelfUser CurrentUser { get; protected set; } + public bool IsLoggedIn { get; private set; } - /// Gets a collection of all extensions added to this DiscordClient. - public IEnumerable Services => _services; - /// Gets a collection of all servers this client is a member of. - public IEnumerable Servers => _servers.Select(x => x.Value); - /// Gets a collection of all private channels this client is a member of. - public IEnumerable PrivateChannels => _privateChannels.Select(x => x.Value); - /// Gets a collection of all voice regions currently offered by Discord. - public IEnumerable Regions => _regions.Select(x => x.Value); + internal CancellationToken CancelToken => _cancelToken.Token; - /// Initializes a new instance of the DiscordClient class. - public DiscordClient(Action configFunc) - : this(ProcessConfig(configFunc)) - { - } - private static DiscordConfigBuilder ProcessConfig(Action func) - { - var config = new DiscordConfigBuilder(); - func(config); - return config; - } - - /// Initializes a new instance of the DiscordClient class. - public DiscordClient() - : this(new DiscordConfigBuilder()) - { - } - /// Initializes a new instance of the DiscordClient class. - public DiscordClient(DiscordConfigBuilder builder) - : this(builder.Build()) - { - if (builder.LogHandler != null) - Log.Message += builder.LogHandler; - } - /// Initializes a new instance of the DiscordClient class. - public DiscordClient(DiscordConfig config) + public DiscordClient(string token, DiscordConfig config = null) { - Config = config; - - State = (int)ConnectionState.Disconnected; - Status = UserStatus.Online; - - //Logging - Log = new LogManager(this); - Logger = Log.CreateLogger("Discord"); - if (config.LogLevel >= LogSeverity.Verbose) - _connectionStopwatch = new Stopwatch(); + if (token == null) throw new ArgumentNullException(nameof(token)); - //Async - _taskManager = new TaskManager(Cleanup); - _connectionLock = new AsyncLock(); - _disconnectedEvent = new ManualResetEvent(true); - _connectedEvent = new ManualResetEventSlim(false); - CancelToken = new CancellationToken(true); - - //Cache - //ConcurrentLevel = 2 (only REST and WebSocket can add/remove) - _servers = new ConcurrentDictionary(2, 0); - _channels = new ConcurrentDictionary(2, 0); - _privateChannels = new ConcurrentDictionary(2, 0); - - //Serialization - Serializer = new JsonSerializer(); - Serializer.DateTimeZoneHandling = DateTimeZoneHandling.Utc; -#if TEST_RESPONSES - Serializer.CheckAdditionalContent = true; - Serializer.MissingMemberHandling = MissingMemberHandling.Error; -#else - Serializer.CheckAdditionalContent = false; - Serializer.MissingMemberHandling = MissingMemberHandling.Ignore; -#endif - Serializer.Error += (s, e) => - { - e.ErrorContext.Handled = true; - Logger.Error("Serialization Failed", e.ErrorContext.Error); - }; + if (config == null) + config = new DiscordConfig(); - //Networking - 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.Disconnected += (s, e) => OnDisconnected(e.WasUnexpected, e.Exception); - GatewaySocket.ReceivedDispatch += (s, e) => OnReceivedEvent(e); + _token = token; + _connectionLock = new SemaphoreSlim(1, 1); - MessageQueue = new MessageQueue(ClientAPI, Log.CreateLogger("MessageQueue")); - - //Extensibility - _services = new ServiceCollection(this); - } - - /// Connects to the Discord server with the provided email and password. - /// Returns a token that can be optionally stored to speed up future connections. - public async Task Connect(string email, string password, string token = null) - { - if (email == null) throw new ArgumentNullException(email); - if (password == null) throw new ArgumentNullException(password); + _restClientProvider = config.RestClientProvider; + UserAgent = GetUserAgent(config.AppName, config.AppVersion, config.AppUrl); - await BeginConnect(email, password, null).ConfigureAwait(false); - return ClientAPI.Token; - } - /// Connects to the Discord server with the provided token. - public async Task Connect(string token) - { - if (token == null) throw new ArgumentNullException(token); - - await BeginConnect(null, null, token).ConfigureAwait(false); + _logManager = new LogManager(config.LogLevel); + _logManager.Message += (s, e) => Log(this, e); + _restLogger = _logManager.CreateLogger("Rest"); } - private async Task BeginConnect(string email, string password, string token = null) + public async Task Login() { + await _connectionLock.WaitAsync().ConfigureAwait(false); try { - using (await _connectionLock.LockAsync().ConfigureAwait(false)) - { - await Disconnect().ConfigureAwait(false); - _taskManager.ClearException(); - - Stopwatch stopwatch = null; - if (Config.LogLevel >= LogSeverity.Verbose) - { - _connectionStopwatch.Restart(); - stopwatch = Stopwatch.StartNew(); - } - State = ConnectionState.Connecting; - _disconnectedEvent.Reset(); - - var cancelSource = new CancellationTokenSource(); - CancelToken = cancelSource.Token; - ClientAPI.CancelToken = CancelToken; - StatusAPI.CancelToken = CancelToken; - - await Login(email, password, token).ConfigureAwait(false); - await GatewaySocket.Connect(ClientAPI, CancelToken).ConfigureAwait(false); - - var tasks = new[] { CancelToken.Wait() } - .Concat(MessageQueue.Run(CancelToken)); - - await _taskManager.Start(tasks, 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($"Handshake + Ready took {seconds} sec"); - } - } - } - catch (Exception ex) - { - await _taskManager.SignalError(ex).ConfigureAwait(false); - throw; + await LoginInternal().ConfigureAwait(false); } + finally { _connectionLock.Release(); } } - private async Task Login(string email = null, string password = null, string token = null) + protected virtual async Task LoginInternal() { - string tokenPath = null, oldToken = null; - byte[] cacheKey = null; + if (IsLoggedIn) + await LogoutInternal().ConfigureAwait(false); - //Get Token - if (email != null && Config.CacheDir != null) + try { - tokenPath = GetTokenCachePath(email); - if (token == null && password != null) - { - Rfc2898DeriveBytes deriveBytes = new Rfc2898DeriveBytes(password, - new byte[] { 0x5A, 0x2A, 0xF8, 0xCF, 0x78, 0xD3, 0x7D, 0x0D }); - cacheKey = deriveBytes.GetBytes(16); + _cancelToken = new CancellationTokenSource(); - oldToken = LoadToken(tokenPath, cacheKey); - token = oldToken; - } - } + RestClient = new RestClient(_restClientProvider(DiscordConfig.ClientAPIUrl, _cancelToken.Token)); + RestClient.SetHeader("accept", "*/*"); + RestClient.SetHeader("authorization", _token); + RestClient.SetHeader("user-agent", UserAgent); + RestClient.SentRequest += (s, e) => _restLogger.Verbose($"{e.Request.Method} {e.Request.Endpoint}: {e.Milliseconds} ms"); - ClientAPI.Token = token; - var request = new LoginRequest() { Email = email, Password = password }; - var response = await ClientAPI.Send(request).ConfigureAwait(false); - token = response.Token; - if (Config.CacheDir != null && token != oldToken && tokenPath != null) - SaveToken(tokenPath, cacheKey, token); - ClientAPI.Token = token; + MessageQueue = new MessageQueue(RestClient, _restLogger); + await MessageQueue.Start(_cancelToken.Token).ConfigureAwait(false); - //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)) - .ToDictionary(x => x.Id); - } - private void EndConnect() - { - if (State == ConnectionState.Connecting) - { - State = ConnectionState.Connected; - _connectedEvent.Set(); + var selfResponse = await RestClient.Send(new GetCurrentUserRequest()).ConfigureAwait(false); + var regionsResponse = await RestClient.Send(new GetVoiceRegionsRequest()).ConfigureAwait(false); - if (Config.LogLevel >= LogSeverity.Verbose) - { - _connectionStopwatch.Stop(); - double seconds = Math.Round(_connectionStopwatch.ElapsedTicks / (double)TimeSpan.TicksPerSecond, 2); - Logger.Verbose($"Connection took {seconds} sec"); - } + CurrentUser = CreateSelfUser(selfResponse); + VoiceRegions = regionsResponse.Select(x => CreateVoiceRegion(x)).ToImmutableArray(); - SendStatus(); - OnReady(); + IsLoggedIn = true; + RaiseEvent(LoggedIn); } + catch (Exception) { await LogoutInternal().ConfigureAwait(false); throw; } } - /// Disconnects from the Discord server, canceling any pending requests. - public Task Disconnect() => _taskManager.Stop(true); - private async Task Cleanup() + public async Task Logout() { - var oldState = State; - State = ConnectionState.Disconnecting; - - if (oldState == ConnectionState.Connected) + _cancelToken?.Cancel(); + await _connectionLock.WaitAsync().ConfigureAwait(false); + try { - try { await ClientAPI.Send(new LogoutRequest()).ConfigureAwait(false); } - catch (OperationCanceledException) { } + await LogoutInternal().ConfigureAwait(false); } - - MessageQueue.Clear(); - - await GatewaySocket.Disconnect().ConfigureAwait(false); - ClientAPI.Token = null; - - _servers.Clear(); - _channels.Clear(); - _privateChannels.Clear(); - - PrivateUser = null; - CurrentUser = null; - - State = (int)ConnectionState.Disconnected; - _connectedEvent.Reset(); - _disconnectedEvent.Set(); + finally { _connectionLock.Release(); } } - - public void SetStatus(UserStatus status) + protected virtual async Task LogoutInternal() { - if (status == null) throw new ArgumentNullException(nameof(status)); - if (status != UserStatus.Online && status != UserStatus.Idle) - throw new ArgumentException($"Invalid status, must be {UserStatus.Online} or {UserStatus.Idle}", nameof(status)); + bool wasLoggedIn = IsLoggedIn; - Status = status; - SendStatus(); - } - public void SetGame(string game) - { - CurrentGame = game; - SendStatus(); - } - private void SendStatus() - { - PrivateUser.Status = Status; - PrivateUser.CurrentGame = CurrentGame; - foreach (var server in Servers) - { - var current = server.CurrentUser; - if (current != null) - { - current.Status = Status; - current.CurrentGame = CurrentGame; - } - } - var socket = GatewaySocket; - if (socket != null) - socket.SendUpdateStatus(Status == UserStatus.Idle ? EpochTime.GetMilliseconds() - (10 * 60 * 1000) : (long?)null, CurrentGame); - } + try { _cancelToken.Cancel(); } catch { } + try { await MessageQueue.Stop().ConfigureAwait(false); } catch { } - #region Channels - internal void AddChannel(IChannel channel) - { - _channels.GetOrAdd(channel.Id, channel); - } - private IChannel RemoveChannel(ulong id) - { - IChannel channel; - if (_channels.TryRemove(id, out channel)) + RestClient = null; + MessageQueue = null; + + if (wasLoggedIn) { - if (channel.IsPrivate) - { - PrivateChannel removed; - _privateChannels.TryRemove((channel as PrivateChannel).Recipient.Id, out removed); - } - else - (channel as PublicChannel).Server.RemoveChannel(id); + IsLoggedIn = false; + RaiseEvent(LoggedOut); } - return channel; - } - public IChannel GetChannel(ulong id) - { - IChannel channel; - _channels.TryGetValue(id, out channel); - return channel; } - private PrivateChannel AddPrivateChannel(APIChannel model) - { - IChannel channel; - if (_channels.TryGetOrAdd(model.Id, x => new PrivateChannel(x, new User(model.Recipient, this, null), model), out channel)) - _privateChannels[model.Recipient.Id] = channel as PrivateChannel; - return channel as PrivateChannel; - } - internal PrivateChannel GetPrivateChannel(ulong recipientId) + public virtual async Task> GetDMChannels() { - PrivateChannel channel; - _privateChannels.TryGetValue(recipientId, out channel); - return channel; - } - public Task CreatePrivateChannel(User user) => CreatePrivateChannel(user.Id); - public async Task CreatePrivateChannel(ulong userId) - { - var channel = GetPrivateChannel(userId); - if (channel != null) return channel; - - var request = new CreatePrivateChannelRequest() { RecipientId = userId }; - var response = await ClientAPI.Send(request).ConfigureAwait(false); - - return AddPrivateChannel(response); + var response = await RestClient.Send(new GetCurrentUserDMsRequest()).ConfigureAwait(false); + var result = ImmutableArray.CreateBuilder(response.Length); + for (int i = 0; i < response.Length; i++) + result[i] = CreateDMChannel(response[i]); + return result.ToImmutable(); } - #endregion - - #region Invites - /// Gets more info about the provided invite code. - /// Supported formats: inviteCode, xkcdCode, https://discord.gg/inviteCode, https://discord.gg/xkcdCode - /// The invite object if found, null if not. - public async Task GetInvite(string inviteIdOrXkcd) + public virtual async Task GetInvite(string inviteIdOrXkcd) { if (inviteIdOrXkcd == null) throw new ArgumentNullException(nameof(inviteIdOrXkcd)); @@ -406,697 +144,212 @@ namespace Discord try { - var response = await ClientAPI.Send(new GetInviteRequest(inviteIdOrXkcd)).ConfigureAwait(false); - var invite = new Invite(response, this); - invite.Update(response); - return invite; + var response = await RestClient.Send(new GetInviteRequest(inviteIdOrXkcd)).ConfigureAwait(false); + return CreatePublicInvite(response); } catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { return null; } } - #endregion - - #region Regions - public Region GetRegion(string id) + public virtual async Task GetGuild(ulong id) { - Region region; - if (_regions.TryGetValue(id, out region)) - return region; - else - return new Region(id, id, "", 0, false); + var response = await RestClient.Send(new GetGuildRequest(id)).ConfigureAwait(false); + return CreateGuild(response); } - #endregion - - #region Servers - private Server AddServer(ulong id) => _servers.GetOrAdd(id, x => new Server(x, this)); - private Server RemoveServer(ulong id) + public virtual async Task> GetGuilds() { - Server server; - if (_servers.TryRemove(id, out server)) + var response = await RestClient.Send(new GetCurrentUserGuildsRequest()).ConfigureAwait(false); + var result = ImmutableArray.CreateBuilder(response.Length); + for (int i = 0; i < response.Length; i++) + result[i] = CreateGuild(response[i]); + return result.ToImmutable(); + } + public virtual async Task GetUser(ulong id) + { + var response = await RestClient.Send(new GetUserRequest(id)); + var user = CreatePublicUser(response); + return user; + } + public virtual async Task GetUser(string username, ushort discriminator) + { + var response = await RestClient.Send(new QueryUserRequest() { Query = $"{username}#{discriminator}", Limit = 1 }); + if (response.Length > 0) { - foreach (var channel in server.AllChannels) - RemoveChannel(channel.Id); + var user = CreatePublicUser(response[0]); + return user; } - return server; + return null; } - - public Server GetServer(ulong id) + public virtual VoiceRegion GetOptimalVoiceRegion() + { + var regions = VoiceRegions; + for (int i = 0; i < regions.Count; i++) + { + if (regions[i].IsOptimal) + return regions[i]; + } + return null; + } + public virtual VoiceRegion GetVoiceRegion(string id) { - Server server; - _servers.TryGetValue(id, out server); - return server; + if (id == null) throw new ArgumentNullException(nameof(id)); + + var regions = VoiceRegions; + for (int i = 0; i < regions.Count; i++) + { + if (regions[i].Id == id) + return regions[i]; + } + return null; } - /// Creates a new server with the provided name and region. - public async Task CreateServer(string name, Region region, ImageType iconType = ImageType.None, Stream icon = null) + public virtual async Task GetOrCreateDMChannel(ulong userId) + { + var response = await RestClient.Send(new CreateDMChannelRequest + { + RecipientId = userId + }).ConfigureAwait(false); + + return CreateDMChannel(response); + } + /// Creates a new guild with the provided name and region. This function requires your bot to be whitelisted by Discord. + public virtual async Task CreateGuild(string name, VoiceRegion region, Stream jpegIcon = null) { if (name == null) throw new ArgumentNullException(nameof(name)); if (region == null) throw new ArgumentNullException(nameof(region)); - var request = new CreateGuildRequest() + var response = await RestClient.Send(new CreateGuildRequest { Name = name, Region = region.Id, - IconBase64 = icon.Base64(iconType, null) - }; - var response = await ClientAPI.Send(request).ConfigureAwait(false); + Icon = jpegIcon + }).ConfigureAwait(false); - var server = AddServer(response.Id); - server.Update(response); - return server; + return CreateGuild(response); } - #endregion - #region Gateway Events - private void OnReceivedEvent(WebSocketEventEventArgs e) + internal virtual DMChannel CreateDMChannel(API.Channel model) { - try - { - switch (e.Type) - { - //Global - case "READY": - { - //TODO: None of this is really threadsafe - should only replace the cache collections when they have been fully populated - - var data = e.Payload.ToObject(Serializer); - - int channelCount = 0; - for (int i = 0; i < data.Guilds.Length; i++) - channelCount += data.Guilds[i].Channels.Length; - - //ConcurrencyLevel = 2 (only REST and WebSocket can add/remove) - _servers = new ConcurrentDictionary(2, (int)(data.Guilds.Length * 1.05)); - _channels = new ConcurrentDictionary(2, (int)(channelCount * 1.05)); - _privateChannels = new ConcurrentDictionary(2, (int)(data.PrivateChannels.Length * 1.05)); - List largeServers = new List(); - - SessionId = data.SessionId; - PrivateUser = new User(data.User, this, null); - PrivateUser.Update(data.User); - CurrentUser = new Profile(data.User, this); - CurrentUser.Update(data.User); - - for (int i = 0; i < data.Guilds.Length; i++) - { - var model = data.Guilds[i]; - if (model.Unavailable != true) - { - var server = AddServer(model.Id); - server.Update(model); - if (model.IsLarge) - largeServers.Add(server.Id); - } - } - for (int i = 0; i < data.PrivateChannels.Length; i++) - AddPrivateChannel(data.PrivateChannels[i]); - if (largeServers.Count > 0) - GatewaySocket.SendRequestMembers(largeServers, "", 0); - else - EndConnect(); - } - break; - - //Servers - case "GUILD_CREATE": - { - var data = e.Payload.ToObject(Serializer); - if (data.Unavailable != true) - { - var server = AddServer(data.Id); - server.Update(data); - - if (data.Unavailable != false) - Logger.Info($"GUILD_CREATE: {server}"); - else - Logger.Info($"GUILD_AVAILABLE: {server}"); - - if (data.Unavailable != false) - OnJoinedServer(server); - OnServerAvailable(server); - } - } - break; - case "GUILD_UPDATE": - { - var data = e.Payload.ToObject(Serializer); - var server = GetServer(data.Id); - if (server != null) - { - var before = Config.EnablePreUpdateEvents ? server.Clone() : null; - server.Update(data); - Logger.Info($"GUILD_UPDATE: {server}"); - OnServerUpdated(before, server); - } - else - Logger.Warning("GUILD_UPDATE referenced an unknown guild."); - } - break; - case "GUILD_DELETE": - { - var data = e.Payload.ToObject(Serializer); - Server server = RemoveServer(data.Id); - if (server != null) - { - if (data.Unavailable != true) - Logger.Info($"GUILD_DELETE: {server}"); - else - Logger.Info($"GUILD_UNAVAILABLE: {server}"); - - OnServerUnavailable(server); - if (data.Unavailable != true) - OnLeftServer(server); - } - else - Logger.Warning("GUILD_DELETE referenced an unknown guild."); - } - break; - - //Channels - case "CHANNEL_CREATE": - { - var data = e.Payload.ToObject(Serializer); - - IChannel channel = null; - if (data.GuildId != null) - { - var server = GetServer(data.GuildId.Value); - if (server != null) - channel = server.AddChannel(data, true); - else - { - Logger.Warning("CHANNEL_CREATE referenced an unknown guild."); - break; - } - } - else - channel = AddPrivateChannel(data); - Logger.Info($"CHANNEL_CREATE: {channel}"); - OnChannelCreated(channel); - } - break; - case "CHANNEL_UPDATE": - { - var data = e.Payload.ToObject(Serializer); - var channel = GetChannel(data.Id); - if (channel != null) - { - var before = Config.EnablePreUpdateEvents ? (channel as Channel).Clone() : null; - (channel as Channel).Update(data); - Logger.Info($"CHANNEL_UPDATE: {channel}"); - OnChannelUpdated(before, channel); - } - else - Logger.Warning("CHANNEL_UPDATE referenced an unknown channel."); - } - break; - case "CHANNEL_DELETE": - { - var data = e.Payload.ToObject(Serializer); - var channel = RemoveChannel(data.Id); - if (channel != null) - { - Logger.Info($"CHANNEL_DELETE: {channel}"); - OnChannelDestroyed(channel); - } - else - Logger.Warning("CHANNEL_DELETE referenced an unknown channel."); - } - break; - - //Members - case "GUILD_MEMBER_ADD": - { - var data = e.Payload.ToObject(Serializer); - var server = GetServer(data.GuildId.Value); - if (server != null) - { - var user = server.AddUser(data, true, true); - user.Update(data); - user.UpdateActivity(); - Logger.Info($"GUILD_MEMBER_ADD: {user}"); - OnUserJoined(user); - } - else - Logger.Warning("GUILD_MEMBER_ADD referenced an unknown guild."); - } - break; - case "GUILD_MEMBER_UPDATE": - { - var data = e.Payload.ToObject(Serializer); - var server = GetServer(data.GuildId.Value); - if (server != null) - { - var user = server.GetUser(data.User.Id); - if (user != null) - { - var before = Config.EnablePreUpdateEvents ? user.Clone() : null; - user.Update(data); - Logger.Info($"GUILD_MEMBER_UPDATE: {user}"); - OnUserUpdated(before, user); - } - else - Logger.Warning("GUILD_MEMBER_UPDATE referenced an unknown user."); - } - else - Logger.Warning("GUILD_MEMBER_UPDATE referenced an unknown guild."); - } - break; - case "GUILD_MEMBER_REMOVE": - { - var data = e.Payload.ToObject(Serializer); - var server = GetServer(data.GuildId.Value); - if (server != null) - { - var user = server.RemoveUser(data.User.Id); - if (user != null) - { - Logger.Info($"GUILD_MEMBER_REMOVE: {user}"); - OnUserLeft(user); - } - else - Logger.Warning("GUILD_MEMBER_REMOVE referenced an unknown user."); - } - else - Logger.Warning("GUILD_MEMBER_REMOVE referenced an unknown guild."); - } - break; - case "GUILD_MEMBERS_CHUNK": - { - var data = e.Payload.ToObject(Serializer); - var server = GetServer(data.GuildId); - if (server != null) - { - foreach (var memberData in data.Members) - { - var user = server.AddUser(memberData, true, false); - user.Update(memberData); - } - Logger.Verbose($"GUILD_MEMBERS_CHUNK: {data.Members.Length} users"); - - if (server.CurrentUserCount >= server.UserCount) //Finished downloading for there - { - bool isConnectComplete = true; - foreach (var server2 in _servers.Select(x => x.Value)) - { - if (server2.CurrentUserCount < server2.UserCount) - isConnectComplete = false; - } - if (isConnectComplete) - EndConnect(); - } - } - else - Logger.Warning("GUILD_MEMBERS_CHUNK referenced an unknown guild."); - } - break; - - //Roles - case "GUILD_ROLE_CREATE": - { - var data = e.Payload.ToObject(Serializer); - var server = GetServer(data.GuildId); - if (server != null) - { - var role = server.AddRole(data.Data.Id); - role.Update(data.Data, false); - Logger.Info($"GUILD_ROLE_CREATE: {role}"); - OnRoleCreated(role); - } - else - Logger.Warning("GUILD_ROLE_CREATE referenced an unknown guild."); - } - break; - case "GUILD_ROLE_UPDATE": - { - var data = e.Payload.ToObject(Serializer); - var server = GetServer(data.GuildId); - if (server != null) - { - var role = server.GetRole(data.Data.Id); - if (role != null) - { - var before = Config.EnablePreUpdateEvents ? role.Clone() : null; - role.Update(data.Data, true); - Logger.Info($"GUILD_ROLE_UPDATE: {role}"); - OnRoleUpdated(before, role); - } - else - Logger.Warning("GUILD_ROLE_UPDATE referenced an unknown role."); - } - else - Logger.Warning("GUILD_ROLE_UPDATE referenced an unknown guild."); - } - break; - case "GUILD_ROLE_DELETE": - { - var data = e.Payload.ToObject(Serializer); - var server = GetServer(data.GuildId); - if (server != null) - { - var role = server.RemoveRole(data.RoleId); - if (role != null) - { - Logger.Info($"GUILD_ROLE_DELETE: {role}"); - OnRoleDeleted(role); - } - else - Logger.Warning("GUILD_ROLE_DELETE referenced an unknown role."); - } - else - Logger.Warning("GUILD_ROLE_DELETE referenced an unknown guild."); - } - break; - - //Bans - case "GUILD_BAN_ADD": - { - var data = e.Payload.ToObject(Serializer); - var server = GetServer(data.GuildId.Value); - if (server != null) - { - var user = server.GetUser(data.User.Id); - if (user != null) - { - Logger.Info($"GUILD_BAN_ADD: {user}"); - OnUserBanned(user); - } - else - Logger.Warning("GUILD_BAN_ADD referenced an unknown user."); - } - else - Logger.Warning("GUILD_BAN_ADD referenced an unknown guild."); - } - break; - case "GUILD_BAN_REMOVE": - { - var data = e.Payload.ToObject(Serializer); - var server = GetServer(data.GuildId.Value); - if (server != null) - { - var user = new User(data.User, this, server); - user.Update(data.User); - Logger.Info($"GUILD_BAN_REMOVE: {user}"); - OnUserUnbanned(user); - } - else - Logger.Warning("GUILD_BAN_REMOVE referenced an unknown guild."); - } - break; - - //Messages - case "MESSAGE_CREATE": - { - var data = e.Payload.ToObject(Serializer); - - var channel = GetChannel(data.ChannelId) as ITextChannel; - if (channel != null) - { - var user = (channel as Channel).GetUser(data.Author.Id); - - if (user != null) - { - Message msg = null; - msg = (channel as Channel).MessageManager.Add(data, user); - user.UpdateActivity(); - - Logger.Verbose($"MESSAGE_CREATE: {channel} ({user})"); - OnMessageReceived(msg); - } - else - Logger.Warning("MESSAGE_CREATE referenced an unknown user."); - } - else - Logger.Warning("MESSAGE_CREATE referenced an unknown channel."); - } - break; - case "MESSAGE_UPDATE": - { - var data = e.Payload.ToObject(Serializer); - var channel = GetChannel(data.ChannelId) as ITextChannel; - if (channel != null) - { - var msg = (channel as Channel).MessageManager.Get(data.Id, data.Author?.Id); - var before = Config.EnablePreUpdateEvents ? msg.Clone() : null; - msg.Update(data); - Logger.Verbose($"MESSAGE_UPDATE: {channel} ({data.Author?.Username ?? "Unknown"})"); - OnMessageUpdated(before, msg); - } - else - Logger.Warning("MESSAGE_UPDATE referenced an unknown channel."); - } - break; - case "MESSAGE_DELETE": - { - var data = e.Payload.ToObject(Serializer); - var channel = GetChannel(data.ChannelId) as ITextChannel; - if (channel != null) - { - var msg = (channel as Channel).MessageManager.Remove(data.Id); - Logger.Verbose($"MESSAGE_DELETE: {channel} ({msg.User?.Name ?? "Unknown"})"); - OnMessageDeleted(msg); - } - else - Logger.Warning("MESSAGE_DELETE referenced an unknown channel."); - } - break; - - //Statuses - case "PRESENCE_UPDATE": - { - var data = e.Payload.ToObject(Serializer); - User user; - Server server; - if (data.GuildId == null) - { - server = null; - user = GetPrivateChannel(data.User.Id)?.Recipient; - } - else - { - server = GetServer(data.GuildId.Value); - if (server == null) - { - Logger.Warning("PRESENCE_UPDATE referenced an unknown server."); - break; - } - else - user = server.GetUser(data.User.Id); - } - - if (user != null) - { - if (Config.LogLevel == LogSeverity.Debug) - Logger.Debug($"PRESENCE_UPDATE: {user}"); - var before = Config.EnablePreUpdateEvents ? user.Clone() : null; - user.Update(data); - OnUserUpdated(before, user); - } - /*else //Occurs when a user leaves a server - Logger.Warning("PRESENCE_UPDATE referenced an unknown user.");*/ - } - break; - case "TYPING_START": - { - var data = e.Payload.ToObject(Serializer); - var channel = GetChannel(data.ChannelId) as ITextChannel; - if (channel != null) - { - User user = (channel as Channel).GetUser(data.UserId); - if (user != null) - { - if (Config.LogLevel == LogSeverity.Debug) - Logger.Debug($"TYPING_START: {user.ToString(channel)}"); - OnUserIsTypingUpdated(channel, user); - user.UpdateActivity(); - } - } - } - break; - - //Voice - case "VOICE_STATE_UPDATE": - { - var data = e.Payload.ToObject(Serializer); - var server = GetServer(data.GuildId); - if (server != null) - { - var user = server.GetUser(data.UserId); - if (user != null) - { - if (Config.LogLevel == LogSeverity.Debug) - Logger.Debug($"VOICE_STATE_UPDATE: {user}"); - var before = Config.EnablePreUpdateEvents ? user.Clone() : null; - user.Update(data); - //Logger.Verbose($"Voice Updated: {server.Name}/{user.Name}"); - OnUserUpdated(before, user); - } - /*else //Occurs when a user leaves a server - Logger.Warning("VOICE_STATE_UPDATE referenced an unknown user.");*/ - } - else - Logger.Warning("VOICE_STATE_UPDATE referenced an unknown server."); - } - break; - - //Settings - case "USER_UPDATE": - { - 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); - Logger.Info($"USER_UPDATE"); - OnProfileUpdated(before, CurrentUser); - } - } - break; - - //Handled in GatewaySocket - case "RESUMED": - break; - - //Ignored - case "USER_SETTINGS_UPDATE": - case "GUILD_INTEGRATIONS_UPDATE": - case "VOICE_SERVER_UPDATE": - case "GUILD_EMOJIS_UPDATE": - case "MESSAGE_ACK": - Logger.Debug($"{e.Type} [Ignored]"); - break; - - //Others - default: - Logger.Warning($"Unknown message type: {e.Type}"); - break; - } - } - catch (Exception ex) - { - Logger.Error($"Error handling {e.Type} event", ex); - } + var channel = new DMChannel(model.Id, this, 0); + channel.Update(model); + return channel; } - #endregion - - #region Services - public T AddService(T instance) - where T : class, IService - => _services.Add(instance); - public T AddService() - where T : class, IService, new() - => _services.Add(new T()); - public T GetService(bool isRequired = true) - where T : class, IService - => _services.Get(isRequired); - #endregion - - #region Async Wrapper - /// 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) + internal virtual TextChannel CreateTextChannel(Guild guild, API.Channel model) { - asyncAction().GetAwaiter().GetResult(); - _disconnectedEvent.WaitOne(); + var channel = new TextChannel(model.Id, guild, 0, false); + channel.Update(model); + return channel; } - /// Blocking call and wait until the client has been manually stopped. This is mainly intended for use in console applications. - public void Wait() + internal virtual VoiceChannel CreateVoiceChannel(Guild guild, API.Channel model) { - _disconnectedEvent.WaitOne(); + var channel = new VoiceChannel(model.Id, guild, false); + channel.Update(model); + return channel; } - #endregion - - #region IDisposable - private bool _isDisposed = false; - - protected virtual void Dispose(bool isDisposing) + internal virtual GuildInvite CreateGuildInvite(GuildChannel channel, API.InviteMetadata model) { - if (!_isDisposed) - { - if (isDisposing) - { - _disconnectedEvent.Dispose(); - _connectedEvent.Dispose(); - } - _isDisposed = true; - } + var invite = new GuildInvite(model.Code, channel); + invite.Update(model); + return invite; } - - public void Dispose() + internal virtual PublicInvite CreatePublicInvite(API.Invite model) { - Dispose(true); + var invite = new PublicInvite(model.Code, this); + invite.Update(model); + return invite; } - #endregion - - //Helpers - private string GetTokenCachePath(string email) + internal virtual Guild CreateGuild(API.Guild model) { - using (var md5 = MD5.Create()) - { - byte[] data = md5.ComputeHash(Encoding.UTF8.GetBytes(email.ToLowerInvariant())); - StringBuilder filenameBuilder = new StringBuilder(); - for (int i = 0; i < data.Length; i++) - filenameBuilder.Append(data[i].ToString("x2")); - return Path.Combine(Config.CacheDir, filenameBuilder.ToString()); - } + var guild = new Guild(model.Id, this); + guild.Update(model); + return guild; } - private string LoadToken(string path, byte[] key) + internal virtual Message CreateMessage(IMessageChannel channel, User user, API.Message model) { - if (File.Exists(path)) - { - try - { - using (var fileStream = File.Open(path, FileMode.Open)) - using (var aes = Aes.Create()) - { - byte[] iv = new byte[aes.BlockSize / 8]; - fileStream.Read(iv, 0, iv.Length); - aes.IV = iv; - aes.Key = key; - using (var cryptoStream = new CryptoStream(fileStream, aes.CreateDecryptor(), CryptoStreamMode.Read)) - { - byte[] tokenBuffer = new byte[64]; - int length = cryptoStream.Read(tokenBuffer, 0, tokenBuffer.Length); - return Encoding.UTF8.GetString(tokenBuffer, 0, length); - } - } - } - catch (Exception ex) - { - Logger.Warning("Failed to load cached token. Wrong/changed password?", ex); - } - } - return null; + var msg = new Message(model.Id, channel, user); + msg.Update(model); + return msg; } - private void SaveToken(string path, byte[] key, string token) + internal virtual Role CreateRole(Guild guild, API.Role model) { - byte[] tokenBytes = Encoding.UTF8.GetBytes(token); - try - { - string parentDir = Path.GetDirectoryName(path); - if (!Directory.Exists(parentDir)) - Directory.CreateDirectory(parentDir); + var role = new Role(model.Id, guild); + role.Update(model); + return role; + } + internal virtual GuildUser CreateBannedUser(Guild guild, API.User model) + { + var user = new GuildUser(model.Id, guild, null, null); + user.Update(model); + return user; + } + internal virtual DMUser CreateDMUser(DMChannel channel, API.User model) + { + var user = new DMUser(model.Id, channel); + user.Update(model); + return user; + } + internal virtual GuildUser CreateGuildUser(Guild guild, GuildPresence presence, VoiceState voiceState, API.GuildMember model) + { + var user = new GuildUser(model.User.Id, guild, presence, voiceState); + user.Update(model); + return user; + } + internal virtual PublicUser CreatePublicUser(API.User model) + { + var user = new PublicUser(model.Id, this); + user.Update(model); + return user; + } + internal virtual SelfUser CreateSelfUser(API.User model) + { + var user = new SelfUser(model.Id, this); + user.Update(model); + return user; + } + internal virtual VoiceRegion CreateVoiceRegion(API.Rest.GetVoiceRegionsResponse model) + { + var region = new VoiceRegion(model.Id, this); + region.Update(model); + return region; + } - using (var fileStream = File.Open(path, FileMode.Create)) - using (var aes = Aes.Create()) + protected virtual void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) { - aes.GenerateIV(); - aes.Key = key; - using (var cryptoStream = new CryptoStream(fileStream, aes.CreateEncryptor(), CryptoStreamMode.Write)) - { - fileStream.Write(aes.IV, 0, aes.IV.Length); - cryptoStream.Write(tokenBytes, 0, tokenBytes.Length); - } + MessageQueue.Dispose(); + RestClient.Dispose(); + _connectionLock.Dispose(); } + _isDisposed = true; } - catch (Exception ex) + } + public void Dispose() => Dispose(true); + + private static string GetUserAgent(string appName, string appVersion, string appUrl) + { + var sb = new StringBuilder(); + if (!string.IsNullOrEmpty(appName)) { - Logger.Warning("Failed to cache token", ex); + sb.Append(appName); + if (!string.IsNullOrEmpty(appVersion)) + sb.Append($"/{appVersion}"); + if (!string.IsNullOrEmpty(appUrl)) + sb.Append($" ({appUrl})"); + sb.Append(' '); } + sb.Append($"DiscordBot ({DiscordConfig.LibUrl}, v{DiscordConfig.LibVersion})"); + return sb.ToString(); } + + protected void RaiseEvent(EventHandler eventHandler) + => eventHandler?.Invoke(this, EventArgs.Empty); + protected void RaiseEvent(EventHandler eventHandler, T eventArgs) where T : EventArgs + => eventHandler?.Invoke(this, eventArgs); + protected void RaiseEvent(EventHandler eventHandler, Func eventArgs) where T : EventArgs + => eventHandler?.Invoke(this, eventArgs()); } -} \ No newline at end of file +} diff --git a/src/Discord.Net/DiscordConfig.cs b/src/Discord.Net/DiscordConfig.cs index ab6f92eba..b23bc3204 100644 --- a/src/Discord.Net/DiscordConfig.cs +++ b/src/Discord.Net/DiscordConfig.cs @@ -1,15 +1,27 @@ -using System; -using System.IO; +using Discord.Net.Rest; +using Discord.Net.WebSockets; using System.Reflection; -using System.Text; namespace Discord { - public class DiscordConfigBuilder + public class DiscordConfig { - //Global + public const int MaxMessageSize = 2000; + public const int MaxMessagesPerBatch = 100; + + public const string LibName = "Discord.Net"; + public static string LibVersion { get; } = typeof(DiscordConfig).GetTypeInfo().Assembly?.GetName().Version.ToString(3) ?? "Unknown"; + public const string LibUrl = "https://github.com/RogueException/Discord.Net"; + + public const string ClientAPIUrl = "https://discordapp.com/api/"; + public const string CDNUrl = "https://cdn.discordapp.com/"; + public const string InviteUrl = "https://discord.gg/"; - /// Gets or sets name of your application, used both for the token cache directory and user agent. + internal const int RestTimeout = 10000; + internal const int MessageQueueInterval = 100; + internal const int WebSocketQueueInterval = 100; + + /// Gets or sets name of your application, used in the user agent. public string AppName { get; set; } = null; /// Gets or sets url to your application, used in the user agent. public string AppUrl { get; set; } = null; @@ -18,124 +30,40 @@ namespace Discord /// Gets or sets the minimum log level severity that will be sent to the LogMessage event. public LogSeverity LogLevel { get; set; } = LogSeverity.Info; - - //WebSocket - + /// Gets or sets the time (in milliseconds) to wait for the websocket to connect and initialize. public int ConnectionTimeout { get; set; } = 30000; - /// Gets or sets the time (in milliseconds) to wait after an unexpected disconnect before reconnecting. - public int ReconnectDelay { get; set; } = 1000; - /// Gets or sets the time (in milliseconds) to wait after an reconnect fails before retrying. - public int FailedReconnectDelay { get; set; } = 15000; + /// Gets or sets the time (in milliseconds) to wait after an unexpected disconnect before reconnecting. + public int ReconnectDelay { get; set; } = 1000; + /// Gets or sets the time (in milliseconds) to wait after an reconnect fails before retrying. + public int FailedReconnectDelay { get; set; } = 15000; //Performance - - /// Gets or sets whether an encrypted login token should be saved to temp dir after successful login. - public bool CacheToken { get; set; } = true; + /// Gets or sets the number of messages per channel that should be kept in cache. Setting this to zero disables the message cache entirely. public int MessageCacheSize { get; set; } = 100; /// /// Gets or sets whether the permissions cache should be used. - /// This makes operations such as User.GetPermissions(Channel), User.ServerPermissions, Channel.GetUser, and Channel.Members much faster while increasing memory usage. + /// This makes operations such as User.GetPermissions(Channel), User.GuildPermissions, Channel.GetUser, and Channel.Members much faster while increasing memory usage. /// public bool UsePermissionsCache { get; set; } = true; /// Gets or sets whether the a copy of a model is generated on an update event to allow you to check which properties changed. public bool EnablePreUpdateEvents { get; set; } = true; /// - /// Gets or sets the max number of users a server may have for offline users to be included in the READY packet. Max is 250. + /// Gets or sets the max number of users a guild may have for offline users to be included in the READY packet. Max is 250. /// Decreasing this may reduce CPU usage while increasing login time and network usage. /// public int LargeThreshold { get; set; } = 250; - //Events - - /// Gets or sets a handler for all log messages. - public EventHandler LogHandler { get; set; } - - public DiscordConfig Build() => new DiscordConfig(this); - } - - public class DiscordConfig - { - public const int MaxMessageSize = 2000; - internal const int RestTimeout = 10000; - internal const int MessageQueueInterval = 100; - internal const int WebSocketQueueInterval = 100; - - public const string LibName = "Discord.Net"; - public static string LibVersion => typeof(DiscordConfigBuilder).GetTypeInfo().Assembly.GetName().Version.ToString(3); - public const string LibUrl = "https://github.com/RogueException/Discord.Net"; - - public const string ClientAPIUrl = "https://discordapp.com/api/"; - public const string StatusAPIUrl = "https://srhpyqt94yxb.statuspage.io/api/v2/"; //"https://status.discordapp.com/api/v2/"; - public const string CDNUrl = "https://cdn.discordapp.com/"; - public const string InviteUrl = "https://discord.gg/"; + //Engines - public LogSeverity LogLevel { get; } - - public string AppName { get; } - public string AppUrl { get; } - public string AppVersion { get; } - public string UserAgent { get; } - public string CacheDir { get; } - - public int ConnectionTimeout { get; } - public int ReconnectDelay { get; } - public int FailedReconnectDelay { get; } - - public int LargeThreshold { get; } - public int MessageCacheSize { get; } - public bool UsePermissionsCache { get; } - public bool EnablePreUpdateEvents { get; } - - - internal DiscordConfig(DiscordConfigBuilder builder) - { - LogLevel = builder.LogLevel; - - AppName = builder.AppName; - AppUrl = builder.AppUrl; - AppVersion = builder.AppVersion; - UserAgent = GetUserAgent(builder); - CacheDir = GetCacheDir(builder); - - ConnectionTimeout = builder.ConnectionTimeout; - ReconnectDelay = builder.ReconnectDelay; - FailedReconnectDelay = builder.FailedReconnectDelay; - - MessageCacheSize = builder.MessageCacheSize; - UsePermissionsCache = builder.UsePermissionsCache; - EnablePreUpdateEvents = builder.EnablePreUpdateEvents; - } - - private static string GetUserAgent(DiscordConfigBuilder builder) - { - StringBuilder sb = new StringBuilder(); - if (!string.IsNullOrEmpty(builder.AppName)) - { - sb.Append(builder.AppName); - if (!string.IsNullOrEmpty(builder.AppVersion)) - { - sb.Append('/'); - sb.Append(builder.AppVersion); - } - if (!string.IsNullOrEmpty(builder.AppUrl)) - { - sb.Append(" ("); - sb.Append(builder.AppUrl); - sb.Append(')'); - } - sb.Append(' '); - } - sb.Append($"DiscordBot ({LibUrl}, v{LibVersion})"); - return sb.ToString(); - } - private static string GetCacheDir(DiscordConfigBuilder builder) - { - if (builder.CacheToken) - return Path.Combine(Path.GetTempPath(), builder.AppName ?? "Discord.Net"); - else - return null; - } + /// Gets or sets the REST engine to use. Defaults to DefaultRestClientProvider, which is built around .Net's HttpClient class. + public RestClientProvider RestClientProvider { get; set; } = (url, ct) => new DefaultRestEngine(url, ct); + /// + /// Gets or sets the WebSocket engine to use. Defaults to DefaultWebSocketProvider, which uses .Net's WebSocketClient class. + /// WebSockets are only used if DiscordClient.Connect() is called. + /// + public WebSocketProvider WebSocketProvider { get; set; } = null; } } + diff --git a/src/Discord.Net/DiscordSocketClient.cs b/src/Discord.Net/DiscordSocketClient.cs new file mode 100644 index 000000000..40beb389e --- /dev/null +++ b/src/Discord.Net/DiscordSocketClient.cs @@ -0,0 +1,176 @@ +using Discord.Logging; +using Discord.Net.WebSockets; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Discord +{ + public class DiscordSocketClient : DiscordClient + { + public event EventHandler Connected, Disconnected; + public event EventHandler VoiceConnected, VoiceDisconnected; + + public event EventHandler ChannelCreated, ChannelDestroyed; + public event EventHandler ChannelUpdated; + public event EventHandler MessageReceived, MessageDeleted; + public event EventHandler MessageUpdated; + public event EventHandler RoleCreated, RoleDeleted; + public event EventHandler RoleUpdated; + public event EventHandler JoinedGuild, LeftGuild; + public event EventHandler GuildAvailable, GuildUnavailable; + public event EventHandler GuildUpdated; + public event EventHandler CurrentUserUpdated; + public event EventHandler UserJoined, UserLeft; + public event EventHandler UserBanned, UserUnbanned; + public event EventHandler UserUpdated; + public event EventHandler UserIsTyping; + + private readonly Logger _discordLogger, _gatewayLogger; + private readonly int _connectionTimeout, _reconnectDelay, _failedReconnectDelay; + private readonly bool _enablePreUpdateEvents, _usePermissionCache; + private readonly int _largeThreshold, _messageCacheSize; + private ConcurrentDictionary _guilds; + private ConcurrentDictionary _channels; + private ConcurrentDictionary _privateChannels; //Key = RecipientId + + public int ConnectionId { get; } + public int TotalConnections { get; } + /// Gets the internal WebSocket for the Gateway event stream. + public GatewaySocket GatewaySocket { get; private set; } + /*/// Gets the current logged-in account. + public CurrentUser CurrentUser { get; private set; }*/ + + public bool IsConnected => GatewaySocket.State == ConnectionState.Connected; + + public DiscordSocketClient(string token, int connectionId = 0, int totalConnections = 1, DiscordConfig config = null) + : base(token, config) + { + if (totalConnections < 1) throw new ArgumentOutOfRangeException(nameof(totalConnections)); + if (connectionId < 0) throw new ArgumentOutOfRangeException(nameof(connectionId)); + if (connectionId >= totalConnections) throw new ArgumentException($"{nameof(connectionId)} must be less than {nameof(totalConnections)}.", nameof(connectionId)); + + ConnectionId = connectionId; + TotalConnections = totalConnections; + + _connectionTimeout = config.ConnectionTimeout; + _reconnectDelay = config.ReconnectDelay; + _failedReconnectDelay = config.FailedReconnectDelay; + + _messageCacheSize = config.MessageCacheSize; + _usePermissionCache = config.UsePermissionsCache; + _enablePreUpdateEvents = config.EnablePreUpdateEvents; + _largeThreshold = config.LargeThreshold; + + _discordLogger = _logManager.CreateLogger("Discord"); + _gatewayLogger = _logManager.CreateLogger("Gateway"); + + _guilds = new ConcurrentDictionary(2, 0); + _channels = new ConcurrentDictionary(2, 0); + _privateChannels = new ConcurrentDictionary(2, 0); + } + + protected override async Task LogoutInternal() + { + await DisconnectInternal().ConfigureAwait(false); + + _guilds.Clear(); + _channels.Clear(); + _privateChannels.Clear(); + + CurrentUser = null; + + await base.LogoutInternal(); + } + + public async Task Connect() + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + await ConnectInternal().ConfigureAwait(false); + } + finally { _connectionLock.Release(); } + } + protected virtual Task ConnectInternal() + { + throw new NotImplementedException(); + //GatewaySocket = new GatewaySocket(_webSocketProvider(_cancelToken.Token)); + //GatewaySocket.SetHeader("user-agent", _userAgent); + //await GatewaySocket.Connect(_cancelToken.Token).ConfigureAwait(false); + } + + public async Task Disconnect() + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + await DisconnectInternal().ConfigureAwait(false); + } + finally { _connectionLock.Release(); } + } + protected virtual async Task DisconnectInternal() + { + if (GatewaySocket != null) + { + await GatewaySocket.Disconnect().ConfigureAwait(false); + GatewaySocket = null; + } + } + + public override async Task GetOrCreateDMChannel(ulong userId) + { + DMChannel channel; + if (_privateChannels.TryGetValue(userId, out channel)) + return channel; + + return await base.GetOrCreateDMChannel(userId).ConfigureAwait(false); + } + public override Task> GetDMChannels() + { + return Task.FromResult(_privateChannels.Select(x => x.Value)); + } + public override Task> GetGuilds() + { + return Task.FromResult(_guilds.Select(x => x.Value)); + } + public override Task GetGuild(ulong id) + { + Guild guild; + _guilds.TryGetValue(id, out guild); + return Task.FromResult(guild); + } + + internal override DMChannel CreateDMChannel(API.Channel model) + { + var channel = new DMChannel(model.Id, this, _messageCacheSize); + channel.Update(model); + return channel; + } + internal override TextChannel CreateTextChannel(Guild guild, API.Channel model) + { + var channel = new TextChannel(model.Id, guild, _messageCacheSize, _usePermissionCache); + channel.Update(model); + return channel; + } + internal override VoiceChannel CreateVoiceChannel(Guild guild, API.Channel model) + { + var channel = new VoiceChannel(model.Id, guild, _usePermissionCache); + channel.Update(model); + return channel; + } + + protected override void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + GatewaySocket.Dispose(); + } + } + } + } +} diff --git a/src/Discord.Net/DynamicIL.cs b/src/Discord.Net/DynamicIL.cs deleted file mode 100644 index bce89424d..000000000 --- a/src/Discord.Net/DynamicIL.cs +++ /dev/null @@ -1,49 +0,0 @@ -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 deleted file mode 100644 index 9e1e4e8ef..000000000 --- a/src/Discord.Net/ETF/ETFReader.cs +++ /dev/null @@ -1,491 +0,0 @@ -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 class ETFReader : IDisposable - { - private static readonly ConcurrentDictionary _deserializers = new ConcurrentDictionary(); - private static readonly Dictionary _readMethods = GetPrimitiveReadMethods(); - - private readonly Stream _stream; - private readonly byte[] _buffer; - private readonly bool _leaveOpen; - private readonly Encoding _encoding; - - public ETFReader(Stream stream, bool leaveOpen = false) - { - if (stream == null) throw new ArgumentNullException(nameof(stream)); - - _stream = stream; - _leaveOpen = leaveOpen; - _buffer = new byte[11]; - _encoding = Encoding.UTF8; - } - - public bool ReadBool() - { - _stream.Read(_buffer, 0, 1); - ETFType type = (ETFType)_buffer[0]; - if (type == ETFType.SMALL_ATOM_EXT) - { - _stream.Read(_buffer, 0, 1); - switch (_buffer[0]) //Length - { - case 4: - ReadTrue(); - return true; - case 5: - ReadFalse(); - return false; - } - } - throw new InvalidDataException(); - } - private void ReadTrue() - { - _stream.Read(_buffer, 0, 4); - if (_buffer[0] != 't' || _buffer[1] != 'r' || _buffer[2] != 'u' || _buffer[3] != 'e') - throw new InvalidDataException(); - } - private void ReadFalse() - { - _stream.Read(_buffer, 0, 5); - if (_buffer[0] != 'f' || _buffer[1] != 'a' || _buffer[2] != 'l' || _buffer[3] != 's' || _buffer[4] != 'e') - throw new InvalidDataException(); - } - - public int ReadSByte() - { - _stream.Read(_buffer, 0, 1); - ETFType type = (ETFType)_buffer[0]; - return (sbyte)ReadLongInternal(type); - } - public uint ReadByte() - { - _stream.Read(_buffer, 0, 1); - ETFType type = (ETFType)_buffer[0]; - return (byte)ReadLongInternal(type); - } - public int ReadShort() - { - _stream.Read(_buffer, 0, 1); - ETFType type = (ETFType)_buffer[0]; - return (short)ReadLongInternal(type); - } - public uint ReadUShort() - { - _stream.Read(_buffer, 0, 1); - ETFType type = (ETFType)_buffer[0]; - return (ushort)ReadLongInternal(type); - } - public int ReadInt() - { - _stream.Read(_buffer, 0, 1); - ETFType type = (ETFType)_buffer[0]; - return (int)ReadLongInternal(type); - } - public uint ReadUInt() - { - _stream.Read(_buffer, 0, 1); - ETFType type = (ETFType)_buffer[0]; - return (uint)ReadLongInternal(type); - } - public long ReadLong() - { - _stream.Read(_buffer, 0, 1); - ETFType type = (ETFType)_buffer[0]; - return ReadLongInternal(type); - } - public ulong ReadULong() - { - _stream.Read(_buffer, 0, 1); - ETFType type = (ETFType)_buffer[0]; - return (ulong)ReadLongInternal(type); - } - public long ReadLongInternal(ETFType type) - { - switch (type) - { - case ETFType.SMALL_INTEGER_EXT: - _stream.Read(_buffer, 0, 1); - return _buffer[0]; - case ETFType.INTEGER_EXT: - _stream.Read(_buffer, 0, 4); - return (_buffer[0] << 24) | (_buffer[1] << 16) | (_buffer[2] << 8) | (_buffer[3]); - case ETFType.SMALL_BIG_EXT: - _stream.Read(_buffer, 0, 2); - bool isPositive = _buffer[0] == 0; - byte count = _buffer[1]; - - int shiftValue = (count - 1) * 8; - ulong value = 0; - _stream.Read(_buffer, 0, count); - for (int i = 0; i < count; i++, shiftValue -= 8) - value = value + _buffer[i] << shiftValue; - if (!isPositive) - return -(long)value; - else - return (long)value; - } - throw new InvalidDataException(); - } - - public float ReadSingle() - { - _stream.Read(_buffer, 0, 1); - ETFType type = (ETFType)_buffer[0]; - return (float)ReadDoubleInternal(type); - } - public double ReadDouble() - { - _stream.Read(_buffer, 0, 1); - ETFType type = (ETFType)_buffer[0]; - return ReadDoubleInternal(type); - } - public double ReadDoubleInternal(ETFType type) - { - throw new NotImplementedException(); - } - - public bool? ReadNullableBool() - { - _stream.Read(_buffer, 0, 1); - ETFType type = (ETFType)_buffer[0]; - if (type == ETFType.SMALL_ATOM_EXT) - { - _stream.Read(_buffer, 0, 1); - switch (_buffer[0]) //Length - { - case 3: - if (ReadNil()) - return null; - break; - case 4: - ReadTrue(); - return true; - case 5: - ReadFalse(); - return false; - } - } - throw new InvalidDataException(); - } - public int? ReadNullableSByte() - { - _stream.Read(_buffer, 0, 1); - ETFType type = (ETFType)_buffer[0]; - if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null; - return (sbyte)ReadLongInternal(type); - } - public uint? ReadNullableByte() - { - _stream.Read(_buffer, 0, 1); - ETFType type = (ETFType)_buffer[0]; - if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null; - return (byte)ReadLongInternal(type); - } - public int? ReadNullableShort() - { - _stream.Read(_buffer, 0, 1); - ETFType type = (ETFType)_buffer[0]; - if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null; - return (short)ReadLongInternal(type); - } - public uint? ReadNullableUShort() - { - _stream.Read(_buffer, 0, 1); - ETFType type = (ETFType)_buffer[0]; - if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null; - return (ushort)ReadLongInternal(type); - } - public int? ReadNullableInt() - { - _stream.Read(_buffer, 0, 1); - ETFType type = (ETFType)_buffer[0]; - if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null; - return (int)ReadLongInternal(type); - } - public uint? ReadNullableUInt() - { - _stream.Read(_buffer, 0, 1); - ETFType type = (ETFType)_buffer[0]; - if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null; - return (uint)ReadLongInternal(type); - } - public long? ReadNullableLong() - { - _stream.Read(_buffer, 0, 1); - ETFType type = (ETFType)_buffer[0]; - if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null; - return ReadLongInternal(type); - } - public ulong? ReadNullableULong() - { - _stream.Read(_buffer, 0, 1); - ETFType type = (ETFType)_buffer[0]; - if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null; - return (ulong)ReadLongInternal(type); - } - public float? ReadNullableSingle() - { - _stream.Read(_buffer, 0, 1); - ETFType type = (ETFType)_buffer[0]; - if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null; - return (float)ReadDoubleInternal(type); - } - public double? ReadNullableDouble() - { - _stream.Read(_buffer, 0, 1); - ETFType type = (ETFType)_buffer[0]; - if (type == ETFType.SMALL_ATOM_EXT && ReadNil()) return null; - return ReadDoubleInternal(type); - } - - public string ReadString() - { - throw new NotImplementedException(); - } - public byte[] ReadByteArray() - { - throw new NotImplementedException(); - } - - public T Read() - where T : new() - { - var type = typeof(T); - var typeInfo = type.GetTypeInfo(); - var action = _deserializers.GetOrAdd(type, _ => CreateDeserializer(type, typeInfo)) as Func; - return action(this); - } - /*public void Read() - where T : Nullable - where U : struct, new() - { - }*/ - public T[] ReadArray() - { - throw new NotImplementedException(); - } - public IDictionary ReadDictionary() - { - throw new NotImplementedException(); - } - /*public object Read(object obj) - { - throw new NotImplementedException(); - }*/ - - private bool ReadNil(bool ignoreLength = false) - { - if (!ignoreLength) - { - _stream.Read(_buffer, 0, 1); - byte length = _buffer[0]; - if (length != 3) return false; - } - - _stream.Read(_buffer, 0, 3); - if (_buffer[0] == 'n' && _buffer[1] == 'i' && _buffer[2] == 'l') - return true; - - return false; - } - - #region Emit - private static Func CreateDeserializer(Type type, TypeInfo typeInfo) - where T : new() - { - var method = new DynamicMethod("DeserializeETF", type, new[] { typeof(ETFReader) }, true); - var generator = method.GetILGenerator(); - - generator.Emit(OpCodes.Ldarg_0); //ETFReader(this) - EmitReadValue(generator, type, typeInfo, true); - - generator.Emit(OpCodes.Ret); - return method.CreateDelegate(typeof(Func)) as Func; - } - private static void EmitReadValue(ILGenerator generator, Type type, TypeInfo typeInfo, bool isTop) - { - //Convert enum types to their base type - if (typeInfo.IsEnum) - { - type = Enum.GetUnderlyingType(type); - typeInfo = type.GetTypeInfo(); - } - //Primitives/Enums - if (!typeInfo.IsEnum && IsType(type, typeof(sbyte), typeof(byte), typeof(short), - typeof(ushort), typeof(int), typeof(uint), 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)*/)) - { - //No conversion needed - generator.EmitCall(OpCodes.Call, GetReadMethod(type), null); - } - //Dictionaries - /*else if (!typeInfo.IsValueType && typeInfo.ImplementedInterfaces - .Any(x => x.IsConstructedGenericType && x.GetGenericTypeDefinition() == typeof(IDictionary<,>))) - { - generator.EmitCall(OpCodes.Call, _writeDictionaryTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null); - } - //Enumerable - else if (!typeInfo.IsValueType && typeInfo.ImplementedInterfaces - .Any(x => x.IsConstructedGenericType && x.GetGenericTypeDefinition() == typeof(IEnumerable<>))) - { - generator.EmitCall(OpCodes.Call, _writeEnumerableTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null); - } - //Nullable Structs - else if (typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(Nullable<>) && - typeInfo.GenericTypeParameters[0].GetTypeInfo().IsValueType) - { - generator.EmitCall(OpCodes.Call, _writeNullableTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null); - } - //Structs/Classes - else if (typeInfo.IsClass || (typeInfo.IsValueType && !typeInfo.IsPrimitive)) - { - if (isTop) - { - typeInfo.ForEachField(f => - { - string name; - if (!f.IsPublic || !IsETFProperty(f, out name)) return; - - generator.Emit(OpCodes.Ldarg_0); //ETFReader(this) - generator.Emit(OpCodes.Ldstr, name); //ETFReader(this), name - generator.EmitCall(OpCodes.Call, GetWriteMethod(typeof(string)), null); - generator.Emit(OpCodes.Ldarg_0); //ETFReader(this) - generator.Emit(OpCodes.Ldarg_1); //ETFReader(this), obj - generator.Emit(OpCodes.Ldfld, f); //ETFReader(this), obj.fieldValue - EmitWriteValue(generator, f.FieldType, f.FieldType.GetTypeInfo(), false); - }); - - typeInfo.ForEachProperty(p => - { - string name; - if (!p.CanRead || !p.GetMethod.IsPublic || !IsETFProperty(p, out name)) return; - - generator.Emit(OpCodes.Ldarg_0); //ETFReader(this) - generator.Emit(OpCodes.Ldstr, name); //ETFReader(this), name - generator.EmitCall(OpCodes.Call, GetWriteMethod(typeof(string)), null); - generator.Emit(OpCodes.Ldarg_0); //ETFReader(this) - generator.Emit(OpCodes.Ldarg_1); //ETFReader(this), obj - generator.EmitCall(OpCodes.Callvirt, p.GetMethod, null); //ETFReader(this), obj.propValue - EmitWriteValue(generator, p.PropertyType, p.PropertyType.GetTypeInfo(), false); - }); - } - else - { - //While we could drill deeper and make a large serializer that also serializes all subclasses, - //it's more efficient to serialize on a per-type basis via another Write call. - generator.EmitCall(OpCodes.Call, _writeTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null); - } - }*/ - //Unsupported (decimal, char) - else - throw new InvalidOperationException($"Deserializing {type.Name} is not supported."); - } - - private static bool IsType(Type type, params Type[] types) - { - for (int i = 0; i < types.Length; i++) - { - if (type == types[i]) - return true; - } - return false; - } - private static 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 static 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; - } - - private static MethodInfo GetReadMethod(string name) - => typeof(ETFReader).GetTypeInfo().GetDeclaredMethods(name).Single(); - private static MethodInfo GetReadMethod(Type type) - { - MethodInfo method; - if (_readMethods.TryGetValue(type, out method)) - return method; - return null; - } - private static Dictionary GetPrimitiveReadMethods() - { - return new Dictionary - { - { typeof(bool), GetReadMethod(nameof(ReadBool)) }, - { typeof(bool?), GetReadMethod(nameof(ReadNullableBool)) }, - { typeof(byte), GetReadMethod(nameof(ReadByte)) }, - { typeof(byte?), GetReadMethod(nameof(ReadNullableByte)) }, - { typeof(sbyte), GetReadMethod(nameof(ReadSByte)) }, - { typeof(sbyte?), GetReadMethod(nameof(ReadNullableSByte)) }, - { typeof(short), GetReadMethod(nameof(ReadShort)) }, - { typeof(short?), GetReadMethod(nameof(ReadNullableShort)) }, - { typeof(ushort), GetReadMethod(nameof(ReadUShort)) }, - { typeof(ushort?), GetReadMethod(nameof(ReadNullableUShort)) }, - { typeof(int), GetReadMethod(nameof(ReadInt)) }, - { typeof(int?), GetReadMethod(nameof(ReadNullableInt)) }, - { typeof(uint), GetReadMethod(nameof(ReadUInt)) }, - { typeof(uint?), GetReadMethod(nameof(ReadNullableUInt)) }, - { typeof(long), GetReadMethod(nameof(ReadLong)) }, - { typeof(long?), GetReadMethod(nameof(ReadNullableLong)) }, - { typeof(ulong), GetReadMethod(nameof(ReadULong)) }, - { typeof(ulong?), GetReadMethod(nameof(ReadNullableULong)) }, - { typeof(float), GetReadMethod(nameof(ReadSingle)) }, - { typeof(float?), GetReadMethod(nameof(ReadNullableSingle)) }, - { typeof(double), GetReadMethod(nameof(ReadDouble)) }, - { typeof(double?), GetReadMethod(nameof(ReadNullableDouble)) }, - }; - } - #endregion - - #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 - } -} \ No newline at end of file diff --git a/src/Discord.Net/ETF/ETFType.cs b/src/Discord.Net/ETF/ETFType.cs deleted file mode 100644 index 53499d5fa..000000000 --- a/src/Discord.Net/ETF/ETFType.cs +++ /dev/null @@ -1,32 +0,0 @@ -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 deleted file mode 100644 index 37d1553db..000000000 --- a/src/Discord.Net/ETF/ETFWriter.cs +++ /dev/null @@ -1,482 +0,0 @@ -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 static readonly ConcurrentDictionary _serializers = new ConcurrentDictionary(); - private static readonly ConcurrentDictionary _indirectSerializers = new ConcurrentDictionary(); - - private static readonly byte[] _nilBytes = new byte[] { (byte)ETFType.SMALL_ATOM_EXT, 3, (byte)'n', (byte)'i', (byte)'l' }; - private static readonly byte[] _falseBytes = new byte[] { (byte)ETFType.SMALL_ATOM_EXT, 5, (byte)'f', (byte)'a', (byte)'l', (byte)'s', (byte)'e' }; - private static readonly byte[] _trueBytes = new byte[] { (byte)ETFType.SMALL_ATOM_EXT, 4, (byte)'t', (byte)'r', (byte)'u', (byte)'e' }; - - private static readonly MethodInfo _writeTMethod = GetGenericWriteMethod(null); - private static readonly MethodInfo _writeNullableTMethod = GetGenericWriteMethod(typeof(Nullable<>)); - private static readonly MethodInfo _writeDictionaryTMethod = GetGenericWriteMethod(typeof(IDictionary<,>)); - private static readonly MethodInfo _writeEnumerableTMethod = GetGenericWriteMethod(typeof(IEnumerable<>)); - private static readonly 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; - - 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; - } - - 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) - { - //TODO: Does this encode negatives correctly? - _buffer[0] = (byte)ETFType.INTEGER_EXT; - _buffer[1] = (byte)(value >> 24); - _buffer[2] = (byte)(value >> 16); - _buffer[3] = (byte)(value >> 8); - _buffer[4] = (byte)value; - _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); - _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); - _buffer[2] = (byte)(value >> 16); - _buffer[3] = (byte)(value >> 8); - _buffer[4] = (byte)value; - _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); - _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); - _buffer[2] = (byte)(value2 >> 48); - _buffer[3] = (byte)(value2 >> 40); - _buffer[4] = (byte)(value2 >> 32); - _buffer[5] = (byte)(value2 >> 24); - _buffer[6] = (byte)(value2 >> 16); - _buffer[7] = (byte)(value2 >> 8); - _buffer[8] = (byte)value2; - _stream.Write(_buffer, 0, 9); - } - - public void Write(DateTime value) => Write((ulong)((value.Ticks - _epochTime.Ticks) / TimeSpan.TicksPerSecond)); - - public void Write(bool? value) { if (value.HasValue) Write(value.Value); else WriteNil(); } - public void Write(sbyte? value) { if (value.HasValue) Write((long)value.Value); else WriteNil(); } - public void Write(byte? 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(ushort? 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(uint? value) { if (value.HasValue) Write((ulong)value.Value); else WriteNil(); } - public void Write(long? 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(double? 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(DateTime? value) { if (value.HasValue) Write(value.Value); 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); - _buffer[2] = (byte)(count >> 16); - _buffer[3] = (byte)(count >> 8); - _buffer[4] = (byte)count; - _stream.Write(_buffer, 0, 5); - _stream.Write(bytes, 0, bytes.Length); - } - 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); - _buffer[2] = (byte)(count >> 16); - _buffer[3] = (byte)(count >> 8); - _buffer[4] = (byte)count; - _stream.Write(_buffer, 0, 5); - _stream.Write(value, 0, value.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); - _buffer[2] = (byte)(length >> 16); - _buffer[3] = (byte)(length >> 8); - _buffer[4] = (byte)length; - _stream.Write(_buffer, 0, 5); - - for (int i = 0; i < array.Length; i++) - Write(array[i]); - - _buffer[0] = (byte)ETFType.NIL_EXT; - _stream.Write(_buffer, 0, 1); - } - 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); - _buffer[2] = (byte)(length >> 16); - _buffer[3] = (byte)(length >> 8); - _buffer[4] = (byte)length; - _stream.Write(_buffer, 0, 5); - - foreach (var pair in obj) - { - Write(pair.Key); - Write(pair.Value); - } - } - 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(); - } - - private void WriteNil() => _stream.Write(_nilBytes, 0, _nilBytes.Length); - - public virtual void Flush() => _stream.Flush(); - public virtual long Seek(int offset, SeekOrigin origin) => _stream.Seek(offset, origin); - - #region Emit - private static Action CreateSerializer(Type type, TypeInfo typeInfo, bool isDirect) - { - var method = new DynamicMethod(isDirect ? "SerializeETF" : "SerializeIndirectETF", - null, new[] { typeof(ETFWriter), isDirect ? type : typeof(object) }, true); - var generator = method.GetILGenerator(); - - generator.Emit(OpCodes.Ldarg_0); //ETFWriter(this) - generator.Emit(OpCodes.Ldarg_1); //ETFWriter(this), value - if (!isDirect) - { - if (typeInfo.IsValueType) //Unbox value types - generator.Emit(OpCodes.Unbox_Any, type); //ETFWriter(this), real_value - else //Cast reference types - generator.Emit(OpCodes.Castclass, type); //ETFWriter(this), real_value - generator.EmitCall(OpCodes.Call, _writeTMethod.MakeGenericMethod(type), null); //Call generic version - } - else - EmitWriteValue(generator, type, typeInfo, true); - - generator.Emit(OpCodes.Ret); - return method.CreateDelegate(typeof(Action)) as Action; - } - private static void EmitWriteValue(ILGenerator generator, Type type, TypeInfo typeInfo, bool isTop) - { - //Convert enum types to their base type - if (typeInfo.IsEnum) - { - type = Enum.GetUnderlyingType(type); - typeInfo = type.GetTypeInfo(); - } - - //Primitives/Enums - 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))) - { - //No conversion needed - targetType = type; - } - else if (IsType(type, typeof(sbyte), typeof(short), typeof(int))) - { - //Convert to long - generator.Emit(OpCodes.Conv_I8); - targetType = typeof(long); - } - else if (IsType(type, typeof(byte), typeof(ushort), typeof(uint))) - { - //Convert to ulong - generator.Emit(OpCodes.Conv_U8); - targetType = typeof(ulong); - } - else if (IsType(type, typeof(float))) - { - //Convert to double - generator.Emit(OpCodes.Conv_R8); - targetType = typeof(double); - } - if (targetType != null) - generator.EmitCall(OpCodes.Call, GetWriteMethod(targetType), null); - - //Dictionaries - else if (!typeInfo.IsValueType && typeInfo.ImplementedInterfaces - .Any(x => x.IsConstructedGenericType && x.GetGenericTypeDefinition() == typeof(IDictionary<,>))) - { - generator.EmitCall(OpCodes.Call, _writeDictionaryTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null); - } - //Enumerable - else if (!typeInfo.IsValueType && typeInfo.ImplementedInterfaces - .Any(x => x.IsConstructedGenericType && x.GetGenericTypeDefinition() == typeof(IEnumerable<>))) - { - generator.EmitCall(OpCodes.Call, _writeEnumerableTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null); - } - //Nullable Structs - else if (typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(Nullable<>) && - typeInfo.GenericTypeParameters[0].GetTypeInfo().IsValueType) - { - generator.EmitCall(OpCodes.Call, _writeNullableTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null); - } - //Structs/Classes - else if (typeInfo.IsClass || (typeInfo.IsValueType && !typeInfo.IsPrimitive)) - { - if (isTop) - { - 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 - EmitWriteValue(generator, f.FieldType, f.FieldType.GetTypeInfo(), false); - }); - - 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 - EmitWriteValue(generator, p.PropertyType, p.PropertyType.GetTypeInfo(), false); - }); - } - else - { - //While we could drill deeper and make a large serializer that also serializes all subclasses, - //it's more efficient to serialize on a per-type basis via another Write call. - generator.EmitCall(OpCodes.Call, _writeTMethod.MakeGenericMethod(typeInfo.GenericTypeParameters), null); - } - } - //Unsupported (decimal, char) - else - throw new InvalidOperationException($"Serializing {type.Name} is not supported."); - } - - private static bool IsType(Type type, params Type[] types) - { - for (int i = 0; i < types.Length; i++) - { - if (type == types[i]) - return true; - } - return false; - } - private static 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 static 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; - } - - private static MethodInfo GetWriteMethod(Type paramType) - { - return typeof(ETFWriter).GetTypeInfo().GetDeclaredMethods(nameof(Write)) - .Where(x => x.GetParameters()[0].ParameterType == paramType) - .Single(); - } - private static MethodInfo GetGenericWriteMethod(Type genericType) - { - if (genericType == null) - { - return typeof(ETFWriter).GetTypeInfo() - .GetDeclaredMethods(nameof(Write)) - .Where(x => x.IsGenericMethodDefinition && x.GetParameters()[0].ParameterType == x.GetGenericArguments()[0]) - .Single(); - } - else - { - return 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() == genericType; - }) - .Single(); - } - } - #endregion - - #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/Entities/Attachment.cs b/src/Discord.Net/Entities/Attachment.cs new file mode 100644 index 000000000..5f2c1ed47 --- /dev/null +++ b/src/Discord.Net/Entities/Attachment.cs @@ -0,0 +1,18 @@ +using Model = Discord.API.Attachment; + +namespace Discord +{ + public struct Attachment + { + public ulong Id { get; } + public int Size { get; } + public string Filename { get; } + + public Attachment(Model model) + { + Id = model.Id; + Size = model.Size; + Filename = model.Filename; + } + } +} diff --git a/src/Discord.Net/Entities/Channel.cs b/src/Discord.Net/Entities/Channel.cs deleted file mode 100644 index 5894ade45..000000000 --- a/src/Discord.Net/Entities/Channel.cs +++ /dev/null @@ -1,55 +0,0 @@ -using APIChannel = Discord.API.Client.Channel; -using System.Collections.Generic; - -namespace Discord -{ - public abstract class Channel : IChannel - { - /// An entry in a public channel's permissions that gives or takes permissions from a specific role or user. - public class PermissionRule - { - /// The type of object TargetId is referring to. - public PermissionTarget TargetType { get; } - /// The Id of an object, whos type is specified by TargetType, that is the target of permissions being added or taken away. - public ulong TargetId { get; } - /// A collection of permissions that are added or taken away from the target. - public ChannelTriStatePermissions Permissions { get; } - - internal PermissionRule(PermissionTarget targetType, ulong targetId, uint allow, uint deny) - { - TargetType = targetType; - TargetId = targetId; - Permissions = new ChannelTriStatePermissions(allow, deny); - } - } - - /// Gets the unique identifier for this channel. - public ulong Id { get; } - - public abstract DiscordClient Client { get; } - /// Gets the type of this channel. - public abstract ChannelType Type { get; } - public bool IsText => (Type & ChannelType.Text) != 0; - public bool IsVoice => (Type & ChannelType.Voice) != 0; - public bool IsPrivate => (Type & ChannelType.Private) != 0; - public bool IsPublic => (Type & ChannelType.Public) != 0; - - public abstract User CurrentUser { get; } - /// Gets a collection of all users in this channel. - public abstract IEnumerable Users { get; } - - internal abstract MessageManager MessageManager { get; } - internal abstract PermissionManager PermissionManager { get; } - - protected Channel(ulong id) - { - Id = id; - } - - internal abstract void Update(APIChannel model); - - internal abstract User GetUser(ulong id); - - internal abstract Channel Clone(); - } -} diff --git a/src/Discord.Net/Entities/Channels/DMChannel.cs b/src/Discord.Net/Entities/Channels/DMChannel.cs new file mode 100644 index 000000000..f8ce92dfa --- /dev/null +++ b/src/Discord.Net/Entities/Channels/DMChannel.cs @@ -0,0 +1,99 @@ +using Discord.API.Rest; +using Discord.Net; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Net; +using System.Threading.Tasks; +using Model = Discord.API.Channel; + +namespace Discord +{ + public class DMChannel : IEntity, IMessageChannel + { + /// + public ulong Id { get; } + /// + public DiscordClient Discord { get; } + + /// + public DMUser Recipient { get; private set; } + + /// + public string Name => $"@{Recipient.Username}#{Recipient.Discriminator}"; + /// + public IEnumerable Users => ImmutableArray.Create(Discord.CurrentUser, Recipient); + /// + ChannelType IChannel.Type => ChannelType.DM; + /// + IEnumerable IChannel.Users => Users; + + private readonly MessageManager _messages; + + internal DMChannel(ulong id, DiscordClient client, int messageCacheSize) + { + Id = id; + Discord = client; + _messages = new MessageManager(this, messageCacheSize); + } + internal void Update(Model model) + { + if (Recipient == null) + Recipient = Discord.CreateDMUser(this, model.Recipient); + else + Recipient.Update(model.Recipient); + } + + /// + public User GetUser(ulong id) + { + if (id == Recipient.Id) + return Recipient; + else if (id == Discord.CurrentUser.Id) + return Discord.CurrentUser; + else + return null; + } + + /// + public Task GetMessage(ulong id) + => _messages.Get(id); + /// + public Task> GetMessages(int limit = 100) + => _messages.GetMany(limit); + /// + public Task> GetMessages(int limit = 100, ulong? relativeMessageId = null, Relative relativeDir = Relative.Before) + => _messages.GetMany(limit, relativeMessageId, relativeDir); + + /// + public Task SendMessage(string text, bool isTTS = false) + => _messages.Send(text, isTTS); + /// + public Task SendFile(string filePath, string text = null, bool isTTS = false) + => _messages.SendFile(filePath, text, isTTS); + /// + public Task SendFile(Stream stream, string filename, string text = null, bool isTTS = false) + => _messages.SendFile(stream, filename, text, isTTS); + + /// + public Task TriggerTyping() + => _messages.TriggerTyping(); + + /// + public async Task Delete() + { + try { await Discord.RestClient.Send(new DeleteChannelRequest(Id)).ConfigureAwait(false); } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } + } + /// + public async Task Update() + { + var response = await Discord.RestClient.Send(new GetChannelRequest(Id)).ConfigureAwait(false); + if (response != null) + Update(response); + } + + /// + public override string ToString() => Name; + } +} diff --git a/src/Discord.Net/Entities/Channels/GuildChannel.cs b/src/Discord.Net/Entities/Channels/GuildChannel.cs new file mode 100644 index 000000000..862c91671 --- /dev/null +++ b/src/Discord.Net/Entities/Channels/GuildChannel.cs @@ -0,0 +1,130 @@ +using Discord.API.Rest; +using Discord.Net; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Model = Discord.API.Channel; + +namespace Discord +{ + public abstract class GuildChannel : IChannel, IEntity + { + private readonly PermissionManager _permissions; + + /// + public ulong Id { get; } + /// Gets the guild this channel is a member of. + public Guild Guild { get; } + /// + public abstract ChannelType Type { get; } + + /// + public string Name { get; private set; } + /// Gets the position of this public channel relative to others of the same type. + public int Position { get; private set; } + + /// + public DiscordClient Discord => Guild.Discord; + /// Gets a collection of all users in this channel. + public IEnumerable Users => _permissions.GetUsers(); + /// + IEnumerable IChannel.Users => _permissions.GetUsers(); + /// Gets a collection of permission overwrites for this channel. + public IEnumerable PermissionOverwrites => _permissions.Overwrites; + + internal GuildChannel(ulong id, Guild guild, bool usePermissionsCache) + { + Id = id; + Guild = guild; + + _permissions = new PermissionManager(this, usePermissionsCache); + } + + internal virtual void Update(Model model) + { + Name = model.Name; + Position = model.Position; + + _permissions.Update(model); + } + + /// Gets a user in this channel with the given id. + public GuildUser GetUser(ulong id) + => _permissions.GetUser(id); + /// + User IChannel.GetUser(ulong id) => GetUser(id); + + /// Gets the permission overwrite for a specific user, or null if one does not exist. + public OverwritePermissions? GetPermissionOverwrite(GuildUser user) + => _permissions.GetOverwrite(user); + /// Gets the permission overwrite for a specific role, or null if one does not exist. + public OverwritePermissions? GetPermissionOverwrite(Role role) + => _permissions.GetOverwrite(role); + /// Downloads a collection of all invites to this channel. + public async Task> GetInvites() + { + var response = await Discord.RestClient.Send(new GetChannelInvitesRequest(Id)).ConfigureAwait(false); + return response.Select(x => + { + var invite = Discord.CreateGuildInvite(this, x); + invite.Update(x); + return invite; + }); + } + + /// Adds or updates the permission overwrite for the given user. + public Task UpdatePermissionOverwrite(GuildUser user, OverwritePermissions permissions) + => _permissions.AddOrUpdateOverwrite(user, permissions); + /// Adds or updates the permission overwrite for the given role. + public Task UpdatePermissionOverwrite(Role role, OverwritePermissions permissions) + => _permissions.AddOrUpdateOverwrite(role, permissions); + /// Removes the permission overwrite for the given user, if one exists. + public Task RemovePermissionOverwrite(GuildUser user) + => _permissions.RemoveOverwrite(user); + /// Removes the permission overwrite for the given role, if one exists. + public Task RemovePermissionOverwrite(Role role) + => _permissions.RemoveOverwrite(role); + + internal ChannelPermissions GetPermissions(GuildUser user) + => _permissions.GetPermissions(user); + internal void UpdatePermissions() + => _permissions.UpdatePermissions(); + internal void UpdatePermissions(GuildUser user) + => _permissions.UpdatePermissions(user); + + /// Creates a new invite to this channel. + /// Time (in seconds) until the invite expires. Set to null to never expire. + /// The max amount of times this invite may be used. Set to null to have unlimited uses. + /// If true, a user accepting this invite will be kicked from the guild after closing their client. + /// If true, creates a human-readable link. Not supported if maxAge is set to null. + public async Task CreateInvite(int? maxAge = 1800, int? maxUses = null, bool tempMembership = false, bool humanReadable = false) + { + var response = await Discord.RestClient.Send(new CreateChannelInviteRequest(Id) + { + MaxAge = maxAge ?? 0, + MaxUses = maxUses ?? 0, + IsTemporary = tempMembership, + WithXkcdPass = humanReadable + }).ConfigureAwait(false); + return Discord.CreatePublicInvite(response); + } + + /// + public async Task Delete() + { + try { await Discord.RestClient.Send(new DeleteChannelRequest(Id)).ConfigureAwait(false); } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } + } + /// + public async Task Update() + { + var response = await Discord.RestClient.Send(new GetChannelRequest(Id)).ConfigureAwait(false); + if (response != null) + Update(response); + } + + /// + public override string ToString() => $"{Guild}/{Name ?? Id.ToString()}"; + } +} diff --git a/ref/Entities/Channels/IChannel.cs b/src/Discord.Net/Entities/Channels/IChannel.cs similarity index 80% rename from ref/Entities/Channels/IChannel.cs rename to src/Discord.Net/Entities/Channels/IChannel.cs index fb82fb30d..046718f24 100644 --- a/ref/Entities/Channels/IChannel.cs +++ b/src/Discord.Net/Entities/Channels/IChannel.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Threading.Tasks; namespace Discord { @@ -9,10 +8,10 @@ namespace Discord ChannelType Type { get; } /// Gets the name of this channel. string Name { get; } + /// Gets a collection of all users in this channel. + IEnumerable Users { get; } /// Gets a user in this channel with the given id. - Task GetUser(ulong id); - /// Gets a collection of all users in this channel. - Task> GetUsers(); + User GetUser(ulong id); } } diff --git a/ref/Entities/Channels/ITextChannel.cs b/src/Discord.Net/Entities/Channels/IMessageChannel.cs similarity index 95% rename from ref/Entities/Channels/ITextChannel.cs rename to src/Discord.Net/Entities/Channels/IMessageChannel.cs index f3701abbf..4bb8b5690 100644 --- a/ref/Entities/Channels/ITextChannel.cs +++ b/src/Discord.Net/Entities/Channels/IMessageChannel.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; namespace Discord { - public interface ITextChannel : IChannel + public interface IMessageChannel : IChannel { /// Gets the message in this text channel with the given id, or null if none was found. Task GetMessage(ulong id); @@ -25,6 +25,6 @@ namespace Discord Task SendFile(Stream stream, string filename, string text = null, bool isTTS = false); /// Broadcasts the "user is typing" message to all users in this channel, lasting 10 seconds. - Task SendIsTyping(); + Task TriggerTyping(); } } diff --git a/src/Discord.Net/Entities/Channels/TextChannel.cs b/src/Discord.Net/Entities/Channels/TextChannel.cs new file mode 100644 index 000000000..cebb1c573 --- /dev/null +++ b/src/Discord.Net/Entities/Channels/TextChannel.cs @@ -0,0 +1,67 @@ +using Discord.API.Rest; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Model = Discord.API.Channel; + +namespace Discord +{ + public class TextChannel : GuildChannel, IMessageChannel, IMentionable + { + private readonly MessageManager _messages; + + /// + public string Topic { get; private set; } + + /// + public override ChannelType Type => ChannelType.Text; + /// + public string Mention => MentionHelper.Mention(this); + + internal TextChannel(ulong id, Guild guild, int messageCacheSize, bool usePermissionsCache) + : base(id, guild, usePermissionsCache) + { + _messages = new MessageManager(this, messageCacheSize); + } + + internal override void Update(Model model) + { + Topic = model.Topic; + base.Update(model); + } + + /// + public Task GetMessage(ulong id) + => _messages.Get(id); + /// + public Task> GetMessages(int limit = 100) + => _messages.GetMany(limit); + /// + public Task> GetMessages(int limit = 100, ulong? relativeMessageId = null, Relative relativeDir = Relative.Before) + => _messages.GetMany(limit, relativeMessageId, relativeDir); + + /// + public Task SendMessage(string text, bool isTTS = false) + => _messages.Send(text, isTTS); + /// + public Task SendFile(string filePath, string text = null, bool isTTS = false) + => _messages.SendFile(filePath, text, isTTS); + /// + public Task SendFile(Stream stream, string filename, string text = null, bool isTTS = false) + => _messages.SendFile(stream, filename, text, isTTS); + + /// + public Task TriggerTyping() + => _messages.TriggerTyping(); + + public async Task Modify(Action func) + { + if (func != null) throw new NullReferenceException(nameof(func)); + + var req = new ModifyTextChannelRequest(Id); + func(req); + await Discord.RestClient.Send(req).ConfigureAwait(false); + } + } +} diff --git a/src/Discord.Net/Entities/Channels/VoiceChannel.cs b/src/Discord.Net/Entities/Channels/VoiceChannel.cs new file mode 100644 index 000000000..80fbae797 --- /dev/null +++ b/src/Discord.Net/Entities/Channels/VoiceChannel.cs @@ -0,0 +1,39 @@ +using Discord.API.Rest; +using System; +using System.Threading.Tasks; +using Model = Discord.API.Channel; + +namespace Discord +{ + public class VoiceChannel : GuildChannel + { + /// + public int Bitrate { get; private set; } + + /// + public override ChannelType Type => ChannelType.Voice; + + internal VoiceChannel(ulong id, Guild guild, bool usePermissionsCache) + : base(id, guild, usePermissionsCache) + { + } + + internal override void Update(Model model) + { + Bitrate = model.Bitrate; + base.Update(model); + } + + public async Task Modify(Action func) + { + if (func != null) throw new NullReferenceException(nameof(func)); + + var req = new ModifyVoiceChannelRequest(Id); + func(req); + await Discord.RestClient.Send(req).ConfigureAwait(false); + } + + /// + public override string ToString() => $"{Guild}/{Name ?? Id.ToString()}"; + } +} diff --git a/src/Discord.Net/Entities/Color.cs b/src/Discord.Net/Entities/Color.cs index 325aa2f39..3aecc39ac 100644 --- a/src/Discord.Net/Entities/Color.cs +++ b/src/Discord.Net/Entities/Color.cs @@ -1,24 +1,37 @@ namespace Discord { - public class Color + public class Color { + /// Gets the default user color value. public static readonly Color Default = new Color(0); - - public uint RawValue { get; } - - public Color(uint rawValue) { RawValue = rawValue; } - public Color(byte r, byte g, byte b) : this(((uint)r << 16) | ((uint)g << 8) | b) { } - public Color(float r, float g, float b) : this((byte)(r * 255.0f), (byte)(g * 255.0f), (byte)(b * 255.0f)) { } - /// Gets or sets the red component for this color. - public byte R => (byte)(RawValue >> 16); - /// Gets or sets the green component for this color. + /// Gets the encoded value for this color. + public uint RawValue { get; } + + /// Gets the red component for this color. + public byte R => (byte)(RawValue >> 16); + /// Gets the green component for this color. public byte G => (byte)(RawValue >> 8); - /// Gets or sets the blue component for this color. + /// Gets the blue component for this color. public byte B => (byte)(RawValue); - private byte GetByte(int pos) => (byte)(RawValue >> (8 * (pos - 1))); - - public override string ToString() => '#' + RawValue.ToString("X"); + public Color(uint rawValue) + { + RawValue = rawValue; + } + public Color(byte r, byte g, byte b) + { + RawValue = + ((uint)r << 16) | + ((uint)g << 8) | + b; + } + public Color(float r, float g, float b) + { + RawValue = + ((uint)(r * 255.0f) << 16) | + ((uint)(g * 255.0f) << 8) | + (uint)(b * 255.0f); + } } } diff --git a/src/Discord.Net/Entities/Embed.cs b/src/Discord.Net/Entities/Embed.cs new file mode 100644 index 000000000..271e47f66 --- /dev/null +++ b/src/Discord.Net/Entities/Embed.cs @@ -0,0 +1,25 @@ +using Model = Discord.API.Embed; + +namespace Discord +{ + public struct Embed + { + public string Url { get; } + public string Type { get; } + public string Title { get; } + public string Description { get; } + public EmbedProvider Provider { get; } + public EmbedThumbnail Thumbnail { get; } + + internal Embed(Model model) + { + Url = model.Url; + Type = model.Type; + Title = model.Title; + Description = model.Description; + + Provider = new EmbedProvider(model.Provider); + Thumbnail = new EmbedThumbnail(model.Thumbnail); + } + } +} diff --git a/src/Discord.Net/Entities/EmbedProvider.cs b/src/Discord.Net/Entities/EmbedProvider.cs new file mode 100644 index 000000000..2fce8dfe7 --- /dev/null +++ b/src/Discord.Net/Entities/EmbedProvider.cs @@ -0,0 +1,16 @@ +using Model = Discord.API.EmbedProvider; + +namespace Discord +{ + public struct EmbedProvider + { + public string Name { get; } + public string Url { get; } + + internal EmbedProvider(Model model) + { + Name = model.Name; + Url = model.Url; + } + } +} diff --git a/src/Discord.Net/Entities/EmbedThumbnail.cs b/src/Discord.Net/Entities/EmbedThumbnail.cs new file mode 100644 index 000000000..a61323ed6 --- /dev/null +++ b/src/Discord.Net/Entities/EmbedThumbnail.cs @@ -0,0 +1,20 @@ +using Model = Discord.API.EmbedThumbnail; + +namespace Discord +{ + public struct EmbedThumbnail + { + public string Url { get; } + public string ProxyUrl { get; } + public int? Height { get; } + public int? Width { get; } + + internal EmbedThumbnail(Model model) + { + Url = model.Url; + ProxyUrl = model.ProxyUrl; + Height = model.Height; + Width = model.Width; + } + } +} diff --git a/src/Discord.Net/Entities/Emoji.cs b/src/Discord.Net/Entities/Emoji.cs new file mode 100644 index 000000000..66010d968 --- /dev/null +++ b/src/Discord.Net/Entities/Emoji.cs @@ -0,0 +1,28 @@ +using System.Collections.Immutable; +using System.Linq; +using Model = Discord.API.Emoji; + +namespace Discord +{ + public struct Emoji + { + private readonly Guild _guild; + private readonly ImmutableArray _roles; + + public ulong Id { get; } + public string Name { get; } + public bool IsManaged { get; } + public bool RequireColons { get; } + + internal Emoji(Model model, Guild guild) + { + Id = model.Id; + _guild = guild; + + Name = model.Name; + IsManaged = model.Managed; + RequireColons = model.RequireColons; + _roles = model.Roles.Select(x => guild.GetRole(x)).ToImmutableArray(); + } + } +} diff --git a/src/Discord.Net/Entities/Guild.cs b/src/Discord.Net/Entities/Guild.cs new file mode 100644 index 000000000..62172f47d --- /dev/null +++ b/src/Discord.Net/Entities/Guild.cs @@ -0,0 +1,383 @@ +using Discord.API.Rest; +using Discord.Net; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Model = Discord.API.Guild; + +namespace Discord +{ + /// Represents a Discord guild (called a server in the official client). + public class Guild : IEntity + { + private struct Member + { + public readonly GuildUser User; + public readonly GuildPermissions Permissions; + public Member(GuildUser user, GuildPermissions permissions) + { + User = user; + Permissions = permissions; + } + } + + private ConcurrentDictionary _channels; + private ConcurrentDictionary _members; + private ConcurrentDictionary _presences; + private ConcurrentDictionary _roles; + private ConcurrentDictionary _voiceStates; + private ulong _ownerId; + private ulong? _afkChannelId, _embedChannelId; + private int _userCount; + private string _iconId, _splashId; + + /// + public ulong Id { get; } + /// + public DiscordClient Discord { get; } + public GuildUser CurrentUser { get; } + + /// Gets the name of this guild. + public string Name { get; private set; } + /// Gets the amount of time (in seconds) a user must be inactive in a voice channel for until they are automatically moved to the AFK voice channel, if one is set. + public int AFKTimeout { get; private set; } + /// Returns true if this guild is embeddable (e.g. widget) + public bool IsEmbeddable { get; private set; } + + /// Gets a list of all custom emojis for this guild. + public IReadOnlyList Emojis { get; private set; } + /// Gets a list of extra features added to this guild. + public IReadOnlyList Features { get; private set; } + + /// Gets the voice region for this guild. + public VoiceRegion Region { get; private set; } + /*/// Gets the date and time you joined this guild. + public DateTime JoinedAt { get; private set; }*/ + /// Gets the the role representing all users in a guild. + public Role EveryoneRole { get; private set; } + + /// Gets the number of channels in this guild. + public int ChannelCount => _channels.Count; + /// Gets the number of roles in this guild. + public int RoleCount => _roles.Count; + /// Gets the number of users in this guild. + public int UserCount => _userCount; + /// Gets the number of users downloaded for this guild so far. + internal int CurrentUserCount => _members.Count; + + /// Gets the URL to this guild's current icon. + public string IconUrl => CDN.GetGuildIconUrl(Id, _iconId); + /// Gets the URL to this guild's splash image. + public string SplashUrl => CDN.GetGuildSplashUrl(Id, _splashId); + + /// Gets the user that created this guild. + public GuildUser Owner => GetUser(_ownerId); + /// Gets the default channel for this guild. + public TextChannel DefaultChannel => GetChannel(Id) as TextChannel; + /// Gets the AFK voice channel for this guild. + public VoiceChannel AFKChannel => GetChannel(_afkChannelId) as VoiceChannel; + /// Gets the embed channel for this guild. + public IChannel EmbedChannel => GetChannel(_embedChannelId); //TODO: Is this text or voice? + /// Gets a collection of all channels in this guild. + public IEnumerable Channels => _channels.Select(x => x.Value); + /// Gets a collection of text channels in this guild. + public IEnumerable TextChannels => _channels.Select(x => x.Value).OfType(); + /// Gets a collection of voice channels in this guild. + public IEnumerable VoiceChannels => _channels.Select(x => x.Value).OfType(); + /// Gets a collection of all members in this guild. + public IEnumerable Users => _members.Select(x => x.Value.User); + /// Gets a collection of all roles in this guild. + public IEnumerable Roles => _roles.Select(x => x.Value); + + internal Guild(ulong id, DiscordClient client) + { + Id = id; + Discord = client; + + _channels = new ConcurrentDictionary(); + _members = new ConcurrentDictionary(); + _presences = new ConcurrentDictionary(); + _roles = new ConcurrentDictionary(); + _voiceStates = new ConcurrentDictionary(); + } + + internal void Update(Model model) + { + Name = model.Name; + AFKTimeout = model.AFKTimeout; + _ownerId = model.OwnerId; + _afkChannelId = model.AFKChannelId; + Region = Discord.GetVoiceRegion(model.Region); + _iconId = model.Icon; + _splashId = model.Splash; + Features = model.Features; + IsEmbeddable = model.EmbedEnabled; + _embedChannelId = model.EmbedChannelId; + _userCount = 0;// model.UserCount; + + _roles = new ConcurrentDictionary(2, model.Roles.Length); + foreach (var x in model.Roles) + _roles[x.Id] = Discord.CreateRole(this, x); + EveryoneRole = _roles[Id]; + + Emojis = model.Emojis.Select(x => new Emoji(x, this)).ToArray(); + } + /*internal void Update(ExtendedModel model) + { + Update(model as Model); + + //Only channels or members should have AddXXX(cachePerms: true), not both + if (model.Channels != null) + { + _channels = new ConcurrentDictionary(2, (int)(model.Channels.Length * 1.05)); + foreach (var subModel in model.Channels) + AddChannel(subModel.Id, false).Update(subModel); + DefaultChannel = _channels[Id]; + } + if (model.MemberCount != null) + { + if (_users == null) + _users = new ConcurrentDictionary(2, (int)(model.MemberCount * 1.05)); + _userCount = model.MemberCount.Value; + } + if (!model.IsLarge) + { + if (model.Members != null) + { + foreach (var subModel in model.Members) + AddUser(subModel.User.Id, true, false).Update(subModel); + } + if (model.VoiceStates != null) + { + foreach (var subModel in model.VoiceStates) + GetUser(subModel.UserId)?.Update(subModel); + } + if (model.Presences != null) + { + foreach (var subModel in model.Presences) + GetUser(subModel.User.Id)?.Update(subModel); + } + } + }*/ + + /// Gets the channel with the given id, or null if not found. + public GuildChannel GetChannel(ulong id) + { + GuildChannel result; + _channels.TryGetValue(id, out result); + return result; + } + /// Gets the channel refered to by the given mention, or null if not found. + public GuildChannel GetChannel(string mention) => GetChannel(MentionHelper.GetChannelId(mention)); + private GuildChannel GetChannel(ulong? id) => id != null ? GetChannel(id.Value) : null; + + /// Gets the channel with the given id, or null if not found. + public Role GetRole(ulong id) + { + Role result; + _roles.TryGetValue(id, out result); + return result; + } + private Role GetRole(ulong? id) => id != null ? GetRole(id.Value) : null; + + public GuildUser GetUser(ulong id) + { + Member result; + if (_members.TryGetValue(id, out result)) + return result.User; + else + return null; + } + public GuildUser GetUser(string username, ushort discriminator) + { + if (username == null) throw new ArgumentNullException(nameof(username)); + + foreach (var member in _members) + { + var user = member.Value.User; + if (user.Discriminator == discriminator && user.Username == username) + return user; + } + return null; + } + public GuildUser GetUser(string mention) => GetUser(MentionHelper.GetUserId(mention)); + private GuildUser GetUser(ulong? id) => id != null ? GetUser(id.Value) : null; + + public async Task> GetBans() + { + var discord = Discord; + var response = await Discord.RestClient.Send(new GetGuildBansRequest(Id)).ConfigureAwait(false); + return response.Select(x => Discord.CreateBannedUser(this, x)); + } + + public async Task> GetInvites() + { + var response = await Discord.RestClient.Send(new GetGuildInvitesRequest(Id)).ConfigureAwait(false); + return response.Select(x => + { + var invite = Discord.CreatePublicInvite(x); + invite.Update(x); + return invite; + }); + } + + public async Task CreateTextChannel(string name) + { + if (name == null) throw new ArgumentNullException(nameof(name)); + + var request = new CreateChannelRequest(Id) { Name = name, Type = ChannelType.Text }; + var response = await Discord.RestClient.Send(request).ConfigureAwait(false); + + return Discord.CreateTextChannel(this, response); + } + public async Task CreateVoiceChannel(string name) + { + if (name == null) throw new ArgumentNullException(nameof(name)); + + var request = new CreateChannelRequest(Id) { Name = name, Type = ChannelType.Voice }; + var response = await Discord.RestClient.Send(request).ConfigureAwait(false); + + return Discord.CreateVoiceChannel(this, response); + } + public Task CreateInvite(int? maxAge = 1800, int? maxUses = null, bool tempMembership = false, bool withXkcd = false) + { + return DefaultChannel.CreateInvite(maxAge, maxUses, tempMembership, withXkcd); + } + public async Task CreateRole(string name, GuildPermissions? permissions = null, Color color = null, bool isHoisted = false) + { + if (name == null) throw new ArgumentNullException(nameof(name)); + + var createRequest = new CreateRoleRequest(Id); + var createResponse = await Discord.RestClient.Send(createRequest).ConfigureAwait(false); + var role = Discord.CreateRole(this, createResponse); + + var editRequest = new ModifyGuildRoleRequest(role.Guild.Id, role.Id) + { + Name = name, + Permissions = (int)(permissions ?? role.Permissions).RawValue, + Color = (int)(color ?? Color.Default).RawValue, + Hoist = isHoisted + }; + var editResponse = await Discord.RestClient.Send(editRequest).ConfigureAwait(false); + role.Update(editResponse); + + return role; + } + + public async Task PruneUsers(int days = 30, bool simulate = false) + { + if (simulate) + { + var response = await Discord.RestClient.Send(new GetGuildPruneCountRequest(Id) { Days = days }).ConfigureAwait(false); + return response.Pruned; + } + else + { + var response = await Discord.RestClient.Send(new BeginGuildPruneRequest(Id) { Days = days }).ConfigureAwait(false); + return response.Pruned; + } + } + + public Task Ban(GuildUser user, int pruneDays = 0) + { + return Discord.RestClient.Send(new CreateGuildBanRequest(Id, user.Id) + { + PruneDays = pruneDays + }); + } + public async Task Unban(GuildUser user) + { + try { await Discord.RestClient.Send(new RemoveGuildBanRequest(Id, user.Id)).ConfigureAwait(false); } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } + } + + public async Task Update() + { + var response = await Discord.RestClient.Send(new GetGuildRequest(Id)).ConfigureAwait(false); + if (response != null) + Update(response); + } + + public async Task Modify(Action func) + { + if (func != null) throw new NullReferenceException(nameof(func)); + + var req = new ModifyGuildRequest(Id); + func(req); + await Discord.RestClient.Send(req).ConfigureAwait(false); + } + + /// Leaves this guild. + public async Task Leave() + { + try { await Discord.RestClient.Send(new LeaveGuildRequest(Id)).ConfigureAwait(false); } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } + } + /// Deletes this guild. + public async Task Delete() + { + try { await Discord.RestClient.Send(new DeleteGuildRequest(Id)).ConfigureAwait(false); } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } + } + + internal void UpdatePermissions(GuildUser user) + { + Member member; + if (_members.TryGetValue(user.Id, out member)) + { + var perms = member.Permissions; + if (UpdatePermissions(member.User, ref perms)) + { + _members[user.Id] = new Member(member.User, perms); + foreach (var channel in _channels) + channel.Value.UpdatePermissions(user); + } + } + } + + private bool UpdatePermissions(GuildUser user, ref GuildPermissions permissions) + { + uint newPermissions = 0; + + if (user.Id == _ownerId) + newPermissions = GuildPermissions.All.RawValue; + else + { + foreach (var role in user.Presence.Roles) + newPermissions |= role.Permissions.RawValue; + } + + if (PermissionsHelper.HasBit(ref newPermissions, (byte)PermissionBit.ManageRolesOrPermissions)) + newPermissions = GuildPermissions.All.RawValue; + + if (newPermissions != permissions.RawValue) + { + permissions = new GuildPermissions(newPermissions); + return true; + } + return false; + } + + /*internal IGuildChannel AddChannel(ulong id, bool cachePerms) + { + var channel = new Channel(Discord, id, this); + if (cachePerms && Discord.UsePermissionsCache) + { + foreach (var user in Users) + channel.AddUser(user); + } + Discord.AddChannel(channel); + return _channels.GetOrAdd(id, x => channel); + } + internal IGuildChannel RemoveChannel(ulong id) + { + IGuildChannel channel; + _channels.TryRemove(id, out channel); + return channel; + }*/ + } +} diff --git a/src/Discord.Net/Entities/Helpers/InviteManager.cs b/src/Discord.Net/Entities/Helpers/InviteManager.cs new file mode 100644 index 000000000..13058e621 --- /dev/null +++ b/src/Discord.Net/Entities/Helpers/InviteManager.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace Discord +{ + public class InviteManager + { + } +} diff --git a/src/Discord.Net/Entities/Helpers/MentionHelper.cs b/src/Discord.Net/Entities/Helpers/MentionHelper.cs new file mode 100644 index 000000000..78777a518 --- /dev/null +++ b/src/Discord.Net/Entities/Helpers/MentionHelper.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Immutable; +using System.Globalization; +using System.Text.RegularExpressions; + +namespace Discord +{ + internal static class MentionHelper + { + 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"); + + internal static string Mention(User user) => $"<@{user.Id}>"; + internal static string Mention(IChannel channel) => $"<#{channel.Id}>"; + internal static string Mention(Role role) => role.IsEveryone ? "@everyone" : ""; + + internal static string CleanUserMentions(GuildChannel channel, string text, ImmutableArray.Builder users = null) + { + var guild = channel.Guild; + return _userRegex.Replace(text, new MatchEvaluator(e => + { + ulong id; + if (ulong.TryParse(e.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture, out id)) + { + var user = guild.GetUser(id); //We're able to mention users outside of our channel + if (user != null) + { + if (users != null) + users.Add(user); + return '@' + user.Username; + } + } + return e.Value; //User not found or parse failed + })); + } + internal static string CleanChannelMentions(GuildChannel channel, string text, ImmutableArray.Builder channels = null) + { + var guild = channel.Guild; + return _channelRegex.Replace(text, new MatchEvaluator(e => + { + ulong id; + if (ulong.TryParse(e.Groups[1].Value, NumberStyles.None, CultureInfo.InvariantCulture, out id)) + { + var mentionedChannel = guild.GetChannel(id); + if (mentionedChannel != null && mentionedChannel.Guild.Id == guild.Id) + { + if (channels != null) + channels.Add(mentionedChannel); + return '#' + mentionedChannel.Name; + } + } + return e.Value; //Channel not found or parse failed + })); + } + /*internal static string CleanRoleMentions(User user, IPublicChannel channel, string text, ImmutableArray.Builder roles = null) + { + var guild = channel.Guild; + if (guild == null) return text; + + return _roleRegex.Replace(text, new MatchEvaluator(e => + { + if (roles != null && user.GetPermissions(channel).MentionEveryone) + roles.Add(guild.EveryoneRole); + return e.Value; + })); + }*/ + + internal static ulong GetUserId(string mention) + { + mention = mention.Trim(); + if (mention.Length >= 3 && mention[0] == '<' && mention[1] == '@' && mention[mention.Length - 1] == '>') + { + mention = mention.Substring(2, mention.Length - 3); + + ulong id; + if (ulong.TryParse(mention, NumberStyles.None, CultureInfo.InvariantCulture, out id)) + return id; + } + throw new ArgumentException("Invalid mention format", nameof(mention)); + } + internal static ulong GetChannelId(string mention) + { + mention = mention.Trim(); + if (mention.Length >= 3 && mention[0] == '<' && mention[1] == '#' && mention[mention.Length - 1] == '>') + { + mention = mention.Substring(2, mention.Length - 3); + + ulong id; + if (ulong.TryParse(mention, NumberStyles.None, CultureInfo.InvariantCulture, out id)) + return id; + } + throw new ArgumentException("Invalid mention format", nameof(mention)); + } + + internal static string ResolveMentions(IChannel channel, string text) + { + if (channel == null) throw new ArgumentNullException(nameof(channel)); + if (text == null) throw new ArgumentNullException(nameof(text)); + + var publicChannel = channel as GuildChannel; + if (publicChannel != null) + { + text = CleanUserMentions(publicChannel, text); + text = CleanChannelMentions(publicChannel, text); + //text = CleanRoleMentions(publicChannel, text); + } + return text; + } + } +} diff --git a/src/Discord.Net/Entities/Helpers/MessageManager.cs b/src/Discord.Net/Entities/Helpers/MessageManager.cs new file mode 100644 index 000000000..1285093ae --- /dev/null +++ b/src/Discord.Net/Entities/Helpers/MessageManager.cs @@ -0,0 +1,174 @@ +using Discord.API.Rest; +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.Message; + +namespace Discord +{ + internal class MessageManager : IEnumerable + { + private readonly IMessageChannel _channel; + private readonly ConcurrentDictionary _messages; + private readonly ConcurrentQueue _orderedMessages; + private readonly int _size; + + public MessageManager(IMessageChannel channel, int size = 0) + { + _channel = channel; + _size = (int)(size * 1.05); + if (size > 0) + { + _messages = new ConcurrentDictionary(2, size); + _orderedMessages = new ConcurrentQueue(); + } + } + + internal Message Add(Model model, User user) + => Add(_channel.Discord.CreateMessage(_channel, user, model)); + private Message Add(Message message) + { + if (_size > 0) + { + if (_messages.TryAdd(message.Id, message)) + { + _orderedMessages.Enqueue(message); + + Message msg; + while (_orderedMessages.Count > _size && _orderedMessages.TryDequeue(out msg)) + _messages.TryRemove(msg.Id, out msg); + } + } + return message; + } + internal void Remove(ulong id) + { + if (_size > 0) + { + Message msg; + _messages.TryRemove(id, out msg); + } + } + + public Task Get(ulong id) + { + if (_messages != null) + { + Message result; + if (_messages.TryGetValue(id, out result)) + return Task.FromResult(result); + } + else + throw new NotSupportedException(); //TODO: Not supported yet + + return Task.FromResult(null); + } + + public async Task> GetMany(int limit = DiscordConfig.MaxMessagesPerBatch, ulong? relativeMessageId = null, Relative relativeDir = Relative.Before) + { + if (limit < 0) throw new ArgumentOutOfRangeException(nameof(limit)); + if (limit == 0) return ImmutableArray.Empty; + + if (_messages != null) + { + ImmutableArray cachedMessages; + if (relativeMessageId == null) + cachedMessages = _orderedMessages.ToImmutableArray(); + else if (relativeDir == Relative.Before) + cachedMessages = _orderedMessages.Where(x => x.Id < relativeMessageId.Value).ToImmutableArray(); + else + cachedMessages = _orderedMessages.Where(x => x.Id > relativeMessageId.Value).ToImmutableArray(); + + if (cachedMessages.Length == limit) + return cachedMessages; + else if (cachedMessages.Length > limit) + return cachedMessages.Skip(cachedMessages.Length - limit); + else + { + var missingMessages = await Download(limit - cachedMessages.Length, cachedMessages[0].Id, Relative.Before).ConfigureAwait(false); + return missingMessages.SelectMany(x => x).Concat(cachedMessages); + } + } + return (await Download(limit, relativeMessageId, relativeDir).ConfigureAwait(false)).SelectMany(x => x); + } + private async Task>> Download(int limit = DiscordConfig.MaxMessagesPerBatch, ulong? relativeMessageId = null, Relative relativeDir = Relative.Before) + { + var request = new GetChannelMessagesRequest(_channel.Id) + { + Limit = limit, + RelativeDir = relativeDir, + RelativeId = relativeMessageId ?? 0 + }; + var guild = (_channel as GuildChannel)?.Guild; + var recipient = (_channel as DMChannel)?.Recipient; + + int runs = limit / DiscordConfig.MaxMessagesPerBatch; + int lastRunCount = limit - runs * DiscordConfig.MaxMessagesPerBatch; + var result = new Message[runs][]; + + int i = 0; + for (; i < runs; i++) + { + request.Limit = (i == runs - 1) ? lastRunCount : DiscordConfig.MaxMessagesPerBatch; + + Model[] models = await _channel.Discord.RestClient.Send(request).ConfigureAwait(false); + + //Was this an empty batch? + if (models.Length == 0) break; + + Message[] msgs = new Message[models.Length]; + for (int j = 0; j < models.Length; j++) + { + var model = models[j]; + var user = _channel.GetUser(model.Author.Id); + msgs[j] = _channel.Discord.CreateMessage(_channel, user, model); + } + result[i] = msgs; + + request.RelativeId = relativeDir == Relative.Before ? msgs[0].Id : msgs[msgs.Length - 1].Id; + + //Was this an incomplete (the last) batch? + if (models.Length != DiscordConfig.MaxMessagesPerBatch) { i++; break; } + } + + //Dont return nulls if we didnt get all the requested messages + for (; i < runs; i++) + result[i] = Array.Empty(); + + return result; + } + + public Task Send(string text, bool isTTS) + { + if (text == "") throw new ArgumentException("Value cannot be blank", nameof(text)); + if (text.Length > DiscordConfig.MaxMessageSize) + throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {DiscordConfig.MaxMessageSize} characters or less."); + return _channel.Discord.MessageQueue.QueueSend(_channel, text, isTTS); + } + public async Task SendFile(string filePath, string text = null, bool isTTS = false) + { + using (var stream = File.OpenRead(filePath)) + return await SendFile(stream, Path.GetFileName(filePath), text, isTTS).ConfigureAwait(false); + } + public async Task SendFile(Stream stream, string filename, string text = null, bool isTTS = false) + { + var request = new SendFileRequest(_channel.Id) + { + Filename = filename, + Stream = stream + }; + var response = await _channel.Discord.RestClient.Send(request).ConfigureAwait(false); + + return _channel.Discord.CreateMessage(_channel, _channel.GetCurrentUser(), response); + } + public Task TriggerTyping() => _channel.Discord.RestClient.Send(new TriggerTypingIndicatorRequest(_channel.Id)); + + public IEnumerator GetEnumerator() => _orderedMessages.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => _orderedMessages.GetEnumerator(); + } +} diff --git a/src/Discord.Net/Entities/Helpers/PermissionManager.cs b/src/Discord.Net/Entities/Helpers/PermissionManager.cs new file mode 100644 index 000000000..a7047bd94 --- /dev/null +++ b/src/Discord.Net/Entities/Helpers/PermissionManager.cs @@ -0,0 +1,274 @@ +using Discord.API.Rest; +using Discord.Net; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Model = Discord.API.Channel; + +namespace Discord +{ + internal class PermissionManager + { + public struct Member + { + public GuildUser User { get; } + public ChannelPermissions Permissions { get; } + + public Member(GuildUser user, ChannelPermissions permissions) + { + User = user; + Permissions = permissions; + } + } + + private readonly GuildChannel _channel; + private readonly ConcurrentDictionary _users; + private Dictionary _rules; + + public IEnumerable Users => _users.Select(x => x.Value); + public IEnumerable Overwrites => _rules.Values; + + public PermissionManager(GuildChannel channel, bool cacheUsers) + { + _channel = channel; + if (cacheUsers) + _users = new ConcurrentDictionary(2, (int)(channel.Guild.UserCount * 1.05)); + } + + public void Update(Model model) + { + _rules = model.PermissionOverwrites + .Select(x => new Overwrite(x)) + .ToDictionary(x => x.TargetId); + UpdatePermissions(); + } + + public OverwritePermissions? GetOverwrite(GuildUser user) + { + if (user == null) throw new ArgumentNullException(nameof(user)); + + Overwrite rule; + if (_rules.TryGetValue(user.Id, out rule)) + return rule.Permissions; + return null; + } + public OverwritePermissions? GetOverwrite(Role role) + { + if (role == null) throw new ArgumentNullException(nameof(role)); + + Overwrite rule; + if (_rules.TryGetValue(role.Id, out rule)) + return rule.Permissions; + return null; + } + public Task AddOrUpdateOverwrite(GuildUser user, OverwritePermissions permissions) + { + if (user == null) throw new ArgumentNullException(nameof(user)); + return AddOrUpdateOverwrite(user.Id, permissions); + } + public Task AddOrUpdateOverwrite(Role role, OverwritePermissions permissions) + { + if (role == null) throw new ArgumentNullException(nameof(role)); + return AddOrUpdateOverwrite(role.Id, permissions); + } + private Task AddOrUpdateOverwrite(ulong targetId, OverwritePermissions permissions) + { + var request = new ModifyChannelPermissionsRequest(_channel.Id, targetId) + { + Allow = permissions.AllowValue, + Deny = permissions.DenyValue + }; + return _channel.Discord.RestClient.Send(request); + } + public Task RemoveOverwrite(GuildUser user) + { + if (user == null) throw new ArgumentNullException(nameof(user)); + return RemoveOverwrite(user.Id); + } + public Task RemoveOverwrite(Role role) + { + if (role == null) throw new ArgumentNullException(nameof(role)); + return RemoveOverwrite(role.Id); + } + private async Task RemoveOverwrite(ulong id) + { + try { await _channel.Discord.RestClient.Send(new DeleteChannelPermissionsRequest(_channel.Id, id)).ConfigureAwait(false); } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } + } + + public ChannelPermissions GetPermissions(GuildUser user) + { + if (user == null) throw new ArgumentNullException(nameof(user)); + + if (_users != null) + { + Member member; + if (_users.TryGetValue(user.Id, out member)) + return member.Permissions; + else + return ChannelPermissions.None; + } + else + { + var perms = new ChannelPermissions(); + ResolvePermissions(user, ref perms); + return perms; + } + } + public void UpdatePermissions() + { + if (_users != null) + { + foreach (var pair in _users) + { + var member = pair.Value; + var perms = member.Permissions; + if (ResolvePermissions(member.User, ref perms)) + _users[pair.Key] = new Member(member.User, perms); + } + } + } + public void UpdatePermissions(GuildUser user) + { + if (user == null) throw new ArgumentNullException(nameof(user)); + + if (_users != null) + { + Member member; + if (_users.TryGetValue(user.Id, out member)) + { + var perms = member.Permissions; + if (ResolvePermissions(member.User, ref perms)) + _users[user.Id] = new Member(member.User, perms); + } + } + } + + + public ChannelPermissions ResolvePermissions(GuildUser user) + { + var permissions = new ChannelPermissions(); + ResolvePermissions(user, ref permissions); + return permissions; + } + public bool ResolvePermissions(GuildUser user, ref ChannelPermissions permissions) + { + if (user == null) throw new ArgumentNullException(nameof(user)); + + uint newPermissions = 0; + var guild = user.Guild; + + uint mask = ChannelPermissions.All(_channel.Type).RawValue; + if (user == guild.Owner) + newPermissions = mask; //Private messages and owners always have all permissions + else + { + //Start with this user's guild permissions + newPermissions = user.GuildPermissions.RawValue; + var rules = _rules; + + Overwrite entry; + var roles = user.Presence.Roles.ToArray(); + if (roles.Length > 0) + { + for (int i = 0; i < roles.Length; i++) + { + if (rules.TryGetValue(roles[i].Id, out entry)) + newPermissions &= ~entry.Permissions.DenyValue; + } + for (int i = 0; i < roles.Length; i++) + { + if (rules.TryGetValue(roles[i].Id, out entry)) + newPermissions |= entry.Permissions.AllowValue; + } + } + if (rules.TryGetValue(user.Id, out entry)) + newPermissions = (newPermissions & ~entry.Permissions.DenyValue) | entry.Permissions.AllowValue; + + if (PermissionsHelper.HasBit(ref newPermissions, (int)PermissionBit.ManageRolesOrPermissions)) + newPermissions = mask; //ManageRolesOrPermissions gives all permisions + else + { + var channelType = _channel.Type; + if (channelType == ChannelType.Text && !PermissionsHelper.HasBit(ref newPermissions, (int)PermissionBit.ReadMessages)) + newPermissions = 0; //No read permission on a text channel removes all other permissions + else if (channelType == ChannelType.Voice && !PermissionsHelper.HasBit(ref newPermissions, (int)PermissionBit.Connect)) + newPermissions = 0; //No connect permissions on a voice channel removes all other permissions + else + newPermissions &= mask; //Ensure we didnt get any permissions this channel doesnt support (from guildPerms, for example) + } + } + + if (newPermissions != permissions.RawValue) + { + permissions = new ChannelPermissions(newPermissions); + return true; + } + return false; + } + + public GuildUser GetUser(ulong id) + { + if (_users != null) + { + Member member; + if (_users.TryGetValue(id, out member)) + return member.User; + } + else + { + var user = _channel.Guild.GetUser(id); + if (_channel.Type == ChannelType.Text) + { + if (ResolvePermissions(user).ReadMessages) + return user; + } + else if (_channel.Type == ChannelType.Voice) + { + if (user.VoiceState?.VoiceChannel == _channel) + return user; + } + } + return null; + } + public IEnumerable GetUsers() + { + if (_users != null) + return _users.Select(x => x.Value.User); + else + { + var users = _channel.Guild.Users; + if (_channel.Type == ChannelType.Text) + { + var perms = new ChannelPermissions(); + return users.Where(x => ResolvePermissions(x, ref perms)); + } + else if (_channel.Type == ChannelType.Voice) + return users.Where(x => x.VoiceState?.VoiceChannel == _channel); + } + return Enumerable.Empty(); + } + + public void AddUser(GuildUser user) + { + if (user == null) throw new ArgumentNullException(nameof(user)); + + if (_users != null) + { + var perms = new ChannelPermissions(); + ResolvePermissions(user, ref perms); + var member = new Member(user, ChannelPermissions.None); + _users[user.Id] = new Member(user, ChannelPermissions.None); + } + } + public void RemoveUser(ulong id) + { + Member ignored; + if (_users != null) + _users.TryRemove(id, out ignored); + } + } +} diff --git a/src/Discord.Net/Entities/Helpers/PermissionsHelper.cs b/src/Discord.Net/Entities/Helpers/PermissionsHelper.cs new file mode 100644 index 000000000..e14d5e210 --- /dev/null +++ b/src/Discord.Net/Entities/Helpers/PermissionsHelper.cs @@ -0,0 +1,61 @@ +using System.Runtime.CompilerServices; + +namespace Discord +{ + internal static class PermissionsHelper + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PermValue GetValue(uint allow, uint deny, PermissionBit bit) + { + if (HasBit(ref allow, (byte)bit)) + return PermValue.Allow; + else if (HasBit(ref deny, (byte)bit)) + return PermValue.Deny; + else + return PermValue.Inherit; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool GetValue(uint value, PermissionBit bit) => HasBit(ref value, (byte)bit); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void SetValue(ref uint rawValue, bool? value, PermissionBit bit) + { + if (value.HasValue) + { + if (value == true) + SetBit(ref rawValue, (byte)bit); + else + UnsetBit(ref rawValue, (byte)bit); + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void SetValue(ref uint allow, ref uint deny, PermValue? value, PermissionBit bit) + { + if (value.HasValue) + { + switch (value) + { + case PermValue.Allow: + SetBit(ref allow, (byte)bit); + UnsetBit(ref deny, (byte)bit); + break; + case PermValue.Deny: + UnsetBit(ref allow, (byte)bit); + SetBit(ref deny, (byte)bit); + break; + default: + UnsetBit(ref allow, (byte)bit); + UnsetBit(ref deny, (byte)bit); + break; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool HasBit(ref uint value, byte bit) => (value & (1U << bit)) != 0; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void SetBit(ref uint value, byte bit) => value |= (1U << bit); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void UnsetBit(ref uint value, byte bit) => value &= ~(1U << bit); + } +} diff --git a/src/Discord.Net/Entities/IChannel.cs b/src/Discord.Net/Entities/IChannel.cs deleted file mode 100644 index f39678abb..000000000 --- a/src/Discord.Net/Entities/IChannel.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; - -namespace Discord -{ - public interface IChannel - { - /// Gets the unique identifier for this channel. - ulong Id { get; } - DiscordClient Client { get; } - - /// Gets the type of this channel. - ChannelType Type { get; } - bool IsText { get; } - bool IsVoice { get; } - bool IsPrivate { get; } - bool IsPublic { get; } - - /// Gets a collection of all users in this channel. - IEnumerable Users { get; } - } -} diff --git a/ref/Entities/IEntity.cs b/src/Discord.Net/Entities/IEntity.cs similarity index 81% rename from ref/Entities/IEntity.cs rename to src/Discord.Net/Entities/IEntity.cs index ac707a69e..ecdde0a56 100644 --- a/ref/Entities/IEntity.cs +++ b/src/Discord.Net/Entities/IEntity.cs @@ -7,13 +7,12 @@ namespace Discord /// Gets the unique identifier for this object. TId Id { get; } } + public interface IEntity { /// Gets the DiscordClient that manages this object. DiscordClient Discord { get; } - /// Gets the state of this object. - EntityState State { get; } - + /// Downloads the latest values and updates this object. Task Update(); } diff --git a/ref/Entities/IMentionable.cs b/src/Discord.Net/Entities/IMentionable.cs similarity index 100% rename from ref/Entities/IMentionable.cs rename to src/Discord.Net/Entities/IMentionable.cs diff --git a/src/Discord.Net/Entities/IPrivateChannel.cs b/src/Discord.Net/Entities/IPrivateChannel.cs deleted file mode 100644 index 44d55a67f..000000000 --- a/src/Discord.Net/Entities/IPrivateChannel.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Discord -{ - public interface IPrivateChannel : IChannel - { - User Recipient { get; } - } -} diff --git a/src/Discord.Net/Entities/IPublicChannel.cs b/src/Discord.Net/Entities/IPublicChannel.cs deleted file mode 100644 index 32b8c610e..000000000 --- a/src/Discord.Net/Entities/IPublicChannel.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Discord -{ - public interface IPublicChannel : IChannel - { - } -} diff --git a/src/Discord.Net/Entities/ITextChannel.cs b/src/Discord.Net/Entities/ITextChannel.cs deleted file mode 100644 index af665ab0f..000000000 --- a/src/Discord.Net/Entities/ITextChannel.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.IO; -using System.Threading.Tasks; - -namespace Discord -{ - public interface ITextChannel : IChannel - { - Message GetMessage(ulong id); - Task DownloadMessages(int limit = 100, ulong? relativeMessageId = null, Relative relativeDir = Relative.Before); - - Task SendMessage(string text, bool isTTS = false); - Task SendFile(string filePath); - Task SendFile(string filename, Stream stream); - - Task SendIsTyping(); - } -} diff --git a/src/Discord.Net/Entities/IVoiceChannel.cs b/src/Discord.Net/Entities/IVoiceChannel.cs deleted file mode 100644 index 7c3f2c194..000000000 --- a/src/Discord.Net/Entities/IVoiceChannel.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Discord -{ - public interface IVoiceChannel : IChannel - { - } -} diff --git a/src/Discord.Net/Entities/Invite.cs b/src/Discord.Net/Entities/Invite.cs deleted file mode 100644 index a1b27ce4a..000000000 --- a/src/Discord.Net/Entities/Invite.cs +++ /dev/null @@ -1,151 +0,0 @@ -using APIInvite = Discord.API.Client.Invite; -using Discord.API.Client; -using Discord.API.Client.Rest; -using Discord.Net; -using System; -using System.Net; -using System.Threading.Tasks; - -namespace Discord -{ - public class Invite - { - private readonly static Action _cloner = DynamicIL.CreateCopyMethod(); - - public class ServerInfo - { - /// Returns the unique identifier of this server. - public ulong Id { get; } - /// Returns the name of this server. - public string Name { get; } - - internal ServerInfo(ulong id, string name) - { - Id = id; - Name = name; - } - } - public class ChannelInfo - { - /// Returns the unique identifier of this channel. - public ulong Id { get; } - /// Returns the name of this channel. - public string Name { get; } - - internal ChannelInfo(ulong id, string name) - { - Id = id; - Name = name; - } - } - public class InviterInfo - { - /// Returns the unique identifier for this user. - public ulong Id { get; } - /// Returns the name of this user. - public string Name { get; } - /// Returns the by-name unique identifier for this user. - public ushort Discriminator { get; } - /// Returns the unique identifier for this user's avatar. - public string AvatarId { get; } - - /// Returns the full path to this user's avatar. - public string AvatarUrl => User.GetAvatarUrl(Id, AvatarId); - - internal InviterInfo(ulong id, string name, ushort discriminator, string avatarId) - { - Id = id; - Name = name; - Discriminator = discriminator; - AvatarId = avatarId; - } - } - - public DiscordClient Client { get; } - - /// Gets the unique code for this invite. - public string Code { get; } - /// Gets, if enabled, an alternative human-readable invite code. - public string XkcdCode { get; } - - /// Gets information about the server this invite is attached to. - public ServerInfo Server { get; private set; } - /// Gets information about the channel this invite is attached to. - public ChannelInfo Channel { get; private set; } - /// Gets the time (in seconds) until the invite expires. - public int? MaxAge { get; private set; } - /// Gets the amount of times this invite has been used. - public int Uses { get; private set; } - /// Gets the max amount of times this invite may be used. - public int? MaxUses { get; private set; } - /// Returns true if this invite has expired, been destroyed, or you are banned from that server. - public bool IsRevoked { get; private set; } - /// If true, a user accepting this invite will be kicked from the server after closing their client. - public bool IsTemporary { get; private set; } - /// Gets when this invite was created. - public DateTime CreatedAt { get; private set; } - - /// Returns a URL for this invite using XkcdCode if available or Id if not. - public string Url => $"{DiscordConfig.InviteUrl}/{Code}"; - - internal Invite(APIInvite model, DiscordClient client) - : this(model.Code, model.XkcdPass) - { - Client = client; - Update(model); - } - internal Invite(InviteReference model, DiscordClient client) - : this(model.Code, model.XkcdPass) - { - Client = client; - Update(model); - } - private Invite(string code, string xkcdCode) - { - Code = code; - XkcdCode = xkcdCode; - } - - internal void Update(APIInvite model) - { - Update(model as InviteReference); - - if (model.IsRevoked != null) - IsRevoked = model.IsRevoked.Value; - if (model.IsTemporary != null) - IsTemporary = model.IsTemporary.Value; - if (model.MaxAge != null) - MaxAge = model.MaxAge.Value != 0 ? model.MaxAge.Value : (int?)null; - if (model.MaxUses != null) - MaxUses = model.MaxUses.Value; - if (model.Uses != null) - Uses = model.Uses.Value; - if (model.CreatedAt != null) - CreatedAt = model.CreatedAt.Value; - } - internal void Update(InviteReference model) - { - if (model.Guild != null) - Server = new ServerInfo(model.Guild.Id, model.Guild.Name); - if (model.Channel != null) - Channel = new ChannelInfo(model.Channel.Id, model.Channel.Name); - } - - public async Task Delete() - { - try { await Client.ClientAPI.Send(new DeleteInviteRequest(Code)).ConfigureAwait(false); } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } - } - public Task Accept() - => Client.ClientAPI.Send(new AcceptInviteRequest(Code)); - - internal Invite Clone() - { - var result = new Invite(Code, XkcdCode); - _cloner(this, result); - return result; - } - - public override string ToString() => $"{Server}/{XkcdCode ?? Code}"; - } -} diff --git a/src/Discord.Net/Entities/Invites/GuildInvite.cs b/src/Discord.Net/Entities/Invites/GuildInvite.cs new file mode 100644 index 000000000..d4387c744 --- /dev/null +++ b/src/Discord.Net/Entities/Invites/GuildInvite.cs @@ -0,0 +1,71 @@ +using Discord.API.Rest; +using Discord.Net; +using System; +using System.Net; +using System.Threading.Tasks; +using Model = Discord.API.InviteMetadata; + +namespace Discord +{ + public class GuildInvite : IInvite, IEntity + { + /// + public string Code { get; } + /// Gets the channel this invite is attached to. + public GuildChannel Channel { get; } + + /// + public string XkcdCode { get; private set; } + /// Gets the time (in seconds) until the invite expires, or null if it never expires. + public int? MaxAge { get; private set; } + /// Gets the amount of times this invite has been used. + public int Uses { get; private set; } + /// Gets the max amount of times this invite may be used, or null if there is no limit. + public int? MaxUses { get; private set; } + /// Returns true if this invite has expired or been deleted. + public bool IsRevoked { get; private set; } + /// Returns true if a user accepting this invite will be kicked from the guild after closing their client. + public bool IsTempMembership { get; private set; } + + /// Gets the guild this invite is attached to. + public Guild Guild => Channel.Guild; + /// + public DiscordClient Discord => Guild.Discord; + /// + public string Url => $"{DiscordConfig.InviteUrl}/{Code}"; + /// + public string XkcdUrl => XkcdCode != null ? $"{DiscordConfig.InviteUrl}/{XkcdCode}" : null; + /// + string IEntity.Id => Code; + /// + InviteChannel IInvite.Channel => new InviteChannel(Channel.Id, Channel.Name); + /// + InviteGuild IInvite.Guild => new InviteGuild(Guild.Id, Guild.Name); + + internal GuildInvite(string code, GuildChannel channel) + { + Code = code; + Channel = channel; + } + + internal void Update(Model model) + { + XkcdCode = model.XkcdPass; + IsRevoked = model.Revoked; + IsTempMembership = model.Temporary; + MaxAge = model.MaxAge != 0 ? model.MaxAge : (int?)null; + MaxUses = model.MaxUses; + Uses = model.Uses; + } + + /// + public Task Update() { throw new NotSupportedException(); } //TODO: Not supported yet + + /// Deletes this invite. + public async Task Delete() + { + try { await Discord.RestClient.Send(new DeleteInviteRequest(Code)).ConfigureAwait(false); } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } + } + } +} diff --git a/src/Discord.Net/Entities/Invites/IInvite.cs b/src/Discord.Net/Entities/Invites/IInvite.cs new file mode 100644 index 000000000..8765cdc93 --- /dev/null +++ b/src/Discord.Net/Entities/Invites/IInvite.cs @@ -0,0 +1,20 @@ +namespace Discord +{ + public interface IInvite : IEntity + { + /// Gets the unique code for this invite. + string Code { get; } + /// Gets, if enabled, an alternative human-readable invite code. + string XkcdCode { get; } + + /// Returns a URL for this invite using Code. + string Url { get; } + /// Returns a URL for this invite using XkcdCode if available or null if not. + string XkcdUrl { get; } + + /// Gets information about the guild this invite is attached to. + InviteGuild Guild { get; } + /// Gets information about the channel this invite is attached to. + InviteChannel Channel { get; } + } +} diff --git a/src/Discord.Net/Entities/Invites/Invite.cs b/src/Discord.Net/Entities/Invites/Invite.cs new file mode 100644 index 000000000..2dbd07ec5 --- /dev/null +++ b/src/Discord.Net/Entities/Invites/Invite.cs @@ -0,0 +1,48 @@ +using Discord.API.Rest; +using System.Threading.Tasks; +using Model = Discord.API.Invite; + +namespace Discord +{ + public class PublicInvite : IInvite, IEntity + { + /// + public string Code { get; } + /// + string IEntity.Id => Code; + /// + public DiscordClient Discord { get; } + + /// + public InviteGuild Guild { get; private set; } + /// + public InviteChannel Channel { get; private set; } + /// + public string XkcdCode { get; private set; } + + /// + public string Url => $"{DiscordConfig.InviteUrl}/{XkcdCode ?? Code}"; + /// + public string XkcdUrl => XkcdCode != null ? $"{DiscordConfig.InviteUrl}/{XkcdCode}" : null; + + internal PublicInvite(string code, DiscordClient client) + { + Code = code; + Discord = client; + } + + internal void Update(Model model) + { + XkcdCode = model.XkcdPass; + Guild = new InviteGuild(model.Guild.Id, model.Guild.Name); + Channel = new InviteChannel(model.Channel.Id, model.Channel.Name); + } + + /// + public async Task Update() + => Update(await Discord.RestClient.Send(new GetInviteRequest(Code)).ConfigureAwait(false)); + + /// + public override string ToString() => Url; + } +} diff --git a/src/Discord.Net/Entities/Invites/InviteChannel.cs b/src/Discord.Net/Entities/Invites/InviteChannel.cs new file mode 100644 index 000000000..45ac5b084 --- /dev/null +++ b/src/Discord.Net/Entities/Invites/InviteChannel.cs @@ -0,0 +1,16 @@ +namespace Discord +{ + public struct InviteChannel + { + /// Returns the unique identifier for this channel. + public ulong Id { get; } + /// Returns the name of this channel. + public string Name { get; } + + internal InviteChannel(ulong id, string name) + { + Id = id; + Name = name; + } + } +} diff --git a/src/Discord.Net/Entities/Invites/InviteGuild.cs b/src/Discord.Net/Entities/Invites/InviteGuild.cs new file mode 100644 index 000000000..0354bd282 --- /dev/null +++ b/src/Discord.Net/Entities/Invites/InviteGuild.cs @@ -0,0 +1,16 @@ +namespace Discord +{ + public struct InviteGuild + { + /// Returns the unique identifier for this guild. + public ulong Id { get; } + /// Returns the name of this guild. + public string Name { get; } + + internal InviteGuild(ulong id, string name) + { + Id = id; + Name = name; + } + } +} diff --git a/src/Discord.Net/Entities/Managers/MessageManager.cs b/src/Discord.Net/Entities/Managers/MessageManager.cs deleted file mode 100644 index 14b569750..000000000 --- a/src/Discord.Net/Entities/Managers/MessageManager.cs +++ /dev/null @@ -1,146 +0,0 @@ -using APIMessage = Discord.API.Client.Message; -using Discord.API.Client.Rest; -using Discord.Net; -using System; -using System.Collections; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Threading.Tasks; - -namespace Discord -{ - internal class MessageManager : IEnumerable - { - private readonly ITextChannel _channel; - private readonly int _size; - private readonly ConcurrentDictionary _messages; - private readonly ConcurrentQueue _orderedMessages; - - public MessageManager(ITextChannel channel, int size = 0) - { - _channel = channel; - _size = size; - if (size > 0) - { - _messages = new ConcurrentDictionary(2, size); - _orderedMessages = new ConcurrentQueue(); - } - } - - internal Message Add(APIMessage model, User user) => Add(new Message(model, _channel, user)); - internal Message Add(ulong id, User user) => Add(new Message(id, _channel, user)); - private Message Add(Message message) - { - message.State = MessageState.Normal; - if (_size > 0) - { - if (_messages.TryAdd(message.Id, message)) - { - _orderedMessages.Enqueue(message.Id); - - ulong msgId; - while (_orderedMessages.Count > _size && _orderedMessages.TryDequeue(out msgId)) - { - Message msg; - if (_messages.TryRemove(msgId, out msg)) - msg.State = MessageState.Detached; - } - } - } - return message; - } - internal Message Remove(ulong id) - { - if (_size > 0) - { - Message msg; - if (_messages.TryRemove(id, out msg)) - return msg; - } - return new Message(id, _channel, null) { State = MessageState.Deleted }; - } - - public Message Get(ulong id, ulong? userId = null) - { - if (_messages != null) - { - Message result; - if (_messages.TryGetValue(id, out result)) - return result; - } - return new Message(id, _channel, userId != null ? (_channel as Channel).GetUser(userId.Value) : null) { State = MessageState.Detached }; - } - - public async Task Download(int limit = 100, ulong? relativeMessageId = null, Relative relativeDir = Relative.Before) - { - if (limit < 0) throw new ArgumentOutOfRangeException(nameof(limit)); - if (limit == 0) return new Message[0]; - - try - { - var request = new GetMessagesRequest(_channel.Id) - { - Limit = limit, - RelativeDir = relativeMessageId.HasValue ? relativeDir == Relative.Before ? "before" : "after" : null, - RelativeId = relativeMessageId ?? 0 - }; - var msgs = await _channel.Client.ClientAPI.Send(request).ConfigureAwait(false); - var server = (_channel as PublicChannel)?.Server; - - return msgs.Select(x => - { - Message msg = null; - ulong id = x.Author.Id; - var user = server?.GetUser(id) ?? (_channel as Channel).GetUser(id); - msg = new Message(x, _channel, user); - return msg; - }).ToArray(); - } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.Forbidden) - { - return new Message[0]; - } - } - - public Task Send(string text, bool isTTS) - { - if (text == "") throw new ArgumentException("Value cannot be blank", nameof(text)); - if (text.Length > DiscordConfig.MaxMessageSize) - throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {DiscordConfig.MaxMessageSize} characters or less."); - return Task.FromResult(_channel.Client.MessageQueue.QueueSend(_channel, text, isTTS)); - } - public async Task SendFile(string filePath) - { - using (var stream = File.OpenRead(filePath)) - return await SendFile(Path.GetFileName(filePath), stream).ConfigureAwait(false); - } - public async Task SendFile(string filename, Stream stream) - { - var request = new SendFileRequest(_channel.Id) - { - Filename = filename, - Stream = stream - }; - var response = await _channel.Client.ClientAPI.Send(request).ConfigureAwait(false); - - return Add(response, (_channel as Channel).CurrentUser); - } - - public IEnumerator GetEnumerator() - { - return _orderedMessages - .Select(x => - { - Message msg; - if (_messages.TryGetValue(x, out msg)) - return msg; - return null; - }) - .Where(x => x != null).GetEnumerator(); - } - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } -} diff --git a/src/Discord.Net/Entities/Managers/PermissionManager.cs b/src/Discord.Net/Entities/Managers/PermissionManager.cs deleted file mode 100644 index 4b8eb5c8c..000000000 --- a/src/Discord.Net/Entities/Managers/PermissionManager.cs +++ /dev/null @@ -1,223 +0,0 @@ -using Discord.API.Client.Rest; -using Discord.Net; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using APIChannel = Discord.API.Client.Channel; - -namespace Discord -{ - internal class PermissionManager - { - public struct Member - { - public User User { get; } - public ChannelPermissions Permissions { get; } - - public Member(User user, ChannelPermissions permissions) - { - User = user; - Permissions = permissions; - } - } - - private readonly PublicChannel _channel; - private readonly ConcurrentDictionary _users; - private Dictionary _rules; - - public IEnumerable Users => _users.Select(x => x.Value); - public IEnumerable Rules => _rules.Values; - - public PermissionManager(PublicChannel channel, APIChannel model, int initialSize = -1) - { - _channel = channel; - if (initialSize >= 0) - _users = new ConcurrentDictionary(2, initialSize); - Update(model); - } - - public void Update(APIChannel model) - { - _rules = model.PermissionOverwrites - .Select(x => new Channel.PermissionRule(EnumConverters.ToPermissionTarget(x.Type), x.Id, x.Allow, x.Deny)) - .ToDictionary(x => x.TargetId); - UpdatePermissions(); - } - - public ChannelTriStatePermissions? GetOverwrite(User user) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - - Channel.PermissionRule rule; - if (_rules.TryGetValue(user.Id, out rule)) - return rule.Permissions; - return null; - } - public ChannelTriStatePermissions? GetOverwrite(Role role) - { - if (role == null) throw new ArgumentNullException(nameof(role)); - - Channel.PermissionRule rule; - if (_rules.TryGetValue(role.Id, out rule)) - return rule.Permissions; - return null; - } - public Task AddOrUpdateOverwrite(User user, ChannelTriStatePermissions permissions) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - return AddOrUpdateOverwrite(user.Id, PermissionTarget.User, permissions); - } - public Task AddOrUpdateOverwrite(Role role, ChannelTriStatePermissions permissions) - { - if (role == null) throw new ArgumentNullException(nameof(role)); - return AddOrUpdateOverwrite(role.Id, PermissionTarget.Role, permissions); - } - private Task AddOrUpdateOverwrite(ulong id, PermissionTarget type, ChannelTriStatePermissions permissions) - { - var request = new AddOrUpdateChannelPermissionsRequest(id) - { - TargetId = id, - TargetType = EnumConverters.ToString(type), - Allow = permissions.AllowValue, - Deny = permissions.DenyValue - }; - return _channel.Client.ClientAPI.Send(request); - } - public Task RemoveOverwrite(User user) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - return RemoveOverwrite(user.Id); - } - public Task RemoveOverwrite(Role role) - { - if (role == null) throw new ArgumentNullException(nameof(role)); - return RemoveOverwrite(role.Id); - } - private async Task RemoveOverwrite(ulong id) - { - try { await _channel.Client.ClientAPI.Send(new RemoveChannelPermissionsRequest(_channel.Id, id)).ConfigureAwait(false); } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } - } - - public ChannelPermissions GetPermissions(User user) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - - if (_users != null) - { - Member member; - if (_users.TryGetValue(user.Id, out member)) - return member.Permissions; - else - return ChannelPermissions.None; - } - else - { - ChannelPermissions perms = new ChannelPermissions(); - ResolvePermissions(user, ref perms); - return perms; - } - } - public void UpdatePermissions() - { - if (_users != null) - { - foreach (var pair in _users) - { - var member = pair.Value; - var perms = member.Permissions; - if (ResolvePermissions(member.User, ref perms)) - _users[pair.Key] = new Member(member.User, perms); - } - } - } - public void UpdatePermissions(User user) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - - if (_users != null) - { - Member member; - if (_users.TryGetValue(user.Id, out member)) - { - var perms = member.Permissions; - if (ResolvePermissions(member.User, ref perms)) - _users[user.Id] = new Member(member.User, perms); - } - } - } - public bool ResolvePermissions(User user, ref ChannelPermissions permissions) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - - uint newPermissions = 0; - var server = user.Server; - - var mask = ChannelPermissions.All(_channel.Type).RawValue; - if (_channel.IsPrivate || user.IsOwner) - newPermissions = mask; //Private messages and owners always have all permissions - else - { - //Start with this user's server permissions - newPermissions = server.GetPermissions(user).RawValue; - var rules = _rules; - - Channel.PermissionRule rule; - var roles = user.Roles.ToArray(); - if (roles.Length > 0) - { - for (int i = 0; i < roles.Length; i++) - { - if (rules.TryGetValue(roles[i].Id, out rule)) - newPermissions &= ~rule.Permissions.DenyValue; - } - for (int i = 0; i < roles.Length; i++) - { - if (rules.TryGetValue(roles[i].Id, out rule)) - newPermissions |= rule.Permissions.AllowValue; - } - } - if (rules.TryGetValue(user.Id, out rule)) - newPermissions = (newPermissions & ~rule.Permissions.DenyValue) | rule.Permissions.AllowValue; - - if (newPermissions.HasBit((byte)PermissionBits.ManageRolesOrPermissions)) - newPermissions = mask; //ManageRolesOrPermissions gives all permisions - else if (_channel.IsText && !newPermissions.HasBit((byte)PermissionBits.ReadMessages)) - newPermissions = 0; //No read permission on a text channel removes all other permissions - else if (_channel.IsVoice && !newPermissions.HasBit((byte)PermissionBits.Connect)) - newPermissions = 0; //No connect permissions on a voice channel removes all other permissions - else - newPermissions &= mask; //Ensure we didnt get any permissions this channel doesnt support (from serverPerms, for example) - } - - if (newPermissions != permissions.RawValue) - { - permissions = new ChannelPermissions(newPermissions); - return true; - } - return false; - } - - public void AddUser(User user) - { - if (user == null) throw new ArgumentNullException(nameof(user)); - - if (_users != null) - { - var perms = new ChannelPermissions(); - ResolvePermissions(user, ref perms); - var member = new Member(user, ChannelPermissions.None); - _users[user.Id] = new Member(user, ChannelPermissions.None); - } - } - public void RemoveUser(ulong id) - { - Member ignored; - if (_users != null) - _users.TryRemove(id, out ignored); - } - } -} diff --git a/src/Discord.Net/Entities/Message.cs b/src/Discord.Net/Entities/Message.cs index a812cd897..7e255ddf7 100644 --- a/src/Discord.Net/Entities/Message.cs +++ b/src/Discord.Net/Entities/Message.cs @@ -1,327 +1,116 @@ -using System; +using Discord.API.Rest; +using Discord.Net; +using System; using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; +using System.Collections.Immutable; +using System.Net; using System.Threading.Tasks; -using APIMessage = Discord.API.Client.Message; +using Model = Discord.API.Message; namespace Discord { - public class Message + public class Message : IEntity { - 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]; - - internal static string CleanUserMentions(PublicChannel channel, string text, List users = null) - { - return _userRegex.Replace(text, new MatchEvaluator(e => - { - ulong id; - if (e.Value.Substring(2, e.Value.Length - 3).TryToId(out id)) - { - var user = channel.GetUser(id); - if (user != null) - { - if (users != null) - users.Add(user); - return '@' + user.Name; - } - } - return e.Value; //User not found or parse failed - })); - } - internal static string CleanChannelMentions(PublicChannel channel, string text, List channels = null) - { - var server = channel.Server; - if (server == null) return text; - - return _channelRegex.Replace(text, new MatchEvaluator(e => - { - ulong id; - if (e.Value.Substring(2, e.Value.Length - 3).TryToId(out id)) - { - var mentionedChannel = server.GetChannel(id); - if (mentionedChannel != null && mentionedChannel.Server.Id == server.Id) - { - if (channels != null) - channels.Add(mentionedChannel); - return '#' + mentionedChannel.Name; - } - } - return e.Value; //Channel not found or parse failed - })); - } - /*internal static string CleanRoleMentions(User user, Channel channel, string text, List roles = null) - { - var server = channel.Server; - if (server == null) return text; - - return _roleRegex.Replace(text, new MatchEvaluator(e => - { - if (roles != null && user.GetPermissions(channel).MentionEveryone) - roles.Add(server.EveryoneRole); - return e.Value; - })); - }*/ - - internal static string ResolveMentions(IChannel channel, string text) - { - if (channel == null) throw new ArgumentNullException(nameof(channel)); - if (text == null) throw new ArgumentNullException(nameof(text)); - - var publicChannel = channel as PublicChannel; - if (publicChannel != null) - { - text = CleanUserMentions(publicChannel, text); - text = CleanChannelMentions(publicChannel, text); - //text = CleanRoleMentions(publicChannel, text); - } - return text; - } - - public class Attachment : File - { - /// Unique identifier for this file. - public string Id { get; internal set; } - /// Size, in bytes, of this file file. - public int Size { get; internal set; } - /// Filename of this file. - public string Filename { get; internal set; } - - internal Attachment() { } - } - - public class Embed - { - /// URL of this embed. - public string Url { get; internal set; } - /// Type of this embed. - public string Type { get; internal set; } - /// Title for this embed. - public string Title { get; internal set; } - /// Summary of this embed. - public string Description { get; internal set; } - /// Returns information about the author of this embed. - public EmbedLink Author { get; internal set; } - /// Returns information about the providing website of this embed. - 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() { } - } - - public class EmbedLink - { - /// URL of this embed provider. - public string Url { get; internal set; } - /// Name of this embed provider. - public string Name { get; internal set; } - - internal EmbedLink() { } - } - - public class File - { - /// Download url for this file. - public string Url { get; internal set; } - /// Preview url for this file. - public string ProxyUrl { get; internal set; } - /// Width of this file, if it is an image. - public int? Width { get; internal set; } - /// Height of this file, if it is an image. - public int? Height { get; internal set; } - - internal File() { } - } - - public DiscordClient Client => Channel.Client; - - /// Returns the unique identifier for this message. - public ulong Id { get; internal set; } - /// Returns the channel this message was sent to. - public ITextChannel Channel { get; } - /// Returns the author of this message. + /// + public ulong Id { get; } + public IMessageChannel Channel { get; } public User User { get; } - /// Returns true if the message was sent as text-to-speech by someone with permissions to do so. public bool IsTTS { get; internal set; } - /// Returns the state of this message. Only useful if UseMessageQueue is true. - public MessageState State { get; internal set; } - /// Returns the raw content of this message as it was received from the server. - public string RawText { get; internal set; } - /// Returns the content of this message with any special references such as mentions converted. - public string Text { get; internal set; } - /// Returns the timestamp for when this message was sent. - public DateTime Timestamp { get; private set; } - /// Returns the timestamp for when this message was last edited. - public DateTime? EditedTimestamp { get; private set; } - /// Returns the attachments included in this message. - public Attachment[] Attachments { get; private set; } - /// Returns a collection of all embeded content in this message. - public Embed[] Embeds { get; private set; } - - /// Returns a collection of all users mentioned in this message. - public IEnumerable MentionedUsers { get; internal set; } - /// Returns a collection of all channels mentioned in this message. - public IEnumerable MentionedChannels { get; internal set; } - /// Returns a collection of all roles mentioned in this message. - public IEnumerable MentionedRoles { get; internal set; } - + public string RawText { get; internal set; } + public string Text { get; internal set; } + public DateTime Timestamp { get; internal set; } + public DateTime? EditedTimestamp { get; private set; } + public IReadOnlyList Attachments { get; private set; } + public IReadOnlyList Embeds { get; private set; } + public IReadOnlyList MentionedUsers { get; private set; } + public IReadOnlyList MentionedChannels { get; private set; } + public IReadOnlyList MentionedRoles { get; private set; } internal int Nonce { get; set; } - /// Returns the server containing the channel this message was sent to. - public Server Server => (Channel as PublicChannel)?.Server; - /// Returns if this message was sent from the logged-in accounts. - public bool IsAuthor => User != null && User.Id == Client.CurrentUser?.Id; + public DiscordClient Discord => Channel.Discord; + public bool IsAuthor => false; - internal Message(APIMessage model, ITextChannel channel, User user) - : this(model.Id, channel, user) - { - Update(model); - } - internal Message(ulong id, ITextChannel channel, User user) + internal Message(ulong id, IMessageChannel channel, User user) { Id = id; Channel = channel; User = user; - Attachments = _initialAttachments; - Embeds = _initialEmbeds; } - internal void Update(APIMessage model) - { - var channel = Channel; - if (model.Attachments != null) - { - Attachments = model.Attachments - .Select(x => new Attachment() - { - Id = x.Id, - Url = x.Url, - ProxyUrl = x.ProxyUrl, - Width = x.Width, - Height = x.Height, - Size = x.Size, - Filename = x.Filename - }) - .ToArray(); - } - if (model.Embeds != null) - { - Embeds = model.Embeds.Select(x => - { - EmbedLink author = null, provider = null; - File thumbnail = null, video = null; + internal void Update(Model model) + { + var channel = Channel; + bool isPublic = channel.Type != ChannelType.DM; - if (x.Author != null) - author = new EmbedLink { Url = x.Author.Url, Name = x.Author.Name }; - if (x.Provider != null) - 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 }; + IsTTS = model.IsTextToSpeech; + Timestamp = model.Timestamp; + EditedTimestamp = model.EditedTimestamp; + RawText = model.Content; - return new Embed - { - Url = x.Url, - Type = x.Type, - Title = x.Title, - Description = x.Description, - Author = author, - Provider = provider, - Thumbnail = thumbnail, - Video = video - }; - }).ToArray(); - } - - if (model.IsTextToSpeech != null) - IsTTS = model.IsTextToSpeech.Value; - if (model.Timestamp != null) - Timestamp = model.Timestamp.Value; - if (model.EditedTimestamp != null) - EditedTimestamp = model.EditedTimestamp; - if (model.Mentions != null) - { - MentionedUsers = model.Mentions - .Select(x => (Channel as Channel).GetUser(x.Id)) - .Where(x => x != null) - .ToArray(); - } - if (model.IsMentioningEveryone != null) + if (model.Attachments.Length > 0) { - var server = (channel as PublicChannel).Server; - if (model.IsMentioningEveryone.Value && server != null) - MentionedRoles = new Role[] { server.EveryoneRole }; - else - MentionedRoles = Enumerable.Empty(); + var attachments = new Attachment[model.Attachments.Length]; + for (int i = 0; i < attachments.Length; i++) + attachments[i] = new Attachment(model.Attachments[i]); + Attachments = ImmutableArray.Create(attachments); } - if (model.Content != null) - { - string text = model.Content; - RawText = text; - - List mentionedChannels = null; - if (Channel.IsPublic) - mentionedChannels = new List(); - - text = CleanUserMentions(Channel as PublicChannel, text); - text = CleanChannelMentions(Channel as PublicChannel, text, mentionedChannels); - - if (Channel.IsPublic) - MentionedChannels = mentionedChannels; - - Text = text; - } - } + else + Attachments = ImmutableArray.Empty; - public Task Edit(string text) - { - if (text == null) throw new ArgumentNullException(nameof(text)); + if (model.Embeds.Length > 0) + { + var embeds = new Embed[model.Attachments.Length]; + for (int i = 0; i < embeds.Length; i++) + embeds[i] = new Embed(model.Embeds[i]); + Embeds = ImmutableArray.Create(embeds); + } + else + Embeds = ImmutableArray.Empty; - var channel = Channel; + if (model.Mentions.Length > 0) + { + var users = new GuildUser[model.Mentions.Length]; + int j = 0; + for (int i = 0; i < users.Length; i++) + { + var user = Channel.GetUser(model.Mentions[i].Id) as GuildUser; + if (user != null) + users[j++] = user; + } + MentionedUsers = ImmutableArray.Create(users, 0, j); + } + else + MentionedUsers = ImmutableArray.Empty; - if (text.Length > DiscordConfig.MaxMessageSize) - throw new ArgumentOutOfRangeException(nameof(text), $"Message must be {DiscordConfig.MaxMessageSize} characters or less."); - - Client.MessageQueue.QueueEdit(this, text); - return TaskHelper.CompletedTask; - } - public Task Delete() - { - Client.MessageQueue.QueueDelete(this); - return TaskHelper.CompletedTask; - } + if (model.IsMentioningEveryone && isPublic) + MentionedRoles = ImmutableArray.Create((channel as GuildChannel).Guild.EveryoneRole); + else + MentionedRoles = ImmutableArray.Empty; - /// Returns true if the logged-in user was mentioned. - public bool IsMentioningMe(bool includeRoles = false) - { - User me = Server != null ? Server.CurrentUser : Channel.Client.PrivateUser; - if (includeRoles) + string text = model.Content; + if (isPublic) { - return (MentionedUsers?.Contains(me) ?? false) || - (MentionedRoles?.Any(x => me.HasRole(x)) ?? false); + var publicChannel = channel as GuildChannel; + var mentionedChannels = ImmutableArray.CreateBuilder(); + text = MentionHelper.CleanUserMentions(publicChannel, text); + text = MentionHelper.CleanChannelMentions(publicChannel, text, mentionedChannels); + MentionedChannels = mentionedChannels.ToImmutable(); } else - return MentionedUsers?.Contains(me) ?? false; + MentionedChannels = ImmutableArray.Empty; + Text = text; } - internal Message Clone() + public bool IsMentioningMe(bool includeRoles = false) => false; + + public Task Update() { throw new NotSupportedException(); } //TODO: Not supported yet + + /// Deletes this message. + public async Task Delete() { - var result = new Message(Id, Channel, User); - _cloner(this, result); - return result; + try { await Discord.RestClient.Send(new DeleteMessageRequest(Channel.Id, Id)).ConfigureAwait(false); } + catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } } - - public override string ToString() => $"{User}: {RawText}"; - } + } } diff --git a/src/Discord.Net/Entities/Permissions.cs b/src/Discord.Net/Entities/Permissions.cs deleted file mode 100644 index badccc116..000000000 --- a/src/Discord.Net/Entities/Permissions.cs +++ /dev/null @@ -1,345 +0,0 @@ -using System; -using System.Runtime.CompilerServices; - -namespace Discord -{ - public struct ServerPermissions - { - public static ServerPermissions None { get; } = new ServerPermissions(); - public static ServerPermissions All { get; } = new ServerPermissions(Convert.ToUInt32("00000011111100111111110000111111", 2)); - - public uint RawValue { get; } - - /// If True, a user may create invites. - public bool CreateInstantInvite => PermissionsHelper.GetValue(RawValue, PermissionBits.CreateInstantInvite); - /// If True, a user may ban users from the server. - public bool BanMembers => PermissionsHelper.GetValue(RawValue, PermissionBits.BanMembers); - /// If True, a user may kick users from the server. - public bool KickMembers => PermissionsHelper.GetValue(RawValue, PermissionBits.KickMembers); - /// If True, a user may adjust roles. This also implictly grants all other permissions. - public bool ManageRoles => PermissionsHelper.GetValue(RawValue, PermissionBits.ManageRolesOrPermissions); - /// If True, a user may create, delete and modify channels. - public bool ManageChannels => PermissionsHelper.GetValue(RawValue, PermissionBits.ManageChannel); - /// If True, a user may adjust server properties. - public bool ManageServer => PermissionsHelper.GetValue(RawValue, PermissionBits.ManageServer); - - /// If True, a user may join channels. - public bool ReadMessages => PermissionsHelper.GetValue(RawValue, PermissionBits.ReadMessages); - /// If True, a user may send messages. - public bool SendMessages => PermissionsHelper.GetValue(RawValue, PermissionBits.SendMessages); - /// If True, a user may send text-to-speech messages. - public bool SendTTSMessages => PermissionsHelper.GetValue(RawValue, PermissionBits.SendTTSMessages); - /// If True, a user may delete messages. - public bool ManageMessages => PermissionsHelper.GetValue(RawValue, PermissionBits.ManageMessages); - /// If True, Discord will auto-embed links sent by this user. - public bool EmbedLinks => PermissionsHelper.GetValue(RawValue, PermissionBits.EmbedLinks); - /// If True, a user may send files. - public bool AttachFiles => PermissionsHelper.GetValue(RawValue, PermissionBits.AttachFiles); - /// If True, a user may read previous messages. - public bool ReadMessageHistory => PermissionsHelper.GetValue(RawValue, PermissionBits.ReadMessageHistory); - /// If True, a user may mention @everyone. - public bool MentionEveryone => PermissionsHelper.GetValue(RawValue, PermissionBits.MentionEveryone); - - /// If True, a user may connect to a voice channel. - public bool Connect => PermissionsHelper.GetValue(RawValue, PermissionBits.Connect); - /// If True, a user may speak in a voice channel. - public bool Speak => PermissionsHelper.GetValue(RawValue, PermissionBits.Speak); - /// If True, a user may mute users. - public bool MuteMembers => PermissionsHelper.GetValue(RawValue, PermissionBits.MuteMembers); - /// If True, a user may deafen users. - public bool DeafenMembers => PermissionsHelper.GetValue(RawValue, PermissionBits.DeafenMembers); - /// If True, a user may move other users between voice channels. - public bool MoveMembers => PermissionsHelper.GetValue(RawValue, PermissionBits.MoveMembers); - /// If True, a user may use voice activation rather than push-to-talk. - public bool UseVoiceActivation => PermissionsHelper.GetValue(RawValue, PermissionBits.UseVoiceActivation); - - public ServerPermissions(bool? createInstantInvite = null, bool? manageRoles = null, - bool? kickMembers = null, bool? banMembers = null, bool? manageChannel = null, bool? manageServer = null, - bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, bool? manageMessages = null, - bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, bool? mentionEveryone = null, - bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, - bool? moveMembers = null, bool? useVoiceActivation = null) - : this(new ServerPermissions(), createInstantInvite, manageRoles, kickMembers, banMembers, manageChannel, manageServer, readMessages, - sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles, mentionEveryone, connect, speak, muteMembers, deafenMembers, - moveMembers, useVoiceActivation) - { - } - public ServerPermissions(ServerPermissions basePerms, bool? createInstantInvite = null, bool? manageRoles = null, - bool? kickMembers = null, bool? banMembers = null, bool? manageChannel = null, bool? manageServer = null, - bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, bool? manageMessages = null, - bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, bool? mentionEveryone = null, - bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, - bool? moveMembers = null, bool? useVoiceActivation = null) - { - uint value = basePerms.RawValue; - - PermissionsHelper.SetValue(ref value, createInstantInvite, PermissionBits.CreateInstantInvite); - PermissionsHelper.SetValue(ref value, banMembers, PermissionBits.BanMembers); - PermissionsHelper.SetValue(ref value, kickMembers, PermissionBits.KickMembers); - PermissionsHelper.SetValue(ref value, manageRoles, PermissionBits.ManageRolesOrPermissions); - PermissionsHelper.SetValue(ref value, manageChannel, PermissionBits.ManageChannel); - PermissionsHelper.SetValue(ref value, manageServer, PermissionBits.ManageServer); - PermissionsHelper.SetValue(ref value, readMessages, PermissionBits.ReadMessages); - PermissionsHelper.SetValue(ref value, sendMessages, PermissionBits.SendMessages); - PermissionsHelper.SetValue(ref value, sendTTSMessages, PermissionBits.SendTTSMessages); - PermissionsHelper.SetValue(ref value, manageMessages, PermissionBits.ManageMessages); - PermissionsHelper.SetValue(ref value, embedLinks, PermissionBits.EmbedLinks); - PermissionsHelper.SetValue(ref value, attachFiles, PermissionBits.AttachFiles); - PermissionsHelper.SetValue(ref value, readMessageHistory, PermissionBits.ReadMessageHistory); - PermissionsHelper.SetValue(ref value, mentionEveryone, PermissionBits.MentionEveryone); - PermissionsHelper.SetValue(ref value, connect, PermissionBits.Connect); - PermissionsHelper.SetValue(ref value, speak, PermissionBits.Speak); - PermissionsHelper.SetValue(ref value, muteMembers, PermissionBits.MuteMembers); - PermissionsHelper.SetValue(ref value, deafenMembers, PermissionBits.DeafenMembers); - PermissionsHelper.SetValue(ref value, moveMembers, PermissionBits.MoveMembers); - PermissionsHelper.SetValue(ref value, useVoiceActivation, PermissionBits.UseVoiceActivation); - - RawValue = value; - } - public ServerPermissions(uint rawValue) { RawValue = rawValue; } - - public override string ToString() => Convert.ToString(RawValue, 2); - } - - public struct ChannelPermissions - { - 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)); - public static ChannelPermissions All(ChannelType channelType) - { - switch (channelType) - { - case ChannelType.Text: return TextOnly; - case ChannelType.Voice: return VoiceOnly; - case ChannelType.Private: return PrivateOnly; - default: return None; - } - } - - public uint RawValue { get; } - - /// If True, a user may create invites. - public bool CreateInstantInvite => PermissionsHelper.GetValue(RawValue, PermissionBits.CreateInstantInvite); - /// If True, a user may adjust permissions. This also implictly grants all other permissions. - public bool ManagePermissions => PermissionsHelper.GetValue(RawValue, PermissionBits.ManageRolesOrPermissions); - /// If True, a user may create, delete and modify this channel. - public bool ManageChannel => PermissionsHelper.GetValue(RawValue, PermissionBits.ManageChannel); - - /// If True, a user may join channels. - public bool ReadMessages => PermissionsHelper.GetValue(RawValue, PermissionBits.ReadMessages); - /// If True, a user may send messages. - public bool SendMessages => PermissionsHelper.GetValue(RawValue, PermissionBits.SendMessages); - /// If True, a user may send text-to-speech messages. - public bool SendTTSMessages => PermissionsHelper.GetValue(RawValue, PermissionBits.SendTTSMessages); - /// If True, a user may delete messages. - public bool ManageMessages => PermissionsHelper.GetValue(RawValue, PermissionBits.ManageMessages); - /// If True, Discord will auto-embed links sent by this user. - public bool EmbedLinks => PermissionsHelper.GetValue(RawValue, PermissionBits.EmbedLinks); - /// If True, a user may send files. - public bool AttachFiles => PermissionsHelper.GetValue(RawValue, PermissionBits.AttachFiles); - /// If True, a user may read previous messages. - public bool ReadMessageHistory => PermissionsHelper.GetValue(RawValue, PermissionBits.ReadMessageHistory); - /// If True, a user may mention @everyone. - public bool MentionEveryone => PermissionsHelper.GetValue(RawValue, PermissionBits.MentionEveryone); - - /// If True, a user may connect to a voice channel. - public bool Connect => PermissionsHelper.GetValue(RawValue, PermissionBits.Connect); - /// If True, a user may speak in a voice channel. - public bool Speak => PermissionsHelper.GetValue(RawValue, PermissionBits.Speak); - /// If True, a user may mute users. - public bool MuteMembers => PermissionsHelper.GetValue(RawValue, PermissionBits.MuteMembers); - /// If True, a user may deafen users. - public bool DeafenMembers => PermissionsHelper.GetValue(RawValue, PermissionBits.DeafenMembers); - /// If True, a user may move other users between voice channels. - public bool MoveMembers => PermissionsHelper.GetValue(RawValue, PermissionBits.MoveMembers); - /// If True, a user may use voice activation rather than push-to-talk. - public bool UseVoiceActivation => PermissionsHelper.GetValue(RawValue, PermissionBits.UseVoiceActivation); - - public ChannelPermissions(bool? createInstantInvite = null, bool? managePermissions = null, - bool? manageChannel = null, bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, - bool? manageMessages = null, bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, - bool? mentionEveryone = null, bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, - bool? moveMembers = null, bool? useVoiceActivation = null) - : this(new ChannelPermissions(), createInstantInvite, managePermissions, manageChannel, readMessages, sendMessages, sendTTSMessages, - manageMessages, embedLinks, attachFiles, mentionEveryone, connect, speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation) - { - } - public ChannelPermissions(ChannelPermissions basePerms, bool? createInstantInvite = null, bool? managePermissions = null, - bool? manageChannel = null, bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, - bool? manageMessages = null, bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, - bool? mentionEveryone = null, bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, - bool? moveMembers = null, bool? useVoiceActivation = null) - { - uint value = basePerms.RawValue; - - PermissionsHelper.SetValue(ref value, createInstantInvite, PermissionBits.CreateInstantInvite); - PermissionsHelper.SetValue(ref value, managePermissions, PermissionBits.ManageRolesOrPermissions); - PermissionsHelper.SetValue(ref value, manageChannel, PermissionBits.ManageChannel); - PermissionsHelper.SetValue(ref value, readMessages, PermissionBits.ReadMessages); - PermissionsHelper.SetValue(ref value, sendMessages, PermissionBits.SendMessages); - PermissionsHelper.SetValue(ref value, sendTTSMessages, PermissionBits.SendTTSMessages); - PermissionsHelper.SetValue(ref value, manageMessages, PermissionBits.ManageMessages); - PermissionsHelper.SetValue(ref value, embedLinks, PermissionBits.EmbedLinks); - PermissionsHelper.SetValue(ref value, attachFiles, PermissionBits.AttachFiles); - PermissionsHelper.SetValue(ref value, readMessageHistory, PermissionBits.ReadMessageHistory); - PermissionsHelper.SetValue(ref value, mentionEveryone, PermissionBits.MentionEveryone); - PermissionsHelper.SetValue(ref value, connect, PermissionBits.Connect); - PermissionsHelper.SetValue(ref value, speak, PermissionBits.Speak); - PermissionsHelper.SetValue(ref value, muteMembers, PermissionBits.MuteMembers); - PermissionsHelper.SetValue(ref value, deafenMembers, PermissionBits.DeafenMembers); - PermissionsHelper.SetValue(ref value, moveMembers, PermissionBits.MoveMembers); - PermissionsHelper.SetValue(ref value, useVoiceActivation, PermissionBits.UseVoiceActivation); - - RawValue = value; - } - public ChannelPermissions(uint rawValue) { RawValue = rawValue; } - - public override string ToString() => Convert.ToString(RawValue, 2); - } - - public struct ChannelTriStatePermissions - { - public static ChannelTriStatePermissions InheritAll { get; } = new ChannelTriStatePermissions(); - - public uint AllowValue { get; } - public uint DenyValue { get; } - - /// If True, a user may create invites. - public PermValue CreateInstantInvite => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.CreateInstantInvite); - /// If True, a user may adjust permissions. This also implictly grants all other permissions. - public PermValue ManagePermissions => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.ManageRolesOrPermissions); - /// If True, a user may create, delete and modify this channel. - public PermValue ManageChannel => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.ManageChannel); - /// If True, a user may join channels. - public PermValue ReadMessages => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.ReadMessages); - /// If True, a user may send messages. - public PermValue SendMessages => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.SendMessages); - /// If True, a user may send text-to-speech messages. - public PermValue SendTTSMessages => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.SendTTSMessages); - /// If True, a user may delete messages. - public PermValue ManageMessages => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.ManageMessages); - /// If True, Discord will auto-embed links sent by this user. - public PermValue EmbedLinks => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.EmbedLinks); - /// If True, a user may send files. - public PermValue AttachFiles => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.AttachFiles); - /// If True, a user may read previous messages. - public PermValue ReadMessageHistory => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.ReadMessageHistory); - /// If True, a user may mention @everyone. - public PermValue MentionEveryone => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.MentionEveryone); - - /// If True, a user may connect to a voice channel. - public PermValue Connect => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.Connect); - /// If True, a user may speak in a voice channel. - public PermValue Speak => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.Speak); - /// If True, a user may mute users. - public PermValue MuteMembers => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.MuteMembers); - /// If True, a user may deafen users. - public PermValue DeafenMembers => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.DeafenMembers); - /// If True, a user may move other users between voice channels. - public PermValue MoveMembers => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.MoveMembers); - /// If True, a user may use voice activation rather than push-to-talk. - public PermValue UseVoiceActivation => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBits.UseVoiceActivation); - - public ChannelTriStatePermissions(PermValue? createInstantInvite = null, PermValue? managePermissions = null, - PermValue? manageChannel = null, PermValue? readMessages = null, PermValue? sendMessages = null, PermValue? sendTTSMessages = null, - PermValue? manageMessages = null, PermValue? embedLinks = null, PermValue? attachFiles = null, PermValue? readMessageHistory = null, - PermValue? mentionEveryone = null, PermValue? connect = null, PermValue? speak = null, PermValue? muteMembers = null, PermValue? deafenMembers = null, - PermValue? moveMembers = null, PermValue? useVoiceActivation = null) - : this(new ChannelTriStatePermissions(), createInstantInvite, managePermissions, manageChannel, readMessages, sendMessages, sendTTSMessages, - manageMessages, embedLinks, attachFiles, mentionEveryone, connect, speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation) - { - } - public ChannelTriStatePermissions(ChannelTriStatePermissions basePerms, PermValue? createInstantInvite = null, PermValue? managePermissions = null, - PermValue? manageChannel = null, PermValue? readMessages = null, PermValue? sendMessages = null, PermValue? sendTTSMessages = null, - PermValue? manageMessages = null, PermValue? embedLinks = null, PermValue? attachFiles = null, PermValue? readMessageHistory = null, - PermValue? mentionEveryone = null, PermValue? connect = null, PermValue? speak = null, PermValue? muteMembers = null, PermValue? deafenMembers = null, - PermValue? moveMembers = null, PermValue? useVoiceActivation = null) - { - uint allow = basePerms.AllowValue, deny = basePerms.DenyValue; - - PermissionsHelper.SetValue(ref allow, ref deny, createInstantInvite, PermissionBits.CreateInstantInvite); - PermissionsHelper.SetValue(ref allow, ref deny, managePermissions, PermissionBits.ManageRolesOrPermissions); - PermissionsHelper.SetValue(ref allow, ref deny, manageChannel, PermissionBits.ManageChannel); - PermissionsHelper.SetValue(ref allow, ref deny, readMessages, PermissionBits.ReadMessages); - PermissionsHelper.SetValue(ref allow, ref deny, sendMessages, PermissionBits.SendMessages); - PermissionsHelper.SetValue(ref allow, ref deny, sendTTSMessages, PermissionBits.SendTTSMessages); - PermissionsHelper.SetValue(ref allow, ref deny, manageMessages, PermissionBits.ManageMessages); - PermissionsHelper.SetValue(ref allow, ref deny, embedLinks, PermissionBits.EmbedLinks); - PermissionsHelper.SetValue(ref allow, ref deny, attachFiles, PermissionBits.AttachFiles); - PermissionsHelper.SetValue(ref allow, ref deny, readMessageHistory, PermissionBits.ReadMessageHistory); - PermissionsHelper.SetValue(ref allow, ref deny, mentionEveryone, PermissionBits.MentionEveryone); - PermissionsHelper.SetValue(ref allow, ref deny, connect, PermissionBits.Connect); - PermissionsHelper.SetValue(ref allow, ref deny, speak, PermissionBits.Speak); - PermissionsHelper.SetValue(ref allow, ref deny, muteMembers, PermissionBits.MuteMembers); - PermissionsHelper.SetValue(ref allow, ref deny, deafenMembers, PermissionBits.DeafenMembers); - PermissionsHelper.SetValue(ref allow, ref deny, moveMembers, PermissionBits.MoveMembers); - PermissionsHelper.SetValue(ref allow, ref deny, useVoiceActivation, PermissionBits.UseVoiceActivation); - - AllowValue = allow; - DenyValue = deny; - } - public ChannelTriStatePermissions(uint allow = 0, uint deny = 0) - { - AllowValue = allow; - DenyValue = deny; - } - - public override string ToString() => $"Allow: {Convert.ToString(AllowValue, 2)}, Deny: {Convert.ToString(DenyValue, 2)}"; - } - internal static class PermissionsHelper - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static PermValue GetValue(uint allow, uint deny, PermissionBits bit) - { - if (allow.HasBit((byte)bit)) - return PermValue.Allow; - else if (deny.HasBit((byte)bit)) - return PermValue.Deny; - else - return PermValue.Inherit; - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool GetValue(uint value, PermissionBits bit) => value.HasBit((byte)bit); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void SetValue(ref uint rawValue, bool? value, PermissionBits bit) - { - if (value.HasValue) - { - if (value == true) - SetBit(ref rawValue, bit); - else - UnsetBit(ref rawValue, bit); - } - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void SetValue(ref uint allow, ref uint deny, PermValue? value, PermissionBits bit) - { - if (value.HasValue) - { - switch (value) - { - case PermValue.Allow: - SetBit(ref allow, bit); - UnsetBit(ref deny, bit); - break; - case PermValue.Deny: - UnsetBit(ref allow, bit); - SetBit(ref deny, bit); - break; - default: - UnsetBit(ref allow, bit); - UnsetBit(ref deny, bit); - break; - } - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void SetBit(ref uint value, PermissionBits bit) => value |= 1U << (int)bit; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void UnsetBit(ref uint value, PermissionBits bit) => value &= ~(1U << (int)bit); - } -} diff --git a/src/Discord.Net/Entities/Permissions/ChannelPermissions.cs b/src/Discord.Net/Entities/Permissions/ChannelPermissions.cs new file mode 100644 index 000000000..7b37fccd0 --- /dev/null +++ b/src/Discord.Net/Entities/Permissions/ChannelPermissions.cs @@ -0,0 +1,122 @@ +using System; + +namespace Discord +{ + public struct ChannelPermissions + { + private static ChannelPermissions _allDM { get; } = new ChannelPermissions(Convert.ToUInt32("00000000000000111111110000011001", 2)); + private static ChannelPermissions _allText { get; } = new ChannelPermissions(Convert.ToUInt32("00000000000000011100110000000000", 2)); + private static ChannelPermissions _allVoice { get; } = new ChannelPermissions(Convert.ToUInt32("00000011111100000000000000011001", 2)); + + /// Gets a blank ChannelPermissions that grants no permissions. + public static ChannelPermissions None { get; } = new ChannelPermissions(); + /// Gets a ChannelPermissions that grants all permissions for a given channelType. + public static ChannelPermissions All(ChannelType channelType) + { + switch (channelType) + { + case ChannelType.DM: + return _allText; + case ChannelType.Text: + return _allDM; + case ChannelType.Voice: + return _allVoice; + default: + throw new ArgumentOutOfRangeException(nameof(channelType)); + } + } + + /// Gets a packed value representing all the permissions in this ChannelPermissions. + public uint RawValue { get; } + + /// If True, a user may create invites. + public bool CreateInstantInvite => PermissionsHelper.GetValue(RawValue, PermissionBit.CreateInstantInvite); + /// If True, a user may adjust permissions. This also implictly grants all other permissions. + public bool ManagePermissions => PermissionsHelper.GetValue(RawValue, PermissionBit.ManageRolesOrPermissions); + /// If True, a user may create, delete and modify this channel. + public bool ManageChannel => PermissionsHelper.GetValue(RawValue, PermissionBit.ManageChannel); + + /// If True, a user may join channels. + public bool ReadMessages => PermissionsHelper.GetValue(RawValue, PermissionBit.ReadMessages); + /// If True, a user may send messages. + public bool SendMessages => PermissionsHelper.GetValue(RawValue, PermissionBit.SendMessages); + /// If True, a user may send text-to-speech messages. + public bool SendTTSMessages => PermissionsHelper.GetValue(RawValue, PermissionBit.SendTTSMessages); + /// If True, a user may delete messages. + public bool ManageMessages => PermissionsHelper.GetValue(RawValue, PermissionBit.ManageMessages); + /// If True, Discord will auto-embed links sent by this user. + public bool EmbedLinks => PermissionsHelper.GetValue(RawValue, PermissionBit.EmbedLinks); + /// If True, a user may send files. + public bool AttachFiles => PermissionsHelper.GetValue(RawValue, PermissionBit.AttachFiles); + /// If True, a user may read previous messages. + public bool ReadMessageHistory => PermissionsHelper.GetValue(RawValue, PermissionBit.ReadMessageHistory); + /// If True, a user may mention @everyone. + public bool MentionEveryone => PermissionsHelper.GetValue(RawValue, PermissionBit.MentionEveryone); + + /// If True, a user may connect to a voice channel. + public bool Connect => PermissionsHelper.GetValue(RawValue, PermissionBit.Connect); + /// If True, a user may speak in a voice channel. + public bool Speak => PermissionsHelper.GetValue(RawValue, PermissionBit.Speak); + /// If True, a user may mute users. + public bool MuteMembers => PermissionsHelper.GetValue(RawValue, PermissionBit.MuteMembers); + /// If True, a user may deafen users. + public bool DeafenMembers => PermissionsHelper.GetValue(RawValue, PermissionBit.DeafenMembers); + /// If True, a user may move other users between voice channels. + public bool MoveMembers => PermissionsHelper.GetValue(RawValue, PermissionBit.MoveMembers); + /// If True, a user may use voice activation rather than push-to-talk. + public bool UseVoiceActivation => PermissionsHelper.GetValue(RawValue, PermissionBit.UseVoiceActivation); + + /// Creates a new ChannelPermissions with the provided packed value. + public ChannelPermissions(uint rawValue) { RawValue = rawValue; } + + private ChannelPermissions(uint initialValue, bool? createInstantInvite = null, bool? managePermissions = null, + bool? manageChannel = null, bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, + bool? manageMessages = null, bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, + bool? mentionEveryone = null, bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, + bool? moveMembers = null, bool? useVoiceActivation = null) + { + uint value = initialValue; + + PermissionsHelper.SetValue(ref value, createInstantInvite, PermissionBit.CreateInstantInvite); + PermissionsHelper.SetValue(ref value, managePermissions, PermissionBit.ManageRolesOrPermissions); + PermissionsHelper.SetValue(ref value, manageChannel, PermissionBit.ManageChannel); + PermissionsHelper.SetValue(ref value, readMessages, PermissionBit.ReadMessages); + PermissionsHelper.SetValue(ref value, sendMessages, PermissionBit.SendMessages); + PermissionsHelper.SetValue(ref value, sendTTSMessages, PermissionBit.SendTTSMessages); + PermissionsHelper.SetValue(ref value, manageMessages, PermissionBit.ManageMessages); + PermissionsHelper.SetValue(ref value, embedLinks, PermissionBit.EmbedLinks); + PermissionsHelper.SetValue(ref value, attachFiles, PermissionBit.AttachFiles); + PermissionsHelper.SetValue(ref value, readMessageHistory, PermissionBit.ReadMessageHistory); + PermissionsHelper.SetValue(ref value, mentionEveryone, PermissionBit.MentionEveryone); + PermissionsHelper.SetValue(ref value, connect, PermissionBit.Connect); + PermissionsHelper.SetValue(ref value, speak, PermissionBit.Speak); + PermissionsHelper.SetValue(ref value, muteMembers, PermissionBit.MuteMembers); + PermissionsHelper.SetValue(ref value, deafenMembers, PermissionBit.DeafenMembers); + PermissionsHelper.SetValue(ref value, moveMembers, PermissionBit.MoveMembers); + PermissionsHelper.SetValue(ref value, useVoiceActivation, PermissionBit.UseVoiceActivation); + + RawValue = value; + } + + /// Creates a new ChannelPermissions with the provided permissions. + public ChannelPermissions(bool createInstantInvite = false, bool managePermissions = false, + bool manageChannel = false, bool readMessages = false, bool sendMessages = false, bool sendTTSMessages = false, + bool manageMessages = false, bool embedLinks = false, bool attachFiles = false, bool readMessageHistory = false, + bool mentionEveryone = false, bool connect = false, bool speak = false, bool muteMembers = false, bool deafenMembers = false, + bool moveMembers = false, bool useVoiceActivation = false) + : this(0, createInstantInvite, managePermissions, manageChannel, readMessages, sendMessages, sendTTSMessages, + manageMessages, embedLinks, attachFiles, mentionEveryone, connect, speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation) { } + + /// Creates a new ChannelPermissions from this one, changing the provided non-null permissions. + public ChannelPermissions Modify(bool? createInstantInvite = null, bool? managePermissions = null, + bool? manageChannel = null, bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, + bool? manageMessages = null, bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, + bool? mentionEveryone = null, bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, + bool? moveMembers = null, bool? useVoiceActivation = null) + => new ChannelPermissions(RawValue, createInstantInvite, managePermissions, manageChannel, readMessages, sendMessages, sendTTSMessages, + manageMessages, embedLinks, attachFiles, mentionEveryone, connect, speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation); + + /// + public override string ToString() => Convert.ToString(RawValue, 2); + } +} diff --git a/src/Discord.Net/Entities/Permissions/GuildPermissions.cs b/src/Discord.Net/Entities/Permissions/GuildPermissions.cs new file mode 100644 index 000000000..7be0febe1 --- /dev/null +++ b/src/Discord.Net/Entities/Permissions/GuildPermissions.cs @@ -0,0 +1,119 @@ +using System; + +namespace Discord +{ + public struct GuildPermissions + { + /// Gets a blank GuildPermissions that grants no permissions. + public static GuildPermissions None { get; } = new GuildPermissions(); + /// Gets a GuildPermissions that grants all permissions. + public static GuildPermissions All { get; } = new GuildPermissions(Convert.ToUInt32("00000011111100111111110000111111", 2)); + + /// Gets a packed value representing all the permissions in this GuildPermissions. + public uint RawValue { get; } + + /// If True, a user may create invites. + public bool CreateInstantInvite => PermissionsHelper.GetValue(RawValue, PermissionBit.CreateInstantInvite); + /// If True, a user may ban users from the guild. + public bool BanMembers => PermissionsHelper.GetValue(RawValue, PermissionBit.BanMembers); + /// If True, a user may kick users from the guild. + public bool KickMembers => PermissionsHelper.GetValue(RawValue, PermissionBit.KickMembers); + /// If True, a user may adjust roles. This also implictly grants all other permissions. + public bool ManageRoles => PermissionsHelper.GetValue(RawValue, PermissionBit.ManageRolesOrPermissions); + /// If True, a user may create, delete and modify channels. + public bool ManageChannels => PermissionsHelper.GetValue(RawValue, PermissionBit.ManageChannel); + /// If True, a user may adjust guild properties. + public bool ManageGuild => PermissionsHelper.GetValue(RawValue, PermissionBit.ManageGuild); + + /// If True, a user may join channels. + public bool ReadMessages => PermissionsHelper.GetValue(RawValue, PermissionBit.ReadMessages); + /// If True, a user may send messages. + public bool SendMessages => PermissionsHelper.GetValue(RawValue, PermissionBit.SendMessages); + /// If True, a user may send text-to-speech messages. + public bool SendTTSMessages => PermissionsHelper.GetValue(RawValue, PermissionBit.SendTTSMessages); + /// If True, a user may delete messages. + public bool ManageMessages => PermissionsHelper.GetValue(RawValue, PermissionBit.ManageMessages); + /// If True, Discord will auto-embed links sent by this user. + public bool EmbedLinks => PermissionsHelper.GetValue(RawValue, PermissionBit.EmbedLinks); + /// If True, a user may send files. + public bool AttachFiles => PermissionsHelper.GetValue(RawValue, PermissionBit.AttachFiles); + /// If True, a user may read previous messages. + public bool ReadMessageHistory => PermissionsHelper.GetValue(RawValue, PermissionBit.ReadMessageHistory); + /// If True, a user may mention @everyone. + public bool MentionEveryone => PermissionsHelper.GetValue(RawValue, PermissionBit.MentionEveryone); + + /// If True, a user may connect to a voice channel. + public bool Connect => PermissionsHelper.GetValue(RawValue, PermissionBit.Connect); + /// If True, a user may speak in a voice channel. + public bool Speak => PermissionsHelper.GetValue(RawValue, PermissionBit.Speak); + /// If True, a user may mute users. + public bool MuteMembers => PermissionsHelper.GetValue(RawValue, PermissionBit.MuteMembers); + /// If True, a user may deafen users. + public bool DeafenMembers => PermissionsHelper.GetValue(RawValue, PermissionBit.DeafenMembers); + /// If True, a user may move other users between voice channels. + public bool MoveMembers => PermissionsHelper.GetValue(RawValue, PermissionBit.MoveMembers); + /// If True, a user may use voice activation rather than push-to-talk. + public bool UseVoiceActivation => PermissionsHelper.GetValue(RawValue, PermissionBit.UseVoiceActivation); + + /// Creates a new GuildPermissions with the provided packed value. + public GuildPermissions(uint rawValue) { RawValue = rawValue; } + + private GuildPermissions(uint initialValue, bool? createInstantInvite = null, bool? manageRoles = null, + bool? kickMembers = null, bool? banMembers = null, bool? manageChannel = null, bool? manageGuild = null, + bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, bool? manageMessages = null, + bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, bool? mentionEveryone = null, + bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, + bool? moveMembers = null, bool? useVoiceActivation = null) + { + uint value = initialValue; + + PermissionsHelper.SetValue(ref value, createInstantInvite, PermissionBit.CreateInstantInvite); + PermissionsHelper.SetValue(ref value, banMembers, PermissionBit.BanMembers); + PermissionsHelper.SetValue(ref value, kickMembers, PermissionBit.KickMembers); + PermissionsHelper.SetValue(ref value, manageRoles, PermissionBit.ManageRolesOrPermissions); + PermissionsHelper.SetValue(ref value, manageChannel, PermissionBit.ManageChannel); + PermissionsHelper.SetValue(ref value, manageGuild, PermissionBit.ManageGuild); + PermissionsHelper.SetValue(ref value, readMessages, PermissionBit.ReadMessages); + PermissionsHelper.SetValue(ref value, sendMessages, PermissionBit.SendMessages); + PermissionsHelper.SetValue(ref value, sendTTSMessages, PermissionBit.SendTTSMessages); + PermissionsHelper.SetValue(ref value, manageMessages, PermissionBit.ManageMessages); + PermissionsHelper.SetValue(ref value, embedLinks, PermissionBit.EmbedLinks); + PermissionsHelper.SetValue(ref value, attachFiles, PermissionBit.AttachFiles); + PermissionsHelper.SetValue(ref value, readMessageHistory, PermissionBit.ReadMessageHistory); + PermissionsHelper.SetValue(ref value, mentionEveryone, PermissionBit.MentionEveryone); + PermissionsHelper.SetValue(ref value, connect, PermissionBit.Connect); + PermissionsHelper.SetValue(ref value, speak, PermissionBit.Speak); + PermissionsHelper.SetValue(ref value, muteMembers, PermissionBit.MuteMembers); + PermissionsHelper.SetValue(ref value, deafenMembers, PermissionBit.DeafenMembers); + PermissionsHelper.SetValue(ref value, moveMembers, PermissionBit.MoveMembers); + PermissionsHelper.SetValue(ref value, useVoiceActivation, PermissionBit.UseVoiceActivation); + + RawValue = value; + } + + /// Creates a new GuildPermissions with the provided permissions. + public GuildPermissions(bool createInstantInvite = false, bool manageRoles = false, + bool kickMembers = false, bool banMembers = false, bool manageChannel = false, bool manageGuild = false, + bool readMessages = false, bool sendMessages = false, bool sendTTSMessages = false, bool manageMessages = false, + bool embedLinks = false, bool attachFiles = false, bool readMessageHistory = false, bool mentionEveryone = false, + bool connect = false, bool speak = false, bool muteMembers = false, bool deafenMembers = false, + bool moveMembers = false, bool useVoiceActivation = false) + : this(0, createInstantInvite, manageRoles, kickMembers, banMembers, manageChannel, manageGuild, readMessages, + sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles, mentionEveryone, connect, speak, muteMembers, deafenMembers, + moveMembers, useVoiceActivation) { } + + /// Creates a new GuildPermissions from this one, changing the provided non-null permissions. + public GuildPermissions Modify(bool? createInstantInvite = null, bool? manageRoles = null, + bool? kickMembers = null, bool? banMembers = null, bool? manageChannel = null, bool? manageGuild = null, + bool? readMessages = null, bool? sendMessages = null, bool? sendTTSMessages = null, bool? manageMessages = null, + bool? embedLinks = null, bool? attachFiles = null, bool? readMessageHistory = null, bool? mentionEveryone = null, + bool? connect = null, bool? speak = null, bool? muteMembers = null, bool? deafenMembers = null, + bool? moveMembers = null, bool? useVoiceActivation = null) + => new GuildPermissions(RawValue, createInstantInvite, manageRoles, kickMembers, banMembers, manageChannel, manageGuild, readMessages, + sendMessages, sendTTSMessages, manageMessages, embedLinks, attachFiles, mentionEveryone, connect, speak, muteMembers, deafenMembers, + moveMembers, useVoiceActivation); + + /// + public override string ToString() => Convert.ToString(RawValue, 2); + } +} diff --git a/src/Discord.Net/Entities/Permissions/Overwrite.cs b/src/Discord.Net/Entities/Permissions/Overwrite.cs new file mode 100644 index 000000000..d964e7068 --- /dev/null +++ b/src/Discord.Net/Entities/Permissions/Overwrite.cs @@ -0,0 +1,22 @@ +using Model = Discord.API.Overwrite; + +namespace Discord +{ + public struct Overwrite + { + /// Gets the unique identifier for the object this overwrite is targeting. + public ulong TargetId { get; } + /// Gets the type of object this overwrite is targeting. + public PermissionTarget TargetType { get; } + /// Gets the permissions associated with this overwrite entry. + public OverwritePermissions Permissions { get; } + + /// Creates a new Overwrite with provided target information and modified permissions. + internal Overwrite(Model model) + { + TargetId = model.TargetId; + TargetType = model.TargetType; + Permissions = new OverwritePermissions(model.Allow, model.Deny); + } + } +} diff --git a/src/Discord.Net/Entities/Permissions/OverwritePermissions.cs b/src/Discord.Net/Entities/Permissions/OverwritePermissions.cs new file mode 100644 index 000000000..0a569c264 --- /dev/null +++ b/src/Discord.Net/Entities/Permissions/OverwritePermissions.cs @@ -0,0 +1,113 @@ +using System; + +namespace Discord +{ + public struct OverwritePermissions + { + /// Gets a blank OverwritePermissions that inherits all permissions. + public static OverwritePermissions InheritAll { get; } = new OverwritePermissions(); + /// Gets a OverwritePermissions that grants all permissions for a given channelType. + public static OverwritePermissions AllowAll(ChannelType channelType) + => new OverwritePermissions(ChannelPermissions.All(channelType).RawValue, 0); + /// Gets a OverwritePermissions that denies all permissions for a given channelType. + public static OverwritePermissions DenyAll(ChannelType channelType) + => new OverwritePermissions(0, ChannelPermissions.All(channelType).RawValue); + + /// Gets a packed value representing all the allowed permissions in this OverwritePermissions. + public uint AllowValue { get; } + /// Gets a packed value representing all the denied permissions in this OverwritePermissions. + public uint DenyValue { get; } + + /// If True, a user may create invites. + public PermValue CreateInstantInvite => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBit.CreateInstantInvite); + /// If True, a user may adjust permissions. This also implictly grants all other permissions. + public PermValue ManagePermissions => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBit.ManageRolesOrPermissions); + /// If True, a user may create, delete and modify this channel. + public PermValue ManageChannel => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBit.ManageChannel); + /// If True, a user may join channels. + public PermValue ReadMessages => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBit.ReadMessages); + /// If True, a user may send messages. + public PermValue SendMessages => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBit.SendMessages); + /// If True, a user may send text-to-speech messages. + public PermValue SendTTSMessages => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBit.SendTTSMessages); + /// If True, a user may delete messages. + public PermValue ManageMessages => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBit.ManageMessages); + /// If True, Discord will auto-embed links sent by this user. + public PermValue EmbedLinks => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBit.EmbedLinks); + /// If True, a user may send files. + public PermValue AttachFiles => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBit.AttachFiles); + /// If True, a user may read previous messages. + public PermValue ReadMessageHistory => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBit.ReadMessageHistory); + /// If True, a user may mention @everyone. + public PermValue MentionEveryone => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBit.MentionEveryone); + + /// If True, a user may connect to a voice channel. + public PermValue Connect => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBit.Connect); + /// If True, a user may speak in a voice channel. + public PermValue Speak => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBit.Speak); + /// If True, a user may mute users. + public PermValue MuteMembers => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBit.MuteMembers); + /// If True, a user may deafen users. + public PermValue DeafenMembers => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBit.DeafenMembers); + /// If True, a user may move other users between voice channels. + public PermValue MoveMembers => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBit.MoveMembers); + /// If True, a user may use voice activation rather than push-to-talk. + public PermValue UseVoiceActivation => PermissionsHelper.GetValue(AllowValue, DenyValue, PermissionBit.UseVoiceActivation); + + /// Creates a new OverwritePermissions with the provided allow and deny packed values. + public OverwritePermissions(uint allowValue, uint denyValue) + { + AllowValue = allowValue; + DenyValue = denyValue; + } + + private OverwritePermissions(uint allowValue, uint denyValue, PermValue? createInstantInvite = null, PermValue? managePermissions = null, + PermValue? manageChannel = null, PermValue? readMessages = null, PermValue? sendMessages = null, PermValue? sendTTSMessages = null, + PermValue? manageMessages = null, PermValue? embedLinks = null, PermValue? attachFiles = null, PermValue? readMessageHistory = null, + PermValue? mentionEveryone = null, PermValue? connect = null, PermValue? speak = null, PermValue? muteMembers = null, PermValue? deafenMembers = null, + PermValue? moveMembers = null, PermValue? useVoiceActivation = null) + { + PermissionsHelper.SetValue(ref allowValue, ref denyValue, createInstantInvite, PermissionBit.CreateInstantInvite); + PermissionsHelper.SetValue(ref allowValue, ref denyValue, managePermissions, PermissionBit.ManageRolesOrPermissions); + PermissionsHelper.SetValue(ref allowValue, ref denyValue, manageChannel, PermissionBit.ManageChannel); + PermissionsHelper.SetValue(ref allowValue, ref denyValue, readMessages, PermissionBit.ReadMessages); + PermissionsHelper.SetValue(ref allowValue, ref denyValue, sendMessages, PermissionBit.SendMessages); + PermissionsHelper.SetValue(ref allowValue, ref denyValue, sendTTSMessages, PermissionBit.SendTTSMessages); + PermissionsHelper.SetValue(ref allowValue, ref denyValue, manageMessages, PermissionBit.ManageMessages); + PermissionsHelper.SetValue(ref allowValue, ref denyValue, embedLinks, PermissionBit.EmbedLinks); + PermissionsHelper.SetValue(ref allowValue, ref denyValue, attachFiles, PermissionBit.AttachFiles); + PermissionsHelper.SetValue(ref allowValue, ref denyValue, readMessageHistory, PermissionBit.ReadMessageHistory); + PermissionsHelper.SetValue(ref allowValue, ref denyValue, mentionEveryone, PermissionBit.MentionEveryone); + PermissionsHelper.SetValue(ref allowValue, ref denyValue, connect, PermissionBit.Connect); + PermissionsHelper.SetValue(ref allowValue, ref denyValue, speak, PermissionBit.Speak); + PermissionsHelper.SetValue(ref allowValue, ref denyValue, muteMembers, PermissionBit.MuteMembers); + PermissionsHelper.SetValue(ref allowValue, ref denyValue, deafenMembers, PermissionBit.DeafenMembers); + PermissionsHelper.SetValue(ref allowValue, ref denyValue, moveMembers, PermissionBit.MoveMembers); + PermissionsHelper.SetValue(ref allowValue, ref denyValue, useVoiceActivation, PermissionBit.UseVoiceActivation); + + AllowValue = allowValue; + DenyValue = denyValue; + } + + /// Creates a new ChannelPermissions with the provided permissions. + public OverwritePermissions(PermValue createInstantInvite = PermValue.Inherit, PermValue managePermissions = PermValue.Inherit, + PermValue manageChannel = PermValue.Inherit, PermValue readMessages = PermValue.Inherit, PermValue sendMessages = PermValue.Inherit, PermValue sendTTSMessages = PermValue.Inherit, + PermValue manageMessages = PermValue.Inherit, PermValue embedLinks = PermValue.Inherit, PermValue attachFiles = PermValue.Inherit, PermValue readMessageHistory = PermValue.Inherit, + PermValue mentionEveryone = PermValue.Inherit, PermValue connect = PermValue.Inherit, PermValue speak = PermValue.Inherit, PermValue muteMembers = PermValue.Inherit, PermValue deafenMembers = PermValue.Inherit, + PermValue moveMembers = PermValue.Inherit, PermValue useVoiceActivation = PermValue.Inherit) + : this(0, 0, createInstantInvite, managePermissions, manageChannel, readMessages, sendMessages, sendTTSMessages, + manageMessages, embedLinks, attachFiles, readMessageHistory, mentionEveryone, connect, speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation) { } + + /// Creates a new OverwritePermissions from this one, changing the provided non-null permissions. + public OverwritePermissions Modify(PermValue? createInstantInvite = null, PermValue? managePermissions = null, + PermValue? manageChannel = null, PermValue? readMessages = null, PermValue? sendMessages = null, PermValue? sendTTSMessages = null, + PermValue? manageMessages = null, PermValue? embedLinks = null, PermValue? attachFiles = null, PermValue? readMessageHistory = null, + PermValue? mentionEveryone = null, PermValue? connect = null, PermValue? speak = null, PermValue? muteMembers = null, PermValue? deafenMembers = null, + PermValue? moveMembers = null, PermValue? useVoiceActivation = null) + => new OverwritePermissions(AllowValue, DenyValue, createInstantInvite, managePermissions, manageChannel, readMessages, sendMessages, sendTTSMessages, + manageMessages, embedLinks, attachFiles, readMessageHistory, mentionEveryone, connect, speak, muteMembers, deafenMembers, moveMembers, useVoiceActivation); + + /// + public override string ToString() => $"Allow: {Convert.ToString(AllowValue, 2)}, Deny: {Convert.ToString(DenyValue, 2)}"; + } +} diff --git a/src/Discord.Net/Entities/Presences/GuildPresence.cs b/src/Discord.Net/Entities/Presences/GuildPresence.cs new file mode 100644 index 000000000..142e042e7 --- /dev/null +++ b/src/Discord.Net/Entities/Presences/GuildPresence.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Model = Discord.API.MemberPresence; + +namespace Discord +{ + public class GuildPresence : Presence + { + public Guild Guild { get; } + public ulong UserId { get; } + + /// + public IReadOnlyList Roles { get; private set; } + + internal GuildPresence(ulong userId, Guild guild) + { + UserId = userId; + Guild = guild; + } + internal override void Update(Model model) + { + base.Update(model); + Roles = model.Roles.Select(x => Guild.GetRole(x)).ToImmutableArray(); + } + + public bool HasRole(Role role) => false; + + //TODO: Unsure about these + /*public Task AddRoles(params Role[] roles) => xxx; + public Task RemoveRoles(params Role[] roles) => xxx;*/ + } +} \ No newline at end of file diff --git a/src/Discord.Net/Entities/Presences/Presence.cs b/src/Discord.Net/Entities/Presences/Presence.cs new file mode 100644 index 000000000..671f60966 --- /dev/null +++ b/src/Discord.Net/Entities/Presences/Presence.cs @@ -0,0 +1,18 @@ +using Model = Discord.API.MemberPresence; + +namespace Discord +{ + public class Presence + { + public string CurrentGame { get; private set; } + public UserStatus Status { get; private set; } + + internal Presence() { } + + internal virtual void Update(Model model) + { + CurrentGame = model.Game?.Name; + Status = model.Status; + } + } +} diff --git a/src/Discord.Net/Entities/Presences/VoiceState.cs b/src/Discord.Net/Entities/Presences/VoiceState.cs new file mode 100644 index 000000000..f6b56d7b6 --- /dev/null +++ b/src/Discord.Net/Entities/Presences/VoiceState.cs @@ -0,0 +1,66 @@ +using Discord.API.Rest; +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.MemberVoiceState; + +namespace Discord +{ + public class VoiceState + { + [Flags] + private enum VoiceStates : byte + { + None = 0x0, + Muted = 0x01, + Deafened = 0x02, + Suppressed = 0x4, + SelfMuted = 0x10, + SelfDeafened = 0x20, + } + + private VoiceStates _voiceStates; + + public Guild Guild { get; } + public ulong UserId { get; } + + /// Gets this user's current voice channel. + public VoiceChannel VoiceChannel { get; internal set; } + + /// Returns true if this user has marked themselves as muted. + public bool IsSelfMuted => (_voiceStates & VoiceStates.SelfMuted) != 0; + /// Returns true if this user has marked themselves as deafened. + public bool IsSelfDeafened => (_voiceStates & VoiceStates.SelfDeafened) != 0; + /// Returns true if the guild is blocking audio from this user. + public bool IsMuted => (_voiceStates & VoiceStates.Muted) != 0; + /// Returns true if the guild is blocking audio to this user. + public bool IsDeafened => (_voiceStates & VoiceStates.Deafened) != 0; + /// Returns true if the guild is temporarily blocking audio to/from this user. + public bool IsSuppressed => (_voiceStates & VoiceStates.Suppressed) != 0; + + internal VoiceState(ulong userId, Guild guild) + { + UserId = userId; + Guild = guild; + } + + internal void Update(Model model) + { + if (model.IsMuted == true) + _voiceStates |= VoiceStates.Muted; + else if (model.IsMuted == false) + _voiceStates &= ~VoiceStates.Muted; + + if (model.IsDeafened == true) + _voiceStates |= VoiceStates.Deafened; + else if (model.IsDeafened == false) + _voiceStates &= ~VoiceStates.Deafened; + + if (model.IsSuppressed == true) + _voiceStates |= VoiceStates.Suppressed; + else if (model.IsSuppressed == false) + _voiceStates &= ~VoiceStates.Suppressed; + } + } +} \ No newline at end of file diff --git a/src/Discord.Net/Entities/PrivateChannel.cs b/src/Discord.Net/Entities/PrivateChannel.cs deleted file mode 100644 index b3f3f1faf..000000000 --- a/src/Discord.Net/Entities/PrivateChannel.cs +++ /dev/null @@ -1,66 +0,0 @@ -using APIChannel = Discord.API.Client.Channel; -using Discord.API.Client.Rest; -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading.Tasks; - -namespace Discord -{ - public class PrivateChannel : Channel, IPrivateChannel, ITextChannel - { - private readonly static Action _cloner = DynamicIL.CreateCopyMethod(); - - private readonly MessageManager _messages; - - /// Gets the target user, if this is a private chat. - public User Recipient { get; } - - public override DiscordClient Client => Recipient.Client; - - public override ChannelType Type => ChannelType.Private; - public override User CurrentUser => Client.PrivateUser; - public override IEnumerable Users => new User[] { Client.PrivateUser, Recipient }; - - internal override MessageManager MessageManager => _messages; - internal override PermissionManager PermissionManager => null; - - internal PrivateChannel(ulong id, User recipient, APIChannel model) - : this(id, recipient) - { - _messages = new MessageManager(this, Client.Config.MessageCacheSize); - Update(model); - } - private PrivateChannel(ulong id, User recipient) - :base(id) - { - Recipient = recipient; - } - - internal override User GetUser(ulong id) - { - if (id == Recipient.Id) return Recipient; - else if (id == Client.CurrentUser.Id) return Client.PrivateUser; - else return null; - } - - public Message GetMessage(ulong id) => _messages.Get(id); - public Task DownloadMessages(int limit = 100, ulong? relativeMessageId = null, Relative relativeDir = Relative.Before) - => _messages.Download(limit, relativeMessageId, relativeDir); - - public Task SendMessage(string text, bool isTTS = false) => _messages.Send(text, isTTS); - public Task SendFile(string filePath) => _messages.SendFile(filePath); - public Task SendFile(string filename, Stream stream) => _messages.SendFile(filename, stream); - public Task SendIsTyping() => Client.ClientAPI.Send(new SendIsTypingRequest(Id)); - - public override string ToString() => $"@{Recipient.Name}"; - - internal override void Update(APIChannel model) { } - internal override Channel Clone() - { - var result = new PrivateChannel(Id, Recipient); - _cloner(this, result); - return result; - } - } -} diff --git a/src/Discord.Net/Entities/Profile.cs b/src/Discord.Net/Entities/Profile.cs deleted file mode 100644 index 7029e4c41..000000000 --- a/src/Discord.Net/Entities/Profile.cs +++ /dev/null @@ -1,92 +0,0 @@ -using Discord.API.Client; -using Discord.API.Client.Rest; -using System; -using System.IO; -using System.Threading.Tasks; -using APIUser = Discord.API.Client.User; - -namespace Discord -{ - public class Profile - { - private readonly static Action _cloner = DynamicIL.CreateCopyMethod(); - - public DiscordClient Client { get; } - - /// Gets the unique identifier for this user. - public ulong Id { get; } - /// Gets the global name of this user. - public string Name => Client.PrivateUser.Name; - /// Gets the unique identifier for this user's current avatar. - public string AvatarId => Client.PrivateUser.AvatarId; - /// Gets the URL to this user's current avatar. - public string AvatarUrl => Client.PrivateUser.AvatarUrl; - /// Gets an id uniquely identifying from others with the same name. - public ushort Discriminator => Client.PrivateUser.Discriminator; - /// Gets the name of the game this user is currently playing. - public string CurrentGame => Client.PrivateUser.CurrentGame; - /// Gets the current status for this user. - public UserStatus Status => Client.PrivateUser.Status; - /// Returns the string used to mention this user. - public string Mention => $"<@{Id}>"; - - /// Gets the email for this user. - public string Email { get; private set; } - /// Gets if the email for this user has been verified. - public bool? IsVerified { get; private set; } - - internal Profile(UserReference model, DiscordClient client) - : this(model.Id, client) - { - } - private Profile(ulong id, DiscordClient client) - { - Id = id; - Client = client; - } - - internal void Update(APIUser model) - { - Email = model.Email; - IsVerified = model.IsVerified; - } - - public async Task Edit(string currentPassword = "", - string username = null, string email = null, string password = null, - Stream avatar = null, ImageType avatarType = ImageType.Png) - { - if (currentPassword == null) throw new ArgumentNullException(nameof(currentPassword)); - - var request = new UpdateProfileRequest() - { - CurrentPassword = currentPassword, - Email = email ?? Email, - Password = password, - Username = username ?? Client.PrivateUser.Name, - AvatarBase64 = avatar.Base64(avatarType, Client.PrivateUser.AvatarId) - }; - - await Client.ClientAPI.Send(request).ConfigureAwait(false); - - if (password != null) - { - var loginRequest = new LoginRequest() - { - Email = Email, - Password = password - }; - var loginResponse = await Client.ClientAPI.Send(loginRequest).ConfigureAwait(false); - Client.ClientAPI.Token = loginResponse.Token; - } - } - - internal Profile Clone() - { - var result = new Profile(Id, Client); - _cloner(this, result); - return result; - } - - public override string ToString() => Name; - } -} diff --git a/src/Discord.Net/Entities/PublicChannel.cs b/src/Discord.Net/Entities/PublicChannel.cs deleted file mode 100644 index a1d5c4f41..000000000 --- a/src/Discord.Net/Entities/PublicChannel.cs +++ /dev/null @@ -1,109 +0,0 @@ -using APIChannel = Discord.API.Client.Channel; -using Discord.API.Client.Rest; -using Discord.Net; -using System; -using System.Collections.Generic; -using System.Net; -using System.Threading.Tasks; - -namespace Discord -{ - /// A public Discord channel - public abstract class PublicChannel : Channel, IModel, IMentionable - { - internal readonly PermissionManager _permissions; - - /// Gets the server owning this channel. - public Server Server { get; } - - /// Gets or sets the name of this channel. - public string Name { get; set; } - /// Getsor sets the position of this channel relative to other channels of the same type in this server. - public int Position { get; set; } - - /// Gets the DiscordClient that created this model. - public override DiscordClient Client => Server.Client; - public override User CurrentUser => Server.CurrentUser; - /// Gets the string used to mention this channel. - public string Mention => $"<#{Id}>"; - /// Gets a collection of all custom permissions used for this channel. - public IEnumerable PermissionRules => _permissions.Rules; - - internal PublicChannel(APIChannel model, Server server) - : this(model.Id, server) - { - _permissions = new PermissionManager(this, model, server.Client.Config.UsePermissionsCache ? (int)(server.UserCount * 1.05) : -1); - Update(model); - } - protected PublicChannel(ulong id, Server server) - : base(id) - { - Server = server; - } - - internal override void Update(APIChannel model) - { - if (model.Name != null) Name = model.Name; - if (model.Position != null) Position = model.Position.Value; - - if (model.PermissionOverwrites != null) - _permissions.Update(model); - } - - public async Task Delete() - { - try { await Client.ClientAPI.Send(new DeleteChannelRequest(Id)).ConfigureAwait(false); } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } - } - public abstract Task Save(); - - internal override User GetUser(ulong id) => Server.GetUser(id); - - public ChannelTriStatePermissions? GetPermissionsRule(User user) => _permissions.GetOverwrite(user); - public ChannelTriStatePermissions? GetPermissionsRule(Role role) => _permissions.GetOverwrite(role); - public Task AddOrUpdatePermissionsRule(User user, ChannelTriStatePermissions permissions) => _permissions.AddOrUpdateOverwrite(user, permissions); - public Task AddOrUpdatePermissionsRule(Role role, ChannelTriStatePermissions permissions) => _permissions.AddOrUpdateOverwrite(role, permissions); - public Task RemovePermissionsRule(User user) => _permissions.RemoveOverwrite(user); - public async Task RemovePermissionsRule(Role role) - { - if (role == null) throw new ArgumentNullException(nameof(role)); - try { await Client.ClientAPI.Send(new RemoveChannelPermissionsRequest(Id, role.Id)).ConfigureAwait(false); } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } - } - - internal ChannelPermissions GetPermissions(User user) => _permissions.GetPermissions(user); - internal void UpdatePermissions() => _permissions.UpdatePermissions(); - internal void UpdatePermissions(User user) => _permissions.UpdatePermissions(user); - internal bool ResolvePermissions(User user, ref ChannelPermissions permissions) => _permissions.ResolvePermissions(user, ref permissions); - - internal override PermissionManager PermissionManager => null; - - /// Creates a new invite to this channel. - /// Time (in seconds) until the invite expires. Set to null to never expire. - /// The max amount of times this invite may be used. Set to null to have unlimited uses. - /// If true, a user accepting this invite will be kicked from the server after closing their client. - /// If true, creates a human-readable link. Not supported if maxAge is set to null. - public async Task CreateInvite(int? maxAge = 1800, int? maxUses = null, bool tempMembership = false, bool withXkcd = false) - { - if (maxAge < 0) throw new ArgumentOutOfRangeException(nameof(maxAge)); - if (maxUses < 0) throw new ArgumentOutOfRangeException(nameof(maxUses)); - - var request = new CreateInviteRequest(Id) - { - MaxAge = maxAge ?? 0, - MaxUses = maxUses ?? 0, - IsTemporary = tempMembership, - WithXkcdPass = withXkcd - }; - - var response = await Client.ClientAPI.Send(request).ConfigureAwait(false); - var invite = new Invite(response, Client); - return invite; - } - - internal void AddUser(User user) => _permissions.AddUser(user); - internal void RemoveUser(ulong id) => _permissions.RemoveUser(id); - - public override string ToString() => $"{Server}/{Name ?? Id.ToIdString()}"; - } -} diff --git a/src/Discord.Net/Entities/Region.cs b/src/Discord.Net/Entities/Region.cs deleted file mode 100644 index cf144a223..000000000 --- a/src/Discord.Net/Entities/Region.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Discord -{ - public class Region - { - public string Id { get; } - public string Name { get; } - public string Hostname { get; } - public int Port { get; } - public bool Vip { get; } - - internal Region(string id, string name, string hostname, int port, bool vip) - { - Id = id; - Name = name; - Hostname = hostname; - Port = port; - Vip = vip; - } - - public override string ToString() => Name; - } -} diff --git a/src/Discord.Net/Entities/Role.cs b/src/Discord.Net/Entities/Role.cs index 2f946f0b8..1e138c911 100644 --- a/src/Discord.Net/Entities/Role.cs +++ b/src/Discord.Net/Entities/Role.cs @@ -1,134 +1,85 @@ -using Discord.API.Client.Rest; +using Discord.API.Rest; using Discord.Net; using System; using System.Collections.Generic; -using System.Linq; using System.Net; using System.Threading.Tasks; -using APIRole = Discord.API.Client.Role; +using Model = Discord.API.Role; namespace Discord { - public class Role : IMentionable + public class Role : IEntity, IMentionable { - private readonly static Action _cloner = DynamicIL.CreateCopyMethod(); - - public DiscordClient Client => Server.Client; - - /// Gets the unique identifier for this role. + /// public ulong Id { get; } - /// Gets the server this role is a member of. - public Server Server { get; } + /// Returns the guild this role belongs to. + public Guild Guild { get; } /// Gets the name of this role. public string Name { get; private set; } - /// If true, this role is displayed isolated from other users. - public bool IsHoisted { get; private set; } - /// Gets the position of this channel relative to other channels in this server. + /// Returns true if members of this role are isolated in the user list. + public bool IsHoisted { get; private set; } + /// Gets the position of this role relative to other roles in this guild. public int Position { get; private set; } - /// Gets whether this role is managed by server (e.g. for Twitch integration) + /// Returns true if this role is managed by the Discord server (e.g. for Twitch integration) public bool IsManaged { get; private set; } - /// Gets the the permissions given to this role. - public ServerPermissions Permissions { get; private set; } - /// Gets the color of this role. + /// Gets the permissions given to all members of this role. + public GuildPermissions Permissions { get; private set; } + /// Gets the color assigned to members of this role. public Color Color { get; private set; } - - /// Gets true if this is the role representing all users in a server. - public bool IsEveryone => Id == Server.Id; - /// Gets a list of all members in this role. - public IEnumerable Members => IsEveryone ? Server.Users : Server.Users.Where(x => x.HasRole(this)); + /// + public DiscordClient Discord => Guild.Discord; + /// Returns true if this is the role representing all users in a server. + public bool IsEveryone => Id == Guild.Id; /// Gets the string used to mention this role. public string Mention => IsEveryone ? "@everyone" : ""; + /// Gets a collection of all members in this role. + public IEnumerable Members { get { throw new NotImplementedException(); } } //TODO: Implement - internal Role(ulong id, Server server) - { + internal Role(ulong id, Guild guild) + { Id = id; - Server = server; - - Permissions = new ServerPermissions(0); - Color = new Color(0); - } + Guild = guild; + } - internal void Update(APIRole model, bool updatePermissions) - { - if (model.Name != null) - Name = model.Name; - if (model.Hoist != null) - IsHoisted = model.Hoist.Value; - if (model.Managed != null) - IsManaged = model.Managed.Value; - if (model.Position != null && !IsEveryone) - Position = model.Position.Value; - if (model.Color != null) - Color = new Color(model.Color.Value); - if (model.Permissions != null) - { - Permissions = new ServerPermissions(model.Permissions.Value); - if (updatePermissions) //Dont update these during READY - { - foreach (var member in Members) - Server.UpdatePermissions(member); - } - } - } - - public async Task Edit(string name = null, ServerPermissions? permissions = null, Color color = null, bool? isHoisted = null, int? position = null) + internal void Update(Model model) { - var updateRequest = new UpdateRoleRequest(Server.Id, Id) - { - Name = name ?? Name, - Permissions = (permissions ?? Permissions).RawValue, - Color = (color ?? Color).RawValue, - IsHoisted = isHoisted ?? IsHoisted - }; - - var updateResponse = await Client.ClientAPI.Send(updateRequest).ConfigureAwait(false); + Name = model.Name; + IsHoisted = model.Hoist.Value; + IsManaged = model.Managed.Value; + Position = model.Position.Value; + Color = new Color(model.Color.Value); + Permissions = new GuildPermissions(model.Permissions.Value); + } - if (position != null) - { - int oldPos = Position; - int newPos = position.Value; - int minPos; - Role[] roles = Server.Roles.OrderBy(x => x.Position).ToArray(); + /// + public Task Update() { throw new NotSupportedException(); } //TODO: Not supported yet - if (oldPos < newPos) //Moving Down - { - minPos = oldPos; - for (int i = oldPos; i < newPos; i++) - roles[i] = roles[i + 1]; - roles[newPos] = this; - } - else //(oldPos > newPos) Moving Up - { - minPos = newPos; - for (int i = oldPos; i > newPos; i--) - roles[i] = roles[i - 1]; - roles[newPos] = this; - } + /// Modifies the properties of this role. + public async Task Modify(Action func) + { + if (func != null) throw new NullReferenceException(nameof(func)); - var reorderRequest = new ReorderRolesRequest(Server.Id) - { - RoleIds = roles.Skip(minPos).Select(x => x.Id).ToArray(), - StartPos = minPos - }; - await Client.ClientAPI.Send(reorderRequest).ConfigureAwait(false); - } + var req = new ModifyGuildRoleRequest(Guild.Id, Id); + func(req); + await Discord.RestClient.Send(req).ConfigureAwait(false); } + /// Deletes this message. public async Task Delete() { - try { await Client.ClientAPI.Send(new DeleteRoleRequest(Server.Id, Id)).ConfigureAwait(false); } + try { await Discord.RestClient.Send(new DeleteGuildRoleRequest(Guild.Id, Id)).ConfigureAwait(false); } catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } } - internal Role Clone() + internal void UpdatePermissions() { - var result = new Role(Id, Server); - _cloner(this, result); - return result; + foreach (var member in Members) + Guild.UpdatePermissions(member); } - public override string ToString() => $"{Server}/{Name ?? Id.ToString()}"; + /// + public override string ToString() => $"{Guild}/{Name ?? Id.ToString()}"; } } diff --git a/src/Discord.Net/Entities/Server.cs b/src/Discord.Net/Entities/Server.cs deleted file mode 100644 index a5e586126..000000000 --- a/src/Discord.Net/Entities/Server.cs +++ /dev/null @@ -1,511 +0,0 @@ -using APIChannel = Discord.API.Client.Channel; -using APIMember = Discord.API.Client.Member; -using Discord.API.Client; -using Discord.API.Client.Rest; -using Discord.Net; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Threading.Tasks; - -namespace Discord -{ - /// Represents a Discord server (also known as a guild). - 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 class Emoji - { - public string Id { get; } - - public string Name { get; internal set; } - public bool IsManaged { get; internal set; } - public bool RequireColons { get; internal set; } - public IEnumerable Roles { get; internal set; } - - internal Emoji(string id) - { - Id = id; - } - } - private struct Member - { - public readonly User User; - public readonly ServerPermissions Permissions; - public Member(User user, ServerPermissions permissions) - { - User = user; - Permissions = permissions; - } - } - - private ConcurrentDictionary _roles; - private ConcurrentDictionary _users; - private ConcurrentDictionary _channels; - private ulong _ownerId; - private ulong? _afkChannelId; - private int _userCount; - - public DiscordClient Client { get; } - - /// Gets the unique identifier for this server. - public ulong Id { get; } - - /// Gets the name of this server. - public string Name { get; set; } - /// Gets the voice region for this server. - public Region Region { get; set; } - /// Gets the AFK voice channel for this server. - public VoiceChannel AFKChannel { get; set; } - /// Gets the amount of time (in seconds) a user must be inactive for until they are automatically moved to the AFK voice channel, if one is set. - public int AFKTimeout { get; set; } - - /// Gets the date and time you joined this server. - public DateTime JoinedAt { get; private set; } - /// Gets the the role representing all users in a server. - public Role EveryoneRole { get; private set; } - /// Gets all extra features added to this server. - public IEnumerable Features { get; private set; } - /// Gets all custom emojis on this server. - public IEnumerable CustomEmojis { get; private set; } - /// Gets the unique identifier for this user's current avatar. - public string IconId { get; private set; } - /// Gets the unique identifier for this server's custom splash image. - public string SplashId { get; private set; } - - /// Gets the user that created this server. - public User Owner => GetUser(_ownerId); - /// Gets the default channel for this server. - public TextChannel DefaultChannel => _channels[Id] as TextChannel; - /// Gets the current user in this server. - public User CurrentUser => GetUser(Client.CurrentUser.Id); - /// 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. - public IEnumerable AllChannels => _channels.Select(x => x.Value); - /// Gets a collection of text channels in this server. - public IEnumerable TextChannels => _channels.Where(x => x.Value.IsText).Select(x => x.Value as TextChannel); - /// Gets a collection of voice channels in this server. - public IEnumerable VoiceChannels => _channels.Where(x => x.Value.IsVoice).Select(x => x.Value as VoiceChannel); - /// Gets a collection of all members in this server. - public IEnumerable Users => _users.Select(x => x.Value.User); - /// Gets a collection of all roles in this server. - public IEnumerable Roles => _roles.Select(x => x.Value); - - /// Gets the number of channels in this server. - public int ChannelCount => _channels.Count; - /// Gets the number of users downloaded for this server so far. - internal int CurrentUserCount => _users.Count; - /// Gets the number of users in this server. - public int UserCount => _userCount; - /// Gets the number of roles in this server. - public int RoleCount => _roles.Count; - - internal Server(ulong id, DiscordClient client) - { - Id = id; - Client = client; - } - - internal void Update(Guild model) - { - if (model.Name != null) - Name = model.Name; - if (model.AFKTimeout != null) - AFKTimeout = model.AFKTimeout.Value; - if (model.JoinedAt != null) - JoinedAt = model.JoinedAt.Value; - if (model.OwnerId != null) - _ownerId = model.OwnerId.Value; - if (model.Region != null) - Region = Client.GetRegion(model.Region); - if (model.Icon != null) - IconId = model.Icon; - if (model.Features != null) - Features = model.Features; - if (model.Roles != null) - { - _roles = new ConcurrentDictionary(2, model.Roles.Length); - foreach (var x in model.Roles) - { - var role = AddRole(x.Id); - role.Update(x, false); - } - EveryoneRole = _roles[Id]; - } - if (model.Emojis != null) //Needs Roles - { - CustomEmojis = model.Emojis.Select(x => new Emoji(x.Id) - { - Name = x.Name, - IsManaged = x.IsManaged, - RequireColons = x.RequireColons, - Roles = x.RoleIds.Select(y => GetRole(y)).Where(y => y != null).ToArray() - }).ToArray(); - } - - //Can be null - _afkChannelId = model.AFKChannelId; - SplashId = model.Splash; - } - internal void Update(ExtendedGuild model) - { - Update(model as Guild); - - //Only channels or members should have AddXXX(cachePerms: true), not both - if (model.Channels != null) - { - _channels = new ConcurrentDictionary(2, (int)(model.Channels.Length * 1.05)); - foreach (var subModel in model.Channels) - AddChannel(subModel, false); - } - if (model.MemberCount != null) - { - if (_users == null) - _users = new ConcurrentDictionary(2, (int)(model.MemberCount * 1.05)); - _userCount = model.MemberCount.Value; - } - if (!model.IsLarge) - { - if (model.Members != null) - { - foreach (var subModel in model.Members) - AddUser(subModel, true, false).Update(subModel); - } - if (model.VoiceStates != null) - { - foreach (var subModel in model.VoiceStates) - GetUser(subModel.UserId)?.Update(subModel); - } - if (model.Presences != null) - { - foreach (var subModel in model.Presences) - GetUser(subModel.User.Id)?.Update(subModel); - } - } - } - - /// Edits this server, changing only non-null attributes. - public Task Edit(string name = null, string region = null, Stream icon = null, ImageType iconType = ImageType.Png) - { - var request = new UpdateGuildRequest(Id) - { - Name = name ?? Name, - Region = region ?? Region.Id, - IconBase64 = icon.Base64(iconType, IconId), - AFKChannelId = AFKChannel?.Id, - AFKTimeout = AFKTimeout, - Splash = SplashId - }; - return Client.ClientAPI.Send(request); - } - - /// Leaves this server. This function will fail if you're the owner - use Delete instead. - public async Task Leave() - { - try { await Client.ClientAPI.Send(new LeaveGuildRequest(Id)).ConfigureAwait(false); } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } - } - /// Deletes this server. This function will fail if you're not the owner - use Leave instead. - public async Task Delete() - { - try { await Client.ClientAPI.Send(new DeleteGuildRequest(Id)).ConfigureAwait(false); } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } - } - - #region Bans - public async Task> GetBans() - { - var response = await Client.ClientAPI.Send(new GetBansRequest(Id)).ConfigureAwait(false); - return response.Select(x => - { - var user = new User(x, Client, this); - return user; - }); - } - - public Task Ban(User user, int pruneDays = 0) - { - var request = new AddGuildBanRequest(user.Server.Id, user.Id) - { - PruneDays = pruneDays - }; - return Client.ClientAPI.Send(request); - } - public Task Unban(User user, int pruneDays = 0) - => Unban(user.Id); - public async Task Unban(ulong userId) - { - try { await Client.ClientAPI.Send(new RemoveGuildBanRequest(Id, userId)).ConfigureAwait(false); } - catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } - } - #endregion - - #region Channels - internal PublicChannel AddChannel(APIChannel model, bool cachePerms) - { - PublicChannel channel; - ChannelType type = EnumConverters.ToChannelType(model.Type); - if (type == ChannelType.Voice) - channel = new VoiceChannel(model, this); - else - channel = new TextChannel(model, this); - - if (cachePerms && Client.Config.UsePermissionsCache) - { - foreach (var user in Users) - channel.AddUser(user); - } - Client.AddChannel(channel); - return _channels.GetOrAdd(model.Id, x => channel); - } - internal PublicChannel RemoveChannel(ulong id) - { - PublicChannel channel; - _channels.TryRemove(id, out channel); - return channel; - } - - /// Gets the channel with the provided id and owned by this server, or null if not found. - public PublicChannel GetChannel(ulong id) - { - PublicChannel result; - _channels.TryGetValue(id, out result); - return result; - } - public TextChannel GetTextChannel(ulong id) => GetChannel(id) as TextChannel; - public VoiceChannel GetVoiceChannel(ulong id) => GetChannel(id) as VoiceChannel; - - /// Creates a new channel. - public async Task CreateChannel(string name, ChannelType type) - { - if (name == null) throw new ArgumentNullException(nameof(name)); - if (type != ChannelType.Text && type != ChannelType.Voice) throw new ArgumentException("Invalid channel type", nameof(type)); - - var request = new CreateChannelRequest(Id) { Name = name, Type = type }; - var response = await Client.ClientAPI.Send(request).ConfigureAwait(false); - - var channel = AddChannel(response, true); - channel.Update(response); - return channel; - } - #endregion - - #region Invites - /// Gets all active (non-expired) invites to this server. - public async Task> DownloadInvites() - { - var response = await Client.ClientAPI.Send(new GetInvitesRequest(Id)).ConfigureAwait(false); - return response.Select(x => - { - var invite = new Invite(x, Client); - invite.Update(x); - return invite; - }); - } - - /// Creates a new invite to the default channel of this server. - /// Time (in seconds) until the invite expires. Set to null to never expire. - /// The max amount of times this invite may be used. Set to null to have unlimited uses. - /// If true, a user accepting this invite will be kicked from the server after closing their client. - /// If true, creates a human-readable link. Not supported if maxAge is set to null. - public Task CreateInvite(int? maxAge = 1800, int? maxUses = null, bool tempMembership = false, bool withXkcd = false) - => DefaultChannel.CreateInvite(maxAge, maxUses, tempMembership, withXkcd); - #endregion - - #region Roles - internal Role AddRole(ulong id) - => _roles.GetOrAdd(id, x => new Role(x, this)); - internal Role RemoveRole(ulong id) - { - Role role; - _roles.TryRemove(id, out role); - return role; - } - - /// Gets the role with the provided id and owned by this server, or null if not found. - public Role GetRole(ulong id) - { - Role result; - _roles.TryGetValue(id, out result); - return result; - } - - /// Creates a new role. - public async Task CreateRole(string name, ServerPermissions? permissions = null, Color color = null, bool isHoisted = false) - { - if (name == null) throw new ArgumentNullException(nameof(name)); - - var createRequest = new CreateRoleRequest(Id); - var createResponse = await Client.ClientAPI.Send(createRequest).ConfigureAwait(false); - var role = AddRole(createResponse.Id); - role.Update(createResponse, false); - - var editRequest = new UpdateRoleRequest(role.Server.Id, role.Id) - { - Name = name, - Permissions = (permissions ?? role.Permissions).RawValue, - Color = (color ?? Color.Default).RawValue, - IsHoisted = isHoisted - }; - var editResponse = await Client.ClientAPI.Send(editRequest).ConfigureAwait(false); - role.Update(editResponse, true); - - return role; - } - - /// Reorders the provided roles and optionally places them after a certain role. - public Task ReorderRoles(IEnumerable roles, Role after = null) - { - if (roles == null) throw new ArgumentNullException(nameof(roles)); - - return Client.ClientAPI.Send(new ReorderRolesRequest(Id) - { - RoleIds = roles.Select(x => x.Id).ToArray(), - StartPos = after != null ? after.Position + 1 : roles.Min(x => x.Position) - }); - } - #endregion - - #region Permissions - internal ServerPermissions GetPermissions(User user) - { - Member member; - if (_users.TryGetValue(user.Id, out member)) - return member.Permissions; - else - return ServerPermissions.None; - } - - internal void UpdatePermissions(User user) - { - Member member; - if (_users.TryGetValue(user.Id, out member)) - { - var perms = member.Permissions; - if (UpdatePermissions(member.User, ref perms)) - { - _users[user.Id] = new Member(member.User, perms); - foreach (var channel in _channels) - channel.Value.UpdatePermissions(user); - } - } - } - - private bool UpdatePermissions(User user, ref ServerPermissions permissions) - { - uint newPermissions = 0; - - if (user.Id == _ownerId) - newPermissions = ServerPermissions.All.RawValue; - else - { - foreach (var serverRole in user.Roles) - newPermissions |= serverRole.Permissions.RawValue; - } - - if (newPermissions.HasBit((byte)PermissionBits.ManageRolesOrPermissions)) - newPermissions = ServerPermissions.All.RawValue; - - if (newPermissions != permissions.RawValue) - { - permissions = new ServerPermissions(newPermissions); - return true; - } - return false; - } - #endregion - - #region Users - internal User AddUser(APIMember model, bool cachePerms, bool incrementCount) - { - if (incrementCount) - _userCount++; - - Member member; - if (!_users.TryGetValue(model.User.Id, out member)) //Users can only be added from websocket thread, ignore threadsafety - { - member = new Member(new User(model, Client, this), ServerPermissions.None); - if (model.User.Id == Client.CurrentUser.Id) - { - member.User.CurrentGame = Client.CurrentGame; - member.User.Status = Client.Status; - } - - _users[model.User.Id] = member; - if (cachePerms && Client.Config.UsePermissionsCache) - { - foreach (var channel in _channels) - channel.Value.AddUser(member.User); - } - } - return member.User; - } - internal User RemoveUser(ulong id) - { - _userCount--; - Member member; - if (_users.TryRemove(id, out member)) - { - foreach (var channel in _channels) - channel.Value.RemoveUser(id); - return member.User; - } - return null; - } - - /// Gets the user with the provided id and is a member of this server, or null if not found. - public User GetUser(ulong id) - { - Member result; - if (_users.TryGetValue(id, out result)) - return result.User; - else - return null; - } - /// Gets the user with the provided username and discriminator, that is a member of this server, or null if not found. - public User GetUser(string name, ushort discriminator) - { - if (name == null) throw new ArgumentNullException(nameof(name)); - - return _users.Select(x => x.Value.User).Where(x => x.Discriminator == discriminator && x.Name == name).SingleOrDefault(); - } - - /// Kicks all users with an inactivity greater or equal to the provided number of days. - /// If true, no pruning will actually be done but instead return the number of users that would be pruned. - public async Task PruneUsers(int days = 30, bool simulate = false) - { - if (days <= 0) throw new ArgumentOutOfRangeException(nameof(days)); - - var request = new PruneMembersRequest(Id) - { - Days = days, - IsSimulation = simulate - }; - var response = await Client.ClientAPI.Send(request).ConfigureAwait(false); - return response.Pruned; - } - #endregion - - internal Server Clone() - { - var result = new Server(Id, Client); - _cloner(this, result); - return result; - } - - public override string ToString() => Name ?? Id.ToIdString(); - } -} diff --git a/src/Discord.Net/Entities/TextChannel.cs b/src/Discord.Net/Entities/TextChannel.cs deleted file mode 100644 index 8ee97aad1..000000000 --- a/src/Discord.Net/Entities/TextChannel.cs +++ /dev/null @@ -1,88 +0,0 @@ -using Discord.API.Client.Rest; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using APIChannel = Discord.API.Client.Channel; - -namespace Discord -{ - public class TextChannel : PublicChannel, IPublicChannel, ITextChannel - { - private readonly static Action _cloner = DynamicIL.CreateCopyMethod(); - - private readonly MessageManager _messages; - - /// Gets or sets the topic of this channel. - public string Topic { get; set; } - - public override ChannelType Type => ChannelType.Text; - /// Gets a collection of all messages the client has seen posted in this channel. This collection does not guarantee any ordering. - public IEnumerable Messages => _messages != null ? _messages : Enumerable.Empty(); - /// Gets a collection of all users with read access to this channel. - public override IEnumerable Users - { - get - { - if (Client.Config.UsePermissionsCache) - return _permissions.Users.Where(x => x.Permissions.ReadMessages == true).Select(x => x.User); - else - { - ChannelPermissions perms = new ChannelPermissions(); - return Server.Users.Where(x => - { - _permissions.ResolvePermissions(x, ref perms); - return perms.ReadMessages == true; - }); - } - } - } - - internal override MessageManager MessageManager => _messages; - - internal TextChannel(APIChannel model, Server server) - : base(model, server) - { - if (Client.Config.MessageCacheSize > 0) - _messages = new MessageManager(this, (int)(Client.Config.MessageCacheSize * 1.05)); - } - private TextChannel(ulong id, Server server) - : base(id, server) - { - } - - internal override void Update(APIChannel model) - { - base.Update(model); - if (model.Topic != null) Topic = model.Topic; - } - /// Save all changes to this channel. - public override async Task Save() - { - var request = new UpdateChannelRequest(Id) - { - Name = Name, - Topic = Topic, - Position = Position - }; - await Client.ClientAPI.Send(request).ConfigureAwait(false); - } - - public Message GetMessage(ulong id) => _messages.Get(id); - public Task DownloadMessages(int limit = 100, ulong? relativeMessageId = null, Relative relativeDir = Relative.Before) - => _messages.Download(limit, relativeMessageId, relativeDir); - - public Task SendMessage(string text, bool isTTS = false) => _messages.Send(text, isTTS); - public Task SendFile(string filePath) => _messages.SendFile(filePath); - public Task SendFile(string filename, Stream stream) => _messages.SendFile(filename, stream); - public Task SendIsTyping() => Client.ClientAPI.Send(new SendIsTypingRequest(Id)); - - internal override Channel Clone() - { - var result = new TextChannel(Id, Server); - _cloner(this, result); - return result; - } - } -} diff --git a/src/Discord.Net/Entities/User.cs b/src/Discord.Net/Entities/User.cs deleted file mode 100644 index 89a0b1133..000000000 --- a/src/Discord.Net/Entities/User.cs +++ /dev/null @@ -1,356 +0,0 @@ -using Discord.API.Client; -using Discord.API.Client.Rest; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using APIMember = Discord.API.Client.Member; - -namespace Discord -{ - 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; - - [Flags] - private enum VoiceState : byte - { - Normal = 0x0, - SelfMuted = 0x01, - SelfDeafened = 0x02, - ServerMuted = 0x10, - ServerDeafened = 0x20, - ServerSuppressed = 0x40, - } - - internal struct CompositeKey : IEquatable - { - public ulong ServerId, UserId; - public CompositeKey(ulong userId, ulong? serverId) - { - ServerId = serverId ?? 0; - UserId = userId; - } - - public bool Equals(CompositeKey other) - => UserId == other.UserId && ServerId == other.ServerId; - public override int GetHashCode() - => unchecked(ServerId.GetHashCode() + UserId.GetHashCode() + 23); - } - - private VoiceState _voiceState; - private DateTime? _lastOnline; - private ulong? _voiceChannelId; - private Dictionary _roles; - - public DiscordClient Client { get; } - - /// Gets the unique identifier for this user. - public ulong Id { get; } - /// Gets the server this user is a member of. - public Server Server { get; } - - /// Gets the name of this user. - public string Name { get; private set; } - /// Gets an id uniquely identifying from others with the same name. - public ushort Discriminator { get; private set; } - /// Gets the unique identifier for this user's current avatar. - public string AvatarId { get; private set; } - /// Gets the name of the game this user is currently playing. - public string CurrentGame { get; internal set; } - /// Gets the current status for this user. - public UserStatus Status { get; internal set; } - /// Gets the datetime that this user joined this server. - public DateTime JoinedAt { get; private set; } - /// Returns the time this user last sent/edited a message, started typing or sent voice data in this server. - public DateTime? LastActivityAt { get; private set; } - // /// Gets this user's voice session id. - // public string SessionId { get; private set; } - // /// Gets this user's voice token. - // public string Token { get; private set; } - - /// Gets the current private channel for this user if one exists. - public PrivateChannel PrivateChannel => Client.GetPrivateChannel(Id); - /// Returns the string used to mention this user. - public string Mention => $"<@{Id}>"; - public bool IsOwner => Server == null ? false : this == Server.Owner; - /// Returns true if this user has marked themselves as muted. - public bool IsSelfMuted => (_voiceState & VoiceState.SelfMuted) != 0; - /// Returns true if this user has marked themselves as deafened. - public bool IsSelfDeafened => (_voiceState & VoiceState.SelfDeafened) != 0; - /// Returns true if the server is blocking audio from this user. - public bool IsServerMuted => (_voiceState & VoiceState.ServerMuted) != 0; - /// Returns true if the server is blocking audio to this user. - public bool IsServerDeafened => (_voiceState & VoiceState.ServerDeafened) != 0; - /// Returns true if the server is temporarily blocking audio to/from this user. - public bool IsServerSuppressed => (_voiceState & VoiceState.ServerSuppressed) != 0; - /// Returns the time this user was last seen online in this server. - public DateTime? LastOnlineAt => Status != UserStatus.Offline ? DateTime.UtcNow : _lastOnline; - /// Gets this user's current voice channel. - public VoiceChannel VoiceChannel => _voiceChannelId != null ? Server.GetVoiceChannel(_voiceChannelId.Value) : null; - /// Gets the URL to this user's current avatar. - public string AvatarUrl => GetAvatarUrl(Id, AvatarId); - /// Gets all roles that have been assigned to this user, including the everyone role. - public IEnumerable Roles => _roles.Select(x => x.Value); - public ServerPermissions ServerPermissions => Server.GetPermissions(this); - - /// Returns a collection of all channels this user has permissions to join on this server. - public IEnumerable Channels - { - get - { - if (Server != null) - { - if (Client.Config.UsePermissionsCache) - { - return Server.AllChannels.Where(x => - (x.IsText && x.GetPermissions(this).ReadMessages) || - (x.IsVoice && x.GetPermissions(this).Connect)); - } - else - { - ChannelPermissions perms = new ChannelPermissions(); - return Server.AllChannels - .Where(x => - { - x.ResolvePermissions(this, ref perms); - return (x.Type == ChannelType.Text && perms.ReadMessages) || - (x.Type == ChannelType.Voice && perms.Connect); - }); - } - } - else - { - if (this == Client.PrivateUser) - return Client.PrivateChannels; - else - { - var privateChannel = Client.GetPrivateChannel(Id); - if (privateChannel != null) - return new IChannel[] { privateChannel }; - else - return new IChannel[0]; - } - } - } - } - - internal User(ExtendedMember model, DiscordClient client, Server server) - : this(model as APIMember, client, server) - { - if (model.IsServerMuted == true) - _voiceState |= VoiceState.ServerMuted; - else if (model.IsServerMuted == false) - _voiceState &= ~VoiceState.ServerMuted; - - if (model.IsServerDeafened == true) - _voiceState |= VoiceState.ServerDeafened; - else if (model.IsServerDeafened == false) - _voiceState &= ~VoiceState.ServerDeafened; - } - internal User(APIMember model, DiscordClient client, Server server) - : this(model.User.Id, client, server) - { - if (server == null) - UpdateRoles(null); - Update(model); - } - internal User(UserReference model, DiscordClient client, Server server) - : this(model.Id, client, server) - { - if (server == null) - UpdateRoles(null); - Update(model); - } - private User(ulong id, DiscordClient client, Server server) - { - Client = client; - Id = id; - Server = server; - - _roles = new Dictionary(); - Status = UserStatus.Offline; - } - - internal void Update(UserReference model) - { - if (model.Username != null) - Name = model.Username; - if (model.Discriminator != null) - Discriminator = model.Discriminator.Value; - if (model.Avatar != null) - AvatarId = model.Avatar; - } - internal void Update(APIMember model) - { - if (model.User != null) - Update(model.User); - - if (model.JoinedAt.HasValue) - JoinedAt = model.JoinedAt.Value; - if (model.Roles != null) - UpdateRoles(model.Roles.Select(x => Server.GetRole(x))); - } - internal void Update(MemberPresence model) - { - if (model.User != null) - Update(model.User as UserReference); - - if (model.Roles != null) - UpdateRoles(model.Roles.Select(x => Server.GetRole(x))); - if (model.Status != null && Status != model.Status) - { - Status = UserStatus.FromString(model.Status); - if (Status == UserStatus.Offline) - _lastOnline = DateTime.UtcNow; - } - - CurrentGame = model.Game?.Name; //Allows null - } - internal void Update(MemberVoiceState model) - { - if (model.IsSelfMuted == true) - _voiceState |= VoiceState.SelfMuted; - else if (model.IsSelfMuted == false) - _voiceState &= ~VoiceState.SelfMuted; - if (model.IsSelfDeafened == true) - _voiceState |= VoiceState.SelfDeafened; - else if (model.IsSelfDeafened == false) - _voiceState &= ~VoiceState.SelfDeafened; - if (model.IsServerMuted == true) - _voiceState |= VoiceState.ServerMuted; - else if (model.IsServerMuted == false) - _voiceState &= ~VoiceState.ServerMuted; - if (model.IsServerDeafened == true) - _voiceState |= VoiceState.ServerDeafened; - else if (model.IsServerDeafened == false) - _voiceState &= ~VoiceState.ServerDeafened; - if (model.IsServerSuppressed == true) - _voiceState |= VoiceState.ServerSuppressed; - else if (model.IsServerSuppressed == false) - _voiceState &= ~VoiceState.ServerSuppressed; - - /*if (model.SessionId != null) - SessionId = model.SessionId; - if (model.Token != null) - Token = model.Token;*/ - - _voiceChannelId = model.ChannelId; //Allows null - } - - internal void UpdateActivity(DateTime? activity = null) - { - if (LastActivityAt == null || activity > LastActivityAt.Value) - LastActivityAt = activity ?? DateTime.UtcNow; - } - - public Task Edit(bool? isMuted = null, bool? isDeafened = null, Channel voiceChannel = null, IEnumerable roles = null) - { - if (Server == null) throw new InvalidOperationException("Unable to edit users in a private channel"); - - //Modify the roles collection and filter out the everyone role - var roleIds = (roles ?? Roles) - .Where(x => !x.IsEveryone) - .Select(x => x.Id) - .Distinct() - .ToArray(); - - var request = new UpdateMemberRequest(Server.Id, Id) - { - IsMuted = isMuted ?? IsServerMuted, - IsDeafened = isDeafened ?? IsServerDeafened, - VoiceChannelId = voiceChannel?.Id, - RoleIds = roleIds - }; - return Client.ClientAPI.Send(request); - } - - public Task Kick() - { - if (Server == null) throw new InvalidOperationException("Unable to kick users from a private channel"); - - var request = new KickMemberRequest(Server.Id, Id); - return Client.ClientAPI.Send(request); - } - - public ChannelPermissions GetPermissions(PublicChannel channel) - { - if (channel == null) throw new ArgumentNullException(nameof(channel)); - return channel.GetPermissions(this); - } - - public Task CreatePMChannel() => Client.CreatePrivateChannel(this); - - private void UpdateRoles(IEnumerable roles) - { - bool updated = false; - var newRoles = new Dictionary(); - - var oldRoles = _roles; - if (roles != null) - { - foreach (var r in roles) - { - if (r != null) - { - newRoles[r.Id] = r; - if (!oldRoles.ContainsKey(r.Id)) - updated = true; //Check for adds - } - } - } - - if (Server != null) - { - var everyone = Server.EveryoneRole; - newRoles[everyone.Id] = everyone; - } - if (oldRoles.Count != newRoles.Count) - updated = true; //Check for removes - - if (updated) - { - _roles = newRoles; - if (Server != null) - Server.UpdatePermissions(this); - } - } - public bool HasRole(Role role) - { - if (role == null) throw new ArgumentNullException(nameof(role)); - - return _roles.ContainsKey(role.Id); - } - - public Task AddRoles(params Role[] roles) => Edit(roles: Roles.Concat(roles)); - public Task AddRoles(IEnumerable roles) => Edit(roles: Roles.Concat(roles)); - public Task RemoveRoles(params Role[] roles) => Edit(roles: Roles.Except(roles)); - public Task RemoveRoles(IEnumerable roles) => Edit(roles: Roles.Except(roles)); - - internal User Clone() - { - var result = new User(Id, Client, Server); - _cloner(this, result); - return result; - } - - public override string ToString() - { - if (Name != null) - return $"{Server?.Name ?? "[Private]"}/{Name}#{Discriminator}"; - else - return $"{Server?.Name ?? "[Private]"}/{Id}"; - } - internal string ToString(IChannel channel) - { - if (Name != null) - return $"{channel}/{Name}#{Discriminator}"; - else - return $"{channel}/{Id}"; - } - } -} \ No newline at end of file diff --git a/src/Discord.Net/Entities/Users/DMUser.cs b/src/Discord.Net/Entities/Users/DMUser.cs new file mode 100644 index 000000000..ed2c7d541 --- /dev/null +++ b/src/Discord.Net/Entities/Users/DMUser.cs @@ -0,0 +1,15 @@ +namespace Discord +{ + public class DMUser : User + { + public DMChannel Channel { get; } + + public override DiscordClient Discord => Channel.Discord; + + internal DMUser(ulong id, DMChannel channel) + : base(id) + { + Channel = channel; + } + } +} diff --git a/src/Discord.Net/Entities/Users/GuildUser.cs b/src/Discord.Net/Entities/Users/GuildUser.cs new file mode 100644 index 000000000..bc0c8db8c --- /dev/null +++ b/src/Discord.Net/Entities/Users/GuildUser.cs @@ -0,0 +1,48 @@ +using Discord.API.Rest; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Model = Discord.API.GuildMember; + +namespace Discord +{ + public class GuildUser : User, IMentionable + { + public Guild Guild { get; } + public GuildPresence Presence { get; } + public VoiceState VoiceState { get; } + + /// + public DateTime JoinedAt { get; private set; } + public GuildPermissions GuildPermissions { get; internal set; } + + public override DiscordClient Discord => Guild.Discord; + public IEnumerable TextChannels => Guild.TextChannels.Where(x => GetPermissions(x).ReadMessages); + + internal GuildUser(ulong id, Guild guild, GuildPresence presence, VoiceState voiceState) + : base(id) + { + Guild = guild; + Presence = presence; + VoiceState = voiceState; + } + internal void Update(Model model) + { + base.Update(model.User); + JoinedAt = model.JoinedAt.Value; + } + + public ChannelPermissions GetPermissions(GuildChannel channel) + { + if (channel == null) throw new ArgumentNullException(nameof(channel)); + return channel.GetPermissions(this); + } + + /// + public override Task Update() { throw new NotSupportedException(); } //TODO: Not supported yet + public Task Kick() => Discord.RestClient.Send(new RemoveGuildMemberRequest(Guild.Id, Id)); + public Task Ban(int pruneDays = 0) => Discord.RestClient.Send(new CreateGuildBanRequest(Guild.Id, Id) { PruneDays = pruneDays }); + public Task Unban() => Discord.RestClient.Send(new RemoveGuildBanRequest(Guild.Id, Id)); + } +} diff --git a/src/Discord.Net/Entities/Users/PublicUser.cs b/src/Discord.Net/Entities/Users/PublicUser.cs new file mode 100644 index 000000000..2ab851a25 --- /dev/null +++ b/src/Discord.Net/Entities/Users/PublicUser.cs @@ -0,0 +1,13 @@ +namespace Discord +{ + public class PublicUser : User + { + public override DiscordClient Discord { get; } + + internal PublicUser(ulong id, DiscordClient discord) + : base(id) + { + Discord = discord; + } + } +} diff --git a/src/Discord.Net/Entities/Users/SelfUser.cs b/src/Discord.Net/Entities/Users/SelfUser.cs new file mode 100644 index 000000000..4174672cf --- /dev/null +++ b/src/Discord.Net/Entities/Users/SelfUser.cs @@ -0,0 +1,26 @@ +using Model = Discord.API.User; + +namespace Discord +{ + public class SelfUser : User + { + public override DiscordClient Discord { get; } + + public string Email { get; private set; } + public bool IsVerified { get; private set; } + + internal SelfUser(ulong id, DiscordClient discord) + : base(id) + { + Discord = discord; + } + + internal override void Update(Model model) + { + base.Update(model); + + Email = model.Email; + IsVerified = model.IsVerified; + } + } +} diff --git a/src/Discord.Net/Entities/Users/User.cs b/src/Discord.Net/Entities/Users/User.cs new file mode 100644 index 000000000..92c5a2773 --- /dev/null +++ b/src/Discord.Net/Entities/Users/User.cs @@ -0,0 +1,39 @@ +using System; +using System.Threading.Tasks; +using Model = Discord.API.User; + +namespace Discord +{ + public abstract class User : IEntity + { + private string _avatarId; + + /// + public ulong Id { get; } + /// + public abstract DiscordClient Discord { get; } + + public string Username { get; private set; } + public ushort Discriminator { get; private set; } + public bool IsBot { get; private set; } + + public string AvatarUrl => CDN.GetUserAvatarUrl(Id, _avatarId); + public string Mention => MentionHelper.Mention(this); + + internal User(ulong id) + { + Id = id; + } + internal virtual void Update(Model model) + { + Username = model.Username; + Discriminator = model.Discriminator; + IsBot = model.Bot; + _avatarId = model.Avatar; + } + + public virtual Task Update() { throw new NotSupportedException(); } + + public async Task CreateDMChannel() => await Discord.GetOrCreateDMChannel(Id); //TODO: We dont want both this and .Channel to appear on DMUser + } +} diff --git a/src/Discord.Net/Entities/VoiceChannel.cs b/src/Discord.Net/Entities/VoiceChannel.cs deleted file mode 100644 index 5b40a6aad..000000000 --- a/src/Discord.Net/Entities/VoiceChannel.cs +++ /dev/null @@ -1,60 +0,0 @@ -using APIChannel = Discord.API.Client.Channel; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Discord.API.Client.Rest; - -namespace Discord -{ - public class VoiceChannel : PublicChannel, IPublicChannel, IVoiceChannel - { - private readonly static Action _cloner = DynamicIL.CreateCopyMethod(); - - public int Bitrate { get; set; } - - public override ChannelType Type => ChannelType.Public | ChannelType.Voice; - /// Gets a collection of all users currently in this voice channel. - public override IEnumerable Users - { - get - { - if (Client.Config.UsePermissionsCache) - return _permissions.Users.Select(x => x.User).Where(x => x.VoiceChannel == this); - else - return Server.Users.Where(x => x.VoiceChannel == this); - } - } - - internal override MessageManager MessageManager => null; - - internal VoiceChannel(APIChannel model, Server server) - : base(model, server) - { - } - private VoiceChannel(ulong id, Server server) - : base(id, server) - { - } - - - /// Save all changes to this channel. - public override async Task Save() - { - var request = new UpdateChannelRequest(Id) - { - Name = Name, - Position = Position, - Bitrate = Bitrate - }; - await Client.ClientAPI.Send(request).ConfigureAwait(false); - } - - internal override Channel Clone() - { - var result = new VoiceChannel(Id, Server); - _cloner(this, result); - return result; - } - } -} diff --git a/src/Discord.Net/Entities/VoiceRegion.cs b/src/Discord.Net/Entities/VoiceRegion.cs new file mode 100644 index 000000000..bba703240 --- /dev/null +++ b/src/Discord.Net/Entities/VoiceRegion.cs @@ -0,0 +1,60 @@ +using System; +using System.Threading.Tasks; +using Model = Discord.API.Rest.GetVoiceRegionsResponse; + +namespace Discord +{ + public class VoiceRegion : IEntity + { + /// + public string Id { get; } + /// + public DiscordClient Discord { get; } + + /// Gets the name of this voice region. + public string Name { get; private set; } + /// Returns true if this voice region is exclusive to VIP accounts. + public bool IsVip { get; private set; } + /// Returns true if this voice region is the closest to your machine. + public bool IsOptimal { get; private set; } + /// Gets an example hostname for this voice region. + public string SampleHostname { get; private set; } + /// Gets an example port for this voice region. + public int SamplePort { get; private set; } + + internal VoiceRegion(string id, DiscordClient client) + { + Id = id; + Discord = client; + } + + /// + public Task Update() { throw new NotSupportedException(); } //TODO: Not supported yet + + public void Update(Model model) + { + Name = model.Name; + IsVip = model.IsVip; + IsOptimal = model.IsOptimal; + SampleHostname = model.SampleHostname; + SamplePort = model.SamplePort; + } + + public override string ToString() + { + string suffix = ""; + + if (IsVip) + { + if (IsOptimal) + suffix = " (VIP, Optimal)"; + else + suffix = " (VIP)"; + } + else if (IsOptimal) + suffix = " (Optimal)"; + + return Name + suffix; + } + } +} diff --git a/src/Discord.Net/EnumConverters.cs b/src/Discord.Net/EnumConverters.cs deleted file mode 100644 index 7448055f6..000000000 --- a/src/Discord.Net/EnumConverters.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; - -namespace Discord -{ - public static class EnumConverters - { - public static ChannelType ToChannelType(string value) - { - switch (value) - { - case "text": return ChannelType.Text; - case "voice": return ChannelType.Voice; - default: throw new ArgumentException("Unknown channel type", nameof(value)); - } - } - public static string ToString(ChannelType value) - { - if ((value & ChannelType.Text) != 0) return "text"; - if ((value & ChannelType.Voice) != 0) return "voice"; - throw new ArgumentException("Invalid channel tType", nameof(value)); - } - - public static PermissionTarget ToPermissionTarget(string value) - { - switch (value) - { - case "member": return PermissionTarget.User; - case "role": return PermissionTarget.Role; - default: throw new ArgumentException("Unknown permission target", nameof(value)); - } - } - public static string ToString(PermissionTarget value) - { - switch (value) - { - case PermissionTarget.User: return "member"; - case PermissionTarget.Role: return "role"; - default: throw new ArgumentException("Invalid permission target", nameof(value)); - } - } - } -} diff --git a/src/Discord.Net/Enums/ChannelType.cs b/src/Discord.Net/Enums/ChannelType.cs index 77b0614ea..e6a3a1e00 100644 --- a/src/Discord.Net/Enums/ChannelType.cs +++ b/src/Discord.Net/Enums/ChannelType.cs @@ -1,13 +1,9 @@ -using System; - -namespace Discord +namespace Discord { - [Flags] - public enum ChannelType : byte + public enum ChannelType : byte { - Public = 0x01, - Private = 0x02, - Text = 0x10, - Voice = 0x20 + DM, + Text, + Voice } } diff --git a/src/Discord.Net/Enums/ConnectionState.cs b/src/Discord.Net/Enums/ConnectionState.cs index 42c505ccd..dfd4ac9eb 100644 --- a/src/Discord.Net/Enums/ConnectionState.cs +++ b/src/Discord.Net/Enums/ConnectionState.cs @@ -1,6 +1,6 @@ namespace Discord { - public enum ConnectionState : byte + public enum ConnectionState { Disconnected, Connecting, diff --git a/src/Discord.Net/Enums/ImageType.cs b/src/Discord.Net/Enums/ImageType.cs deleted file mode 100644 index 738c67a3d..000000000 --- a/src/Discord.Net/Enums/ImageType.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Discord -{ - public enum ImageType - { - None, - Jpeg, - Png - } -} diff --git a/src/Discord.Net/Enums/LogSeverity.cs b/src/Discord.Net/Enums/LogSeverity.cs index c62d8c250..785b0ef46 100644 --- a/src/Discord.Net/Enums/LogSeverity.cs +++ b/src/Discord.Net/Enums/LogSeverity.cs @@ -1,7 +1,8 @@ namespace Discord { - public enum LogSeverity : byte + public enum LogSeverity { + Critical = 0, Error = 1, Warning = 2, Info = 3, diff --git a/src/Discord.Net/Enums/MessageState.cs b/src/Discord.Net/Enums/MessageState.cs deleted file mode 100644 index 59f65614d..000000000 --- a/src/Discord.Net/Enums/MessageState.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace Discord -{ - public enum MessageState : byte - { - /// Message did not originate from this session, or was successfully sent. - Normal = 0, - /// Message is current queued. - Queued, - /// Message was deleted. - Deleted, - /// Message was deleted before it was sent. - Aborted, - /// Message failed to be sent. - Failed, - /// Message has been removed from cache and will no longer receive updates. - Detached - } -} diff --git a/src/Discord.Net/Enums/PermissionBits.cs b/src/Discord.Net/Enums/PermissionBits.cs index 0766dadc4..ac51dfb05 100644 --- a/src/Discord.Net/Enums/PermissionBits.cs +++ b/src/Discord.Net/Enums/PermissionBits.cs @@ -1,6 +1,6 @@ namespace Discord { - internal enum PermissionBits : byte + internal enum PermissionBit : byte { //General CreateInstantInvite = 0, @@ -8,7 +8,7 @@ BanMembers = 2, ManageRolesOrPermissions = 3, ManageChannel = 4, - ManageServer = 5, + ManageGuild = 5, //Text ReadMessages = 10, diff --git a/src/Discord.Net/Enums/PermissionTarget.cs b/src/Discord.Net/Enums/PermissionTarget.cs index d1381a5ec..96595fb69 100644 --- a/src/Discord.Net/Enums/PermissionTarget.cs +++ b/src/Discord.Net/Enums/PermissionTarget.cs @@ -1,8 +1,8 @@ namespace Discord { - public enum PermissionTarget : byte - { - User, - Role + public enum PermissionTarget + { + Role, + User } } diff --git a/src/Discord.Net/Enums/Relative.cs b/src/Discord.Net/Enums/Relative.cs index 4bd44c5ab..aade047d1 100644 --- a/src/Discord.Net/Enums/Relative.cs +++ b/src/Discord.Net/Enums/Relative.cs @@ -2,6 +2,7 @@ { public enum Relative { - Before, After + Before, + After } } diff --git a/src/Discord.Net/Enums/StringEnum.cs b/src/Discord.Net/Enums/StringEnum.cs deleted file mode 100644 index 903bdfdba..000000000 --- a/src/Discord.Net/Enums/StringEnum.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Discord -{ - public abstract class StringEnum - { - public string Value { get; } - - protected StringEnum(string value) - { - Value = value; - } - - public override string ToString() => Value; - } -} diff --git a/src/Discord.Net/Enums/UserStatus.cs b/src/Discord.Net/Enums/UserStatus.cs index 80def4234..f2fdfda7c 100644 --- a/src/Discord.Net/Enums/UserStatus.cs +++ b/src/Discord.Net/Enums/UserStatus.cs @@ -1,40 +1,9 @@ namespace Discord { - public class UserStatus : StringEnum - { - /// User is currently online and active. - public static UserStatus Online { get; } = new UserStatus("online"); - /// User is currently online but inactive. - public static UserStatus Idle { get; } = new UserStatus("idle"); - /// User is offline. - public static UserStatus Offline { get; } = new UserStatus("offline"); - - private UserStatus(string value) - : base(value) { } - - public static UserStatus FromString(string value) - { - switch (value) - { - case null: - return null; - case "online": - return Online; - case "idle": - return Idle; - case "offline": - return Offline; - default: - return new UserStatus(value); - } - } - - - public static implicit operator UserStatus(string value) => FromString(value); - public static bool operator ==(UserStatus a, UserStatus b) => ((object)a == null && (object)b == null) || (a?.Equals(b) ?? false); - public static bool operator !=(UserStatus a, UserStatus b) => !(a == b); - public override int GetHashCode() => Value.GetHashCode(); - public override bool Equals(object obj) => (obj as UserStatus)?.Equals(this) ?? false; - public bool Equals(UserStatus type) => type != null && type.Value == Value; + public enum UserStatus + { + Online, + Idle, + Offline } } diff --git a/src/Discord.Net/Events/ChannelEventArgs.cs b/src/Discord.Net/Events/ChannelEventArgs.cs index ac31a27f5..b03db540f 100644 --- a/src/Discord.Net/Events/ChannelEventArgs.cs +++ b/src/Discord.Net/Events/ChannelEventArgs.cs @@ -6,6 +6,9 @@ namespace Discord { public IChannel Channel { get; } - public ChannelEventArgs(IChannel channel) { Channel = channel; } + public ChannelEventArgs(IChannel channel) + { + Channel = channel; + } } } diff --git a/src/Discord.Net/Events/ChannelUpdatedEventArgs.cs b/src/Discord.Net/Events/ChannelUpdatedEventArgs.cs index 138f2dd8e..b4fbc258f 100644 --- a/src/Discord.Net/Events/ChannelUpdatedEventArgs.cs +++ b/src/Discord.Net/Events/ChannelUpdatedEventArgs.cs @@ -1,16 +1,14 @@ -using System; - -namespace Discord +namespace Discord { - public class ChannelUpdatedEventArgs : EventArgs + public class ChannelUpdatedEventArgs : ChannelEventArgs { public IChannel Before { get; } - public IChannel After { get; } + public IChannel After => Channel; public ChannelUpdatedEventArgs(IChannel before, IChannel after) + : base(after) { Before = before; - After = after; } } } diff --git a/src/Discord.Net/Events/CurrentUserEventArgs.cs b/src/Discord.Net/Events/CurrentUserEventArgs.cs new file mode 100644 index 000000000..a947bbfc5 --- /dev/null +++ b/src/Discord.Net/Events/CurrentUserEventArgs.cs @@ -0,0 +1,14 @@ +using System; + +namespace Discord +{ + public class CurrentUserEventArgs : EventArgs + { + public SelfUser CurrentUser { get; } + + public CurrentUserEventArgs(SelfUser currentUser) + { + CurrentUser = currentUser; + } + } +} diff --git a/src/Discord.Net/Events/CurrentUserUpdatedEventArgs.cs b/src/Discord.Net/Events/CurrentUserUpdatedEventArgs.cs new file mode 100644 index 000000000..4309f0312 --- /dev/null +++ b/src/Discord.Net/Events/CurrentUserUpdatedEventArgs.cs @@ -0,0 +1,14 @@ +namespace Discord +{ + public class CurrentUserUpdatedEventArgs : CurrentUserEventArgs + { + public SelfUser Before { get; } + public SelfUser After => CurrentUser; + + public CurrentUserUpdatedEventArgs(SelfUser before, SelfUser after) + : base(after) + { + Before = before; + } + } +} diff --git a/src/Discord.Net/Events/DisconnectedEventArgs.cs b/src/Discord.Net/Events/DisconnectedEventArgs.cs index 87f9ec955..cf7c1bf70 100644 --- a/src/Discord.Net/Events/DisconnectedEventArgs.cs +++ b/src/Discord.Net/Events/DisconnectedEventArgs.cs @@ -7,10 +7,10 @@ namespace Discord public bool WasUnexpected { get; } public Exception Exception { get; } - public DisconnectedEventArgs(bool wasUnexpected, Exception ex) + public DisconnectedEventArgs(bool wasUnexpected, Exception exception = null) { WasUnexpected = wasUnexpected; - Exception = ex; + Exception = exception; } } } diff --git a/src/Discord.Net/Events/GuildEventArgs.cs b/src/Discord.Net/Events/GuildEventArgs.cs new file mode 100644 index 000000000..50625d882 --- /dev/null +++ b/src/Discord.Net/Events/GuildEventArgs.cs @@ -0,0 +1,14 @@ +using System; + +namespace Discord +{ + public class GuildEventArgs : EventArgs + { + public Guild Guild { get; } + + public GuildEventArgs(Guild guild) + { + Guild = guild; + } + } +} diff --git a/src/Discord.Net/Events/GuildUpdatedEventArgs.cs b/src/Discord.Net/Events/GuildUpdatedEventArgs.cs new file mode 100644 index 000000000..9258072f7 --- /dev/null +++ b/src/Discord.Net/Events/GuildUpdatedEventArgs.cs @@ -0,0 +1,14 @@ +namespace Discord +{ + public class GuildUpdatedEventArgs : GuildEventArgs + { + public Guild Before { get; } + public Guild After => Guild; + + public GuildUpdatedEventArgs(Guild before, Guild after) + : base(after) + { + Before = before; + } + } +} diff --git a/src/Discord.Net/Events/LogMessageEventArgs.cs b/src/Discord.Net/Events/LogMessageEventArgs.cs index bd1fa5b93..83de48616 100644 --- a/src/Discord.Net/Events/LogMessageEventArgs.cs +++ b/src/Discord.Net/Events/LogMessageEventArgs.cs @@ -1,4 +1,5 @@ using System; +using System.Text; namespace Discord { @@ -9,12 +10,54 @@ namespace Discord public string Message { get; } public Exception Exception { get; } - public LogMessageEventArgs(LogSeverity severity, string source, string msg, Exception exception) + public LogMessageEventArgs(LogSeverity severity, string source, string message, Exception exception = null) { Severity = severity; Source = source; - Message = msg; + Message = message; Exception = exception; } + + public override string ToString() => ToString(null, true); + + public string ToString(StringBuilder builder = null, bool fullException = true) + { + string sourceName = Source; + string message = Message; + string exMessage = fullException ? Exception?.ToString() : Exception?.Message; + + int maxLength = 1 + (sourceName?.Length ?? 0) + 2 + (message?.Length ?? 0) + 3 + (exMessage?.Length ?? 0); + if (builder == null) + builder = new StringBuilder(maxLength); + else + { + builder.Clear(); + builder.EnsureCapacity(maxLength); + } + + if (sourceName != null) + { + builder.Append('['); + builder.Append(sourceName); + builder.Append("] "); + } + if (!string.IsNullOrEmpty(Message)) + { + for (int i = 0; i < message.Length; i++) + { + //Strip control chars + char c = message[i]; + if (!char.IsControl(c)) + builder.Append(c); + } + } + if (exMessage != null) + { + builder.AppendLine(":"); + builder.Append(exMessage); + } + + return builder.ToString(); + } } } diff --git a/src/Discord.Net/Events/MessageEventArgs.cs b/src/Discord.Net/Events/MessageEventArgs.cs index 76c9455dc..366b16dbb 100644 --- a/src/Discord.Net/Events/MessageEventArgs.cs +++ b/src/Discord.Net/Events/MessageEventArgs.cs @@ -6,10 +6,9 @@ namespace Discord { public Message Message { get; } - public User User => Message.User; - public ITextChannel Channel => Message.Channel; - public Server Server => Message.Server; - - public MessageEventArgs(Message msg) { Message = msg; } + public MessageEventArgs(Message message) + { + Message = message; + } } } diff --git a/src/Discord.Net/Events/MessageUpdatedEventArgs.cs b/src/Discord.Net/Events/MessageUpdatedEventArgs.cs index 1583dc981..318ac9d54 100644 --- a/src/Discord.Net/Events/MessageUpdatedEventArgs.cs +++ b/src/Discord.Net/Events/MessageUpdatedEventArgs.cs @@ -1,20 +1,14 @@ -using System; - -namespace Discord +namespace Discord { - public class MessageUpdatedEventArgs : EventArgs + public class MessageUpdatedEventArgs : MessageEventArgs { public Message Before { get; } - public Message After { get; } - - public User User => After.User; - public ITextChannel Channel => After.Channel; - public Server Server => After.Server; + public Message After => Message; public MessageUpdatedEventArgs(Message before, Message after) + : base(after) { Before = before; - After = after; } } } diff --git a/src/Discord.Net/Events/ProfileUpdatedEventArgs.cs b/src/Discord.Net/Events/ProfileUpdatedEventArgs.cs deleted file mode 100644 index 2365908e8..000000000 --- a/src/Discord.Net/Events/ProfileUpdatedEventArgs.cs +++ /dev/null @@ -1,16 +0,0 @@ -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/Events/RoleEventArgs.cs b/src/Discord.Net/Events/RoleEventArgs.cs index 13eb0f7f4..887972ffe 100644 --- a/src/Discord.Net/Events/RoleEventArgs.cs +++ b/src/Discord.Net/Events/RoleEventArgs.cs @@ -6,8 +6,9 @@ namespace Discord { public Role Role { get; } - public Server Server => Role.Server; - - public RoleEventArgs(Role role) { Role = role; } + public RoleEventArgs(Role role) + { + Role = role; + } } } diff --git a/src/Discord.Net/Events/RoleUpdatedEventArgs.cs b/src/Discord.Net/Events/RoleUpdatedEventArgs.cs index 26151c98b..285ab42c5 100644 --- a/src/Discord.Net/Events/RoleUpdatedEventArgs.cs +++ b/src/Discord.Net/Events/RoleUpdatedEventArgs.cs @@ -1,18 +1,14 @@ -using System; - -namespace Discord +namespace Discord { - public class RoleUpdatedEventArgs : EventArgs + public class RoleUpdatedEventArgs : RoleEventArgs { public Role Before { get; } - public Role After { get; } - - public Server Server => After.Server; + public Role After => Role; public RoleUpdatedEventArgs(Role before, Role after) + : base(after) { Before = before; - After = after; } } } diff --git a/src/Discord.Net/Events/ServerEventArgs.cs b/src/Discord.Net/Events/ServerEventArgs.cs deleted file mode 100644 index e9e564e1b..000000000 --- a/src/Discord.Net/Events/ServerEventArgs.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace Discord -{ - public class ServerEventArgs : EventArgs - { - public Server Server { get; } - - public ServerEventArgs(Server server) { Server = server; } - } -} diff --git a/src/Discord.Net/Events/ServerUpdatedEventArgs.cs b/src/Discord.Net/Events/ServerUpdatedEventArgs.cs deleted file mode 100644 index 8532f72dc..000000000 --- a/src/Discord.Net/Events/ServerUpdatedEventArgs.cs +++ /dev/null @@ -1,16 +0,0 @@ -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/Events/TypingEventArgs.cs b/src/Discord.Net/Events/TypingEventArgs.cs index 73d47f688..797d0d9d6 100644 --- a/src/Discord.Net/Events/TypingEventArgs.cs +++ b/src/Discord.Net/Events/TypingEventArgs.cs @@ -1,11 +1,13 @@ -namespace Discord +using System; + +namespace Discord { - public class TypingEventArgs + public class TypingEventArgs : EventArgs { - public ITextChannel Channel { get; } + public IMessageChannel Channel { get; } public User User { get; } - public TypingEventArgs(ITextChannel channel, User user) + public TypingEventArgs(IMessageChannel channel, User user) { Channel = channel; User = user; diff --git a/src/Discord.Net/Events/UserEventArgs.cs b/src/Discord.Net/Events/UserEventArgs.cs index bf7dd2cac..6bbcaef01 100644 --- a/src/Discord.Net/Events/UserEventArgs.cs +++ b/src/Discord.Net/Events/UserEventArgs.cs @@ -1,12 +1,14 @@ using System; + namespace Discord { public class UserEventArgs : EventArgs { public User User { get; } - public Server Server => User.Server; - - public UserEventArgs(User user) { User = user; } + public UserEventArgs(User user) + { + User = user; + } } } diff --git a/src/Discord.Net/Events/UserUpdatedEventArgs.cs b/src/Discord.Net/Events/UserUpdatedEventArgs.cs index 89e8cce0c..35b68bc55 100644 --- a/src/Discord.Net/Events/UserUpdatedEventArgs.cs +++ b/src/Discord.Net/Events/UserUpdatedEventArgs.cs @@ -1,17 +1,14 @@ -using System; -namespace Discord +namespace Discord { - public class UserUpdatedEventArgs : EventArgs + public class UserUpdatedEventArgs : UserEventArgs { public User Before { get; } - public User After { get; } - - public Server Server => After.Server; + public User After => User; public UserUpdatedEventArgs(User before, User after) + : base(after) { Before = before; - After = after; } } } diff --git a/src/Discord.Net/Events/VoiceChannelEventArgs.cs b/src/Discord.Net/Events/VoiceChannelEventArgs.cs new file mode 100644 index 000000000..78d519d2f --- /dev/null +++ b/src/Discord.Net/Events/VoiceChannelEventArgs.cs @@ -0,0 +1,14 @@ +using System; + +namespace Discord +{ + public class VoiceChannelEventArgs : EventArgs + { + public VoiceChannel Channel { get; } + + public VoiceChannelEventArgs(VoiceChannel channel) + { + Channel = channel; + } + } +} diff --git a/src/Discord.Net/Format.cs b/src/Discord.Net/Format.cs index 0ff7e0a4f..1e8ae00b9 100644 --- a/src/Discord.Net/Format.cs +++ b/src/Discord.Net/Format.cs @@ -2,109 +2,104 @@ namespace Discord { - public static class Format - { - private static readonly string[] _patterns; - private static readonly StringBuilder _builder; + public static class Format + { + private static readonly string[] _patterns = new string[] { "__", "_", "**", "*", "~~", "```", "`" }; + private static readonly StringBuilder _builder = new StringBuilder(DiscordConfig.MaxMessageSize); - static Format() - { - _patterns = new string[] { "__", "_", "**", "*", "~~", "```", "`"}; - _builder = new StringBuilder(DiscordConfig.MaxMessageSize); - } - - /// Removes all special formatting characters from the provided text. - public static string Escape(string text) - { - lock (_builder) - { - _builder.Clear(); + /// Removes all special formatting characters from the provided text. + public static string Escape(string text) + { + //TODO: Fix me + lock (_builder) + { + _builder.Clear(); - //Escape all backslashes - for (int i = 0; i < text.Length; i++) - { - _builder.Append(text[i]); - if (text[i] == '\\') - _builder.Append('\\'); - } + //Escape all backslashes + for (int i = 0; i < text.Length; i++) + { + _builder.Append(text[i]); + if (text[i] == '\\') + _builder.Append('\\'); + } - EscapeSubstring(0, _builder.Length); + EscapeSubstring(0, _builder.Length); - return _builder.ToString(); - } - } - private static int EscapeSubstring(int start, int end) - { - int totalAddedChars = 0; - for (int i = start; i < end + totalAddedChars; i++) - { - for (int p = 0; p < _patterns.Length; p++) - { - string pattern = _patterns[p]; - if (i + pattern.Length * 2 > _builder.Length) - continue; - int s = FindPattern(pattern, i, i + 1); - if (s == -1) continue; - int e = FindPattern(pattern, i + 1, end + totalAddedChars); - if (e == -1) continue; + return _builder.ToString(); + } + } + private static int EscapeSubstring(int start, int end) + { + int totalAddedChars = 0; + for (int i = start; i < end + totalAddedChars; i++) + { + for (int p = 0; p < _patterns.Length; p++) + { + string pattern = _patterns[p]; + if (i + pattern.Length * 2 > _builder.Length) + continue; + int s = FindPattern(pattern, i, i + 1); + if (s == -1) continue; + int e = FindPattern(pattern, i + 1, end + totalAddedChars); + if (e == -1) continue; - if (e - s - pattern.Length > 0) - { - //By going right to left, we dont need to adjust any offsets - for (int k = pattern.Length - 1; k >= 0; k--) - _builder.Insert(e + k, '\\'); - for (int k = pattern.Length - 1; k >= 0; k--) - _builder.Insert(s + k, '\\'); + if (e - s - pattern.Length > 0) + { + //By going right to left, we dont need to adjust any offsets + for (int k = pattern.Length - 1; k >= 0; k--) + _builder.Insert(e + k, '\\'); + for (int k = pattern.Length - 1; k >= 0; k--) + _builder.Insert(s + k, '\\'); int addedChars = pattern.Length * 2; - addedChars += EscapeSubstring(s + pattern.Length * 2, e + pattern.Length); - i = e + addedChars + pattern.Length - 1; - totalAddedChars += addedChars; - break; + addedChars += EscapeSubstring(s + pattern.Length * 2, e + pattern.Length); + i = e + addedChars + pattern.Length - 1; + totalAddedChars += addedChars; + break; } - } - } - return totalAddedChars; - } - private static int FindPattern(string pattern, int start, int end) - { - for (int j = start; j < end; j++) - { - if (_builder[j] == '\\') - { - j++; - continue; - } - for (int k = 0; k < pattern.Length; k++) - { - if (_builder[j + k] != pattern[k]) - goto nextpos; - } - return j; - nextpos:; - } - return -1; - } - - /// Returns a markdown-formatted string with bold formatting, optionally escaping the contents. - public static string Bold(string text, bool escape = true) - => escape ? $"**{Escape(text)}**" : $"**{text}**"; - /// Returns a markdown-formatted string with italics formatting, optionally escaping the contents. - public static string Italics(string text, bool escape = true) - => escape ? $"*{Escape(text)}*" : $"*{text}*"; - /// Returns a markdown-formatted string with underline formatting, optionally escaping the contents. - public static string Underline(string text, bool escape = true) - => escape ? $"__{Escape(text)}__" : $"__{text}__"; - /// Returns a markdown-formatted string with strikeout formatting, optionally escaping the contents. - public static string Strikeout(string text, bool escape = true) - => escape ? $"~~{Escape(text)}~~" : $"~~{text}~~"; + } + } + return totalAddedChars; + } + private static int FindPattern(string pattern, int start, int end) + { + for (int j = start; j < end; j++) + { + if (_builder[j] == '\\') + { + j++; + continue; + } + for (int k = 0; k < pattern.Length; k++) + { + if (_builder[j + k] != pattern[k]) + goto nextpos; + } + return j; + nextpos:; + } + return -1; + } + + /// Returns a markdown-formatted string with bold formatting, optionally escaping the contents. + public static string Bold(string text, bool escape = true) + => escape ? $"**{Escape(text)}**" : $"**{text}**"; + /// Returns a markdown-formatted string with italics formatting, optionally escaping the contents. + public static string Italics(string text, bool escape = true) + => escape ? $"*{Escape(text)}*" : $"*{text}*"; + /// Returns a markdown-formatted string with underline formatting, optionally escaping the contents. + public static string Underline(string text, bool escape = true) + => escape ? $"__{Escape(text)}__" : $"__{text}__"; + /// Returns a markdown-formatted string with strikeout formatting, optionally escaping the contents. + public static string Strikeout(string text, bool escape = true) + => escape ? $"~~{Escape(text)}~~" : $"~~{text}~~"; - /// Returns a markdown-formatted string with strikeout formatting, optionally escaping the contents. - public static string Code(string text, string language = null) - { - if (language != null || text.Contains("\n")) - return $"```{language ?? ""}\n{text}\n```"; - else - return $"`{text}`"; - } - } + /// Returns a markdown-formatted string with strikeout formatting, optionally escaping the contents. + public static string Code(string text, string language = null) + { + if (language != null || text.Contains("\n")) + return $"```{language ?? ""}\n{text}\n```"; + else + return $"`{text}`"; + } + } } diff --git a/src/Discord.Net/IMentionable.cs b/src/Discord.Net/IMentionable.cs deleted file mode 100644 index 0a4bf439c..000000000 --- a/src/Discord.Net/IMentionable.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Discord -{ - public interface IMentionable - { - string Mention { get; } - } -} diff --git a/src/Discord.Net/IModel.cs b/src/Discord.Net/IModel.cs deleted file mode 100644 index 8a86ceb3d..000000000 --- a/src/Discord.Net/IModel.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Threading.Tasks; - -namespace Discord -{ - public interface IModel - { - ulong Id { get; } - - Task Save(); - } -} diff --git a/src/Discord.Net/IService.cs b/src/Discord.Net/IService.cs deleted file mode 100644 index 15f79b0c4..000000000 --- a/src/Discord.Net/IService.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Discord -{ - public interface IService - { - void Install(DiscordClient client); - } -} diff --git a/src/Discord.Net/InternalExtensions.cs b/src/Discord.Net/InternalExtensions.cs index 676f0f8e6..6b8342e57 100644 --- a/src/Discord.Net/InternalExtensions.cs +++ b/src/Discord.Net/InternalExtensions.cs @@ -1,75 +1,22 @@ -using System; -using System.Collections.Concurrent; -using System.Globalization; -using System.IO; -using System.Runtime.CompilerServices; - -namespace Discord +namespace Discord { internal static class InternalExtensions { - internal static readonly IFormatProvider _format = CultureInfo.InvariantCulture; - - public static ulong ToId(this string value) => ulong.Parse(value, NumberStyles.None, _format); - public static ulong? ToNullableId(this string value) => value == null ? (ulong?)null : ulong.Parse(value, NumberStyles.None, _format); - public static bool TryToId(this string value, out ulong result) => ulong.TryParse(value, NumberStyles.None, _format, out result); - - public static string ToIdString(this ulong value) => value.ToString(_format); - public static string ToIdString(this ulong? value) => value?.ToString(_format); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool HasBit(this uint rawValue, byte bit) => ((rawValue >> bit) & 1U) == 1; - - public static bool TryGetOrAdd(this ConcurrentDictionary d, - TKey key, Func factory, out TValue result) - { - bool created = false; - TValue newValue = default(TValue); - while (true) - { - if (d.TryGetValue(key, out result)) - return false; - if (!created) - { - newValue = factory(key); - created = true; - } - if (d.TryAdd(key, newValue)) - { - result = newValue; - return true; - } - } - } - public static bool TryGetOrAdd(this ConcurrentDictionary d, - TKey key, TValue value, out TValue result) + public static User GetCurrentUser(this IChannel channel) { - while (true) + switch (channel.Type) { - if (d.TryGetValue(key, out result)) - return false; - if (d.TryAdd(key, value)) - { - result = value; - return true; - } + case ChannelType.Text: + case ChannelType.Voice: + return (channel as GuildChannel).Guild.CurrentUser; + default: + return channel.Discord.CurrentUser; } } - public static string Base64(this Stream stream, ImageType type, string existingId) - { - if (type == ImageType.None) - return null; - else if (stream != null) - { - byte[] bytes = new byte[stream.Length - stream.Position]; - stream.Read(bytes, 0, bytes.Length); - - string base64 = Convert.ToBase64String(bytes); - string imageType = type == ImageType.Jpeg ? "image/jpeg;base64" : "image/png;base64"; - return $"data:{imageType},{base64}"; - } - return existingId; - } +/*#if NETSTANDARD1_2 + //https://github.com/dotnet/coreclr/blob/master/src/mscorlib/src/System/DateTimeOffset.cs + public static long ToUnixTimeMilliseconds(this DateTimeOffset dto) => (dto.UtcDateTime.Ticks / TimeSpan.TicksPerMillisecond) - 62135596800000; +#endif*/ } } diff --git a/src/Discord.Net/Logging/ILogger.cs b/src/Discord.Net/Logging/ILogger.cs deleted file mode 100644 index abc713cc2..000000000 --- a/src/Discord.Net/Logging/ILogger.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; - -namespace Discord.Logging -{ - public interface ILogger - { - LogSeverity Level { get; } - - void Log(LogSeverity severity, string message, Exception exception = null); -#if DOTNET5_4 - void Log(LogSeverity severity, FormattableString message, Exception exception = null); -#endif - - void Error(string message, Exception exception = null); -#if DOTNET5_4 - void Error(FormattableString message, Exception exception = null); -#endif - void Error(Exception exception); - - void Warning(string message, Exception exception = null); -#if DOTNET5_4 - void Warning(FormattableString message, Exception exception = null); -#endif - void Warning(Exception exception); - - void Info(string message, Exception exception = null); -#if DOTNET5_4 - void Info(FormattableString message, Exception exception = null); -#endif - void Info(Exception exception); - - void Verbose(string message, Exception exception = null); -#if DOTNET5_4 - void Verbose(FormattableString message, Exception exception = null); -#endif - void Verbose(Exception exception); - - void Debug(string message, Exception exception = null); -#if DOTNET5_4 - void Debug(FormattableString message, Exception exception = null); -#endif - void Debug(Exception exception); - } -} diff --git a/src/Discord.Net/Logging/LogManager.cs b/src/Discord.Net/Logging/LogManager.cs index 2abc4f10d..d6cc5f435 100644 --- a/src/Discord.Net/Logging/LogManager.cs +++ b/src/Discord.Net/Logging/LogManager.cs @@ -4,72 +4,61 @@ namespace Discord.Logging { public class LogManager { - private readonly DiscordClient _client; - public LogSeverity Level { get; } public event EventHandler Message = delegate { }; - internal LogManager(DiscordClient client) - { - _client = client; - Level = client.Config.LogLevel; + internal LogManager(LogSeverity minSeverity) + { + Level = minSeverity; } public void Log(LogSeverity severity, string source, string message, Exception exception = null) { if (severity <= Level) - { - try { Message(this, new LogMessageEventArgs(severity, source, message, exception)); } - catch { } //We dont want to log on log errors - } + Message(this, new LogMessageEventArgs(severity, source, message, exception)); } - -#if DOTNET5_4 public void Log(LogSeverity severity, string source, FormattableString message, Exception exception = null) { if (severity <= Level) - { - try { Message(this, new LogMessageEventArgs(severity, source, message.ToString(), exception)); } - catch { } //We dont want to log on log errors - } + Message(this, new LogMessageEventArgs(severity, source, message.ToString(), exception)); } -#endif public void Error(string source, string message, Exception ex = null) => Log(LogSeverity.Error, source, message, ex); + public void Error(string source, FormattableString message, Exception ex = null) + => Log(LogSeverity.Error, source, message, ex); public void Error(string source, Exception ex) => Log(LogSeverity.Error, source, (string)null, ex); + public void Warning(string source, string message, Exception ex = null) => Log(LogSeverity.Warning, source, message, ex); + public void Warning(string source, FormattableString message, Exception ex = null) + => Log(LogSeverity.Warning, source, message, ex); public void Warning(string source, Exception ex) => Log(LogSeverity.Warning, source, (string)null, ex); + public void Info(string source, string message, Exception ex = null) => Log(LogSeverity.Info, source, message, ex); + public void Info(string source, FormattableString message, Exception ex = null) + => Log(LogSeverity.Info, source, message, ex); public void Info(string source, Exception ex) => Log(LogSeverity.Info, source, (string)null, ex); + public void Verbose(string source, string message, Exception ex = null) => Log(LogSeverity.Verbose, source, message, ex); + public void Verbose(string source, FormattableString message, Exception ex = null) + => Log(LogSeverity.Verbose, source, message, ex); public void Verbose(string source, Exception ex) => Log(LogSeverity.Verbose, source, (string)null, ex); + public void Debug(string source, string message, Exception ex = null) => Log(LogSeverity.Debug, source, message, ex); - public void Debug(string source, Exception ex) - => Log(LogSeverity.Debug, source, (string)null, ex); - -#if DOTNET5_4 - public void Error(string source, FormattableString message, Exception ex = null) - => Log(LogSeverity.Error, source, message, ex); - public void Warning(string source, FormattableString message, Exception ex = null) - => Log(LogSeverity.Warning, source, message, ex); - public void Info(string source, FormattableString message, Exception ex = null) - => Log(LogSeverity.Info, source, message, ex); - public void Verbose(string source, FormattableString message, Exception ex = null) - => Log(LogSeverity.Verbose, source, message, ex); public void Debug(string source, FormattableString message, Exception ex = null) => Log(LogSeverity.Debug, source, message, ex); -#endif + public void Debug(string source, Exception ex) + => Log(LogSeverity.Debug, source, (string)null, ex); - public Logger CreateLogger(string name) => new Logger(this, name); + internal Logger CreateLogger(string name) => new Logger(this, name); } } diff --git a/src/Discord.Net/Logging/Logger.cs b/src/Discord.Net/Logging/Logger.cs index 59d591163..f64520591 100644 --- a/src/Discord.Net/Logging/Logger.cs +++ b/src/Discord.Net/Logging/Logger.cs @@ -2,7 +2,7 @@ namespace Discord.Logging { - public class Logger : ILogger + public class Logger { private readonly LogManager _manager; @@ -17,40 +17,42 @@ namespace Discord.Logging public void Log(LogSeverity severity, string message, Exception exception = null) => _manager.Log(severity, Name, message, exception); + public void Log(LogSeverity severity, FormattableString message, Exception exception = null) + => _manager.Log(severity, Name, message, exception); + public void Error(string message, Exception exception = null) => _manager.Error(Name, message, exception); + public void Error(FormattableString message, Exception exception = null) + => _manager.Error(Name, message, exception); public void Error(Exception exception) => _manager.Error(Name, exception); + public void Warning(string message, Exception exception = null) => _manager.Warning(Name, message, exception); + public void Warning(FormattableString message, Exception exception = null) + => _manager.Warning(Name, message, exception); public void Warning(Exception exception) => _manager.Warning(Name, exception); + public void Info(string message, Exception exception = null) => _manager.Info(Name, message, exception); + public void Info(FormattableString message, Exception exception = null) + => _manager.Info(Name, message, exception); public void Info(Exception exception) => _manager.Info(Name, exception); + public void Verbose(string message, Exception exception = null) => _manager.Verbose(Name, message, exception); + public void Verbose(FormattableString message, Exception exception = null) + => _manager.Verbose(Name, message, exception); public void Verbose(Exception exception) => _manager.Verbose(Name, exception); + public void Debug(string message, Exception exception = null) => _manager.Debug(Name, message, exception); - public void Debug(Exception exception) - => _manager.Debug(Name, exception); - -#if DOTNET5_4 - public void Log(LogSeverity severity, FormattableString message, Exception exception = null) - => _manager.Log(severity, Name, message, exception); - public void Error(FormattableString message, Exception exception = null) - => _manager.Error(Name, message, exception); - public void Warning(FormattableString message, Exception exception = null) - => _manager.Warning(Name, message, exception); - public void Info(FormattableString message, Exception exception = null) - => _manager.Info(Name, message, exception); - public void Verbose(FormattableString message, Exception exception = null) - => _manager.Verbose(Name, message, exception); public void Debug(FormattableString message, Exception exception = null) => _manager.Debug(Name, message, exception); -#endif + public void Debug(Exception exception) + => _manager.Debug(Name, exception); } } diff --git a/src/Discord.Net/MessageQueue.cs b/src/Discord.Net/MessageQueue.cs index c1a0948b0..cdbbcc251 100644 --- a/src/Discord.Net/MessageQueue.cs +++ b/src/Discord.Net/MessageQueue.cs @@ -1,5 +1,6 @@ -using Discord.API.Client.Rest; +using Discord.API.Rest; using Discord.Logging; +using Discord.Net; using Discord.Net.Rest; using System; using System.Collections.Concurrent; @@ -7,203 +8,242 @@ using System.Net; using System.Threading; using System.Threading.Tasks; -namespace Discord.Net + +namespace Discord { /// Manages an outgoing message queue for DiscordClient. - public class MessageQueue + public class MessageQueue : IDisposable { + private struct MessageSend + { + public readonly TaskCompletionSource Promise; + public readonly IMessageChannel Channel; + public readonly bool IsTTS; + public readonly string Text; + + public MessageSend(TaskCompletionSource promise, IMessageChannel channel, bool isTTS, string text) + { + Promise = promise; + Channel = channel; + IsTTS = isTTS; + Text = text; + } + } private struct MessageEdit { + public readonly TaskCompletionSource Promise; public readonly Message Message; public readonly string NewText; - public MessageEdit(Message message, string newText) + public MessageEdit(TaskCompletionSource promise, Message message, string newText) { + Promise = promise; Message = message; NewText = newText; } } + private struct MessageDelete + { + public readonly TaskCompletionSource Promise; + public readonly Message Message; - private const int WarningStart = 30; + public MessageDelete(TaskCompletionSource promise, Message message) + { + Promise = promise; + Message = message; + } + } - private readonly Random _nonceRand; + private const int WarningStart = 30; + private readonly RestClient _rest; private readonly Logger _logger; - private readonly ConcurrentQueue _pendingSends; + private readonly ConcurrentQueue _pendingSends; private readonly ConcurrentQueue _pendingEdits; - private readonly ConcurrentQueue _pendingDeletes; - private readonly ConcurrentDictionary _pendingSendsByNonce; + private readonly ConcurrentQueue _pendingDeletes; + private readonly SemaphoreSlim _connectionLock; private int _count, _nextWarning; + private Task[] _tasks; + private bool _isDisposed = false; /// Gets the current number of queued actions. public int Count => _count; - /// Gets the current number of queued sends. - public int SendCount => _pendingSends.Count; - /// Gets the current number of queued edits. - public int EditCount => _pendingEdits.Count; - /// Gets the current number of queued deletes. - public int DeleteCount => _pendingDeletes.Count; internal MessageQueue(RestClient rest, Logger logger) { _rest = rest; _logger = logger; _nextWarning = WarningStart; - - _nonceRand = new Random(); - _pendingSends = new ConcurrentQueue(); + + _connectionLock = new SemaphoreSlim(1, 1); + _pendingSends = new ConcurrentQueue(); _pendingEdits = new ConcurrentQueue(); - _pendingDeletes = new ConcurrentQueue(); - _pendingSendsByNonce = new ConcurrentDictionary(); + _pendingDeletes = new ConcurrentQueue(); } - - internal Message QueueSend(ITextChannel channel, string text, bool isTTS) - { - Message msg = new Message(0, channel, (channel as Channel).CurrentUser); - msg.IsTTS = isTTS; - msg.RawText = text; - msg.Text = Message.ResolveMentions(msg.Channel, msg.Text); - msg.Nonce = GenerateNonce(); - if (_pendingSendsByNonce.TryAdd(msg.Nonce, text)) - { - msg.State = MessageState.Queued; - IncrementCount(); - _pendingSends.Enqueue(msg); - } - else - msg.State = MessageState.Failed; - return msg; - } - internal void QueueEdit(Message msg, string text) + protected virtual void Dispose(bool disposing) { - string msgText = msg.RawText; - if (msg.State == MessageState.Queued && _pendingSendsByNonce.TryUpdate(msg.Nonce, text, msgText)) + if (!_isDisposed) { - //Successfully edited the message before it was sent. - return; + if (disposing) + _connectionLock.Dispose(); + _isDisposed = true; } - IncrementCount(); - _pendingEdits.Enqueue(new MessageEdit(msg, text)); } - internal void QueueDelete(Message msg) + public void Dispose() => Dispose(true); + + internal async Task Start(CancellationToken cancelToken) { - string ignored; - if (msg.State == MessageState.Queued && _pendingSendsByNonce.TryRemove(msg.Nonce, out ignored)) + await _connectionLock.WaitAsync().ConfigureAwait(false); + try { - //Successfully stopped the message from being sent - msg.State = MessageState.Aborted; - return; + await StartInternal(cancelToken).ConfigureAwait(false); } - IncrementCount(); - _pendingDeletes.Enqueue(msg); + finally { _connectionLock.Release(); } } - - internal Task[] Run(CancellationToken cancelToken) + internal async Task StartInternal(CancellationToken cancelToken) { - return new[] + await StopInternal().ConfigureAwait(false); + + _tasks = new Task[] { RunSendQueue(cancelToken), RunEditQueue(cancelToken), RunDeleteQueue(cancelToken) }; } + internal async Task Stop() + { + await _connectionLock.WaitAsync().ConfigureAwait(false); + try + { + await StopInternal().ConfigureAwait(false); + } + finally { _connectionLock.Release(); } + } + private async Task StopInternal() + { + if (_tasks != null) + { + await Task.WhenAll(_tasks).ConfigureAwait(false); + _tasks = null; + } + } + + internal Task QueueSend(IMessageChannel channel, string text, bool isTTS) + { + var promise = new TaskCompletionSource(); + IncrementCount(); + _pendingSends.Enqueue(new MessageSend(promise, channel, isTTS, text)); + return promise.Task; + } + internal Task QueueEdit(Message msg, string text) + { + var promise = new TaskCompletionSource(); + IncrementCount(); + _pendingEdits.Enqueue(new MessageEdit(promise, msg, text)); + return promise.Task; + } + internal Task QueueDelete(Message msg) + { + var promise = new TaskCompletionSource(); + IncrementCount(); + _pendingDeletes.Enqueue(new MessageDelete(promise, msg)); + return promise.Task; + } + private Task RunSendQueue(CancellationToken cancelToken) { return Task.Run(async () => { - while (!cancelToken.IsCancellationRequested) + try { - Message msg; - while (_pendingSends.TryDequeue(out msg)) + while (!cancelToken.IsCancellationRequested) { - DecrementCount(); - string text; - if (_pendingSendsByNonce.TryRemove(msg.Nonce, out text)) //If it was deleted from queue, this will fail + MessageSend item; + while (_pendingSends.TryDequeue(out item)) { try { - //msg.RawText = text; - //msg.Text = Message.ResolveMentions(msg.Channel, text); - var request = new SendMessageRequest(msg.Channel.Id) + var request = new CreateMessageRequest(item.Channel.Id) { - Content = text, - Nonce = msg.Nonce.ToString(), - IsTTS = msg.IsTTS + Content = item.Text, + IsTTS = item.IsTTS }; var response = await _rest.Send(request).ConfigureAwait(false); - msg.Id = response.Id; - msg.State = MessageState.Normal; - msg.Update(response); - } - catch (Exception ex) - { - msg.State = MessageState.Failed; - _logger.Error($"Failed to send message to {msg.Channel}", ex); + item.Promise.SetResult(item.Channel.Discord.CreateMessage(item.Channel, item.Channel.GetCurrentUser(), response)); } + catch (Exception ex) { item.Promise.SetException(ex); } + DecrementCount(); } + await Task.Delay(DiscordConfig.MessageQueueInterval).ConfigureAwait(false); } - await Task.Delay(DiscordConfig.MessageQueueInterval).ConfigureAwait(false); } + catch (OperationCanceledException) { } }); } private Task RunEditQueue(CancellationToken cancelToken) { return Task.Run(async () => { - while (!cancelToken.IsCancellationRequested) + try { - MessageEdit edit; - while (_pendingEdits.TryPeek(out edit) && edit.Message.State != MessageState.Queued) + while (!cancelToken.IsCancellationRequested) { - if (_pendingEdits.TryDequeue(out edit)) + MessageEdit item; + while (_pendingEdits.TryDequeue(out item)) { - DecrementCount(); - if (edit.Message.State == MessageState.Normal) - { + var msg = item.Message; + //if (msg.State != EntityState.Deleted) + //{ try { - var request = new UpdateMessageRequest(edit.Message.Channel.Id, edit.Message.Id) + var request = new UpdateMessageRequest(msg.Channel.Id, msg.Id) { - Content = edit.NewText + Content = item.NewText }; await _rest.Send(request).ConfigureAwait(false); + item.Promise.SetResult(null); } - catch (Exception ex) { _logger.Error($"Failed to edit message {edit.Message}", ex); } - } + catch (Exception ex) { item.Promise.SetException(ex); } + //} + DecrementCount(); } + await Task.Delay(DiscordConfig.MessageQueueInterval).ConfigureAwait(false); } - await Task.Delay(DiscordConfig.MessageQueueInterval).ConfigureAwait(false); } + catch (OperationCanceledException) { } }); } private Task RunDeleteQueue(CancellationToken cancelToken) { return Task.Run(async () => { - while (!cancelToken.IsCancellationRequested) + try { - Message msg; - while (_pendingDeletes.TryPeek(out msg) && msg.State != MessageState.Queued) + while (!cancelToken.IsCancellationRequested) { - if (_pendingDeletes.TryDequeue(out msg)) + MessageDelete item; + while (_pendingDeletes.TryDequeue(out item)) { - DecrementCount(); - if (msg.State == MessageState.Normal) - { + var msg = item.Message; + //if (msg.State != EntityState.Deleted) + //{ try { var request = new DeleteMessageRequest(msg.Channel.Id, msg.Id); await _rest.Send(request).ConfigureAwait(false); - msg.State = MessageState.Deleted; + item.Promise.SetResult(null); } catch (HttpException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { } //Ignore - catch (Exception ex) { _logger.Error($"Failed to delete message {msg}", ex); } - } + catch (Exception ex) { item.Promise.SetException(ex); } + //} + DecrementCount(); } - } - await Task.Delay(Discord.DiscordConfig.MessageQueueInterval).ConfigureAwait(false); + await Task.Delay(DiscordConfig.MessageQueueInterval).ConfigureAwait(false); + } } + catch (OperationCanceledException) { } }); } @@ -232,22 +272,16 @@ namespace Discord.Net /// Clears all queued message sends/edits/deletes. public void Clear() { - Message msg; + MessageSend send; MessageEdit edit; + MessageDelete delete; - while (_pendingSends.TryDequeue(out msg)) + while (_pendingSends.TryDequeue(out send)) DecrementCount(); while (_pendingEdits.TryDequeue(out edit)) DecrementCount(); - while (_pendingDeletes.TryDequeue(out msg)) + while (_pendingDeletes.TryDequeue(out delete)) DecrementCount(); - _pendingSendsByNonce.Clear(); - } - - private int GenerateNonce() - { - lock (_nonceRand) - return _nonceRand.Next(1, int.MaxValue); } } } diff --git a/src/Discord.Net/Net/HttpException.cs b/src/Discord.Net/Net/HttpException.cs index 306122ba3..6f53fae19 100644 --- a/src/Discord.Net/Net/HttpException.cs +++ b/src/Discord.Net/Net/HttpException.cs @@ -1,23 +1,17 @@ -using System; +#pragma warning disable CA1032, CA2237 +using System; using System.Net; namespace Discord.Net { -#if NET46 - [Serializable] -#endif - public class HttpException : Exception - { - public HttpStatusCode StatusCode { get; } - - public HttpException(HttpStatusCode statusCode) - : base($"The server responded with error {(int)statusCode} ({statusCode})") - { - StatusCode = statusCode; + public class HttpException : Exception + { + public HttpStatusCode StatusCode { get; } + + public HttpException(HttpStatusCode statusCode) + : base($"The server responded with error {(int)statusCode} ({statusCode})") + { + StatusCode = statusCode; } -#if NET46 - public override void GetObjectData(SerializationInfo info, StreamingContext context) - => base.GetObjectData(info, context); -#endif } } diff --git a/src/Discord.Net/Net/JsonConverters/ChannelTypeConverter.cs b/src/Discord.Net/Net/JsonConverters/ChannelTypeConverter.cs new file mode 100644 index 000000000..4299df7bf --- /dev/null +++ b/src/Discord.Net/Net/JsonConverters/ChannelTypeConverter.cs @@ -0,0 +1,40 @@ +using Newtonsoft.Json; +using System; + +namespace Discord.Net.JsonConverters +{ + public class ChannelTypeConverter : JsonConverter + { + public override bool CanConvert(Type objectType) => objectType == typeof(ChannelType); + public override bool CanRead => true; + public override bool CanWrite => true; + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + switch ((string)reader.Value) + { + case "text": + return ChannelType.Text; + case "voice": + return ChannelType.Voice; + default: + throw new JsonSerializationException("Unknown channel type"); + } + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + switch ((ChannelType)value) + { + case ChannelType.Text: + writer.WriteValue("text"); + break; + case ChannelType.Voice: + writer.WriteValue("voice"); + break; + default: + throw new JsonSerializationException("Invalid channel type"); + } + } + } +} diff --git a/src/Discord.Net/Net/JsonConverters/ImageConverter.cs b/src/Discord.Net/Net/JsonConverters/ImageConverter.cs new file mode 100644 index 000000000..bdbfa6915 --- /dev/null +++ b/src/Discord.Net/Net/JsonConverters/ImageConverter.cs @@ -0,0 +1,29 @@ +using Newtonsoft.Json; +using System; +using System.IO; + +namespace Discord.Net.JsonConverters +{ + public class ImageConverter : JsonConverter + { + public override bool CanConvert(Type objectType) => objectType == typeof(Stream); + public override bool CanRead => true; + public override bool CanWrite => true; + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + throw new InvalidOperationException(); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + var stream = value as Stream; + + byte[] bytes = new byte[stream.Length - stream.Position]; + stream.Read(bytes, 0, bytes.Length); + + string base64 = Convert.ToBase64String(bytes); + writer.WriteValue($"data:image/jpeg;base64,{base64}"); + } + } +} diff --git a/src/Discord.Net/Net/JsonConverters/NullableUInt64Converter.cs b/src/Discord.Net/Net/JsonConverters/NullableUInt64Converter.cs new file mode 100644 index 000000000..33c7c8ce7 --- /dev/null +++ b/src/Discord.Net/Net/JsonConverters/NullableUInt64Converter.cs @@ -0,0 +1,30 @@ +using Newtonsoft.Json; +using System; +using System.Globalization; + +namespace Discord.Net.JsonConverters +{ + public class NullableUInt64Converter : JsonConverter + { + public override bool CanConvert(Type objectType) => objectType == typeof(ulong?); + public override bool CanRead => true; + public override bool CanWrite => true; + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + object value = reader.Value; + if (value != null) + return ulong.Parse((string)value, NumberStyles.None, CultureInfo.InvariantCulture); + else + return null; + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value != null) + writer.WriteValue(((ulong?)value).Value.ToString(CultureInfo.InvariantCulture)); + else + writer.WriteNull(); + } + } +} diff --git a/src/Discord.Net/Net/JsonConverters/PermissionTargetConverter.cs b/src/Discord.Net/Net/JsonConverters/PermissionTargetConverter.cs new file mode 100644 index 000000000..21a8ed8db --- /dev/null +++ b/src/Discord.Net/Net/JsonConverters/PermissionTargetConverter.cs @@ -0,0 +1,40 @@ +using Newtonsoft.Json; +using System; + +namespace Discord.Net.JsonConverters +{ + public class PermissionTargetConverter : JsonConverter + { + public override bool CanConvert(Type objectType) => objectType == typeof(PermissionTarget); + public override bool CanRead => true; + public override bool CanWrite => true; + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + switch ((string)reader.Value) + { + case "member": + return PermissionTarget.User; + case "role": + return PermissionTarget.Role; + default: + throw new JsonSerializationException("Unknown permission target"); + } + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + switch ((PermissionTarget)value) + { + case PermissionTarget.User: + writer.WriteValue("member"); + break; + case PermissionTarget.Role: + writer.WriteValue("role"); + break; + default: + throw new JsonSerializationException("Invalid permission target"); + } + } + } +} diff --git a/src/Discord.Net/Net/JsonConverters/StringEntityConverter.cs b/src/Discord.Net/Net/JsonConverters/StringEntityConverter.cs new file mode 100644 index 000000000..b19b5977b --- /dev/null +++ b/src/Discord.Net/Net/JsonConverters/StringEntityConverter.cs @@ -0,0 +1,25 @@ +using Newtonsoft.Json; +using System; + +namespace Discord.Net.JsonConverters +{ + public class StringEntityConverter : JsonConverter + { + public override bool CanConvert(Type objectType) => objectType == typeof(IEntity); + public override bool CanRead => false; + public override bool CanWrite => true; + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + throw new InvalidOperationException(); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value != null) + writer.WriteValue((value as IEntity).Id); + else + writer.WriteNull(); + } + } +} diff --git a/src/Discord.Net/Net/JsonConverters/UInt64ArrayConverter.cs b/src/Discord.Net/Net/JsonConverters/UInt64ArrayConverter.cs new file mode 100644 index 000000000..4b03f6fa3 --- /dev/null +++ b/src/Discord.Net/Net/JsonConverters/UInt64ArrayConverter.cs @@ -0,0 +1,43 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace Discord.Net.JsonConverters +{ + internal class UInt64ArrayConverter : JsonConverter + { + public override bool CanConvert(Type objectType) => objectType == typeof(IEnumerable); + public override bool CanRead => true; + public override bool CanWrite => true; + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var result = new List(); + if (reader.TokenType == JsonToken.StartArray) + { + reader.Read(); + while (reader.TokenType != JsonToken.EndArray) + { + ulong id = ulong.Parse((string)reader.Value, NumberStyles.None, CultureInfo.InvariantCulture); + result.Add(id); + reader.Read(); + } + } + return result.ToArray(); + } + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value != null) + { + writer.WriteStartArray(); + var a = (ulong[])value; + for (int i = 0; i < a.Length; i++) + writer.WriteValue(a[i].ToString(CultureInfo.InvariantCulture)); + writer.WriteEndArray(); + } + else + writer.WriteNull(); + } + } +} diff --git a/src/Discord.Net/Net/JsonConverters/UInt64Converter.cs b/src/Discord.Net/Net/JsonConverters/UInt64Converter.cs new file mode 100644 index 000000000..49c21564a --- /dev/null +++ b/src/Discord.Net/Net/JsonConverters/UInt64Converter.cs @@ -0,0 +1,23 @@ +using Newtonsoft.Json; +using System; +using System.Globalization; + +namespace Discord.Net.JsonConverters +{ + public class UInt64Converter : JsonConverter + { + public override bool CanConvert(Type objectType) => objectType == typeof(ulong); + public override bool CanRead => true; + public override bool CanWrite => true; + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + return ulong.Parse((string)reader.Value, NumberStyles.None, CultureInfo.InvariantCulture); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + writer.WriteValue(((ulong)value).ToString(CultureInfo.InvariantCulture)); + } + } +} diff --git a/src/Discord.Net/Net/JsonConverters/UInt64EntityConverter.cs b/src/Discord.Net/Net/JsonConverters/UInt64EntityConverter.cs new file mode 100644 index 000000000..84945a672 --- /dev/null +++ b/src/Discord.Net/Net/JsonConverters/UInt64EntityConverter.cs @@ -0,0 +1,25 @@ +using Newtonsoft.Json; +using System; + +namespace Discord.Net.JsonConverters +{ + public class UInt64EntityConverter : JsonConverter + { + public override bool CanConvert(Type objectType) => objectType == typeof(IEntity); + public override bool CanRead => false; + public override bool CanWrite => true; + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + throw new InvalidOperationException(); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value != null) + writer.WriteValue((value as IEntity).Id); + else + writer.WriteNull(); + } + } +} diff --git a/src/Discord.Net/Net/JsonConverters/UserStatusConverter.cs b/src/Discord.Net/Net/JsonConverters/UserStatusConverter.cs new file mode 100644 index 000000000..a60d75f75 --- /dev/null +++ b/src/Discord.Net/Net/JsonConverters/UserStatusConverter.cs @@ -0,0 +1,45 @@ +using Newtonsoft.Json; +using System; + +namespace Discord.Net.JsonConverters +{ + public class UserStatusConverter : JsonConverter + { + public override bool CanConvert(Type objectType) => objectType == typeof(UserStatus); + public override bool CanRead => true; + public override bool CanWrite => true; + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + switch ((string)reader.Value) + { + case "online": + return UserStatus.Online; + case "idle": + return UserStatus.Idle; + case "offline": + return UserStatus.Offline; + default: + throw new JsonSerializationException("Unknown user status"); + } + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + switch ((UserStatus)value) + { + case UserStatus.Online: + writer.WriteValue("online"); + break; + case UserStatus.Idle: + writer.WriteValue("idle"); + break; + case UserStatus.Offline: + writer.WriteValue("offline"); + break; + default: + throw new JsonSerializationException("Invalid user status"); + } + } + } +} diff --git a/src/Discord.Net/Net/Rest/BuiltInEngine.cs b/src/Discord.Net/Net/Rest/BuiltInEngine.cs deleted file mode 100644 index 83d12ec50..000000000 --- a/src/Discord.Net/Net/Rest/BuiltInEngine.cs +++ /dev/null @@ -1,145 +0,0 @@ -#if DOTNET5_4 -using Discord.Logging; -using System; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using System.Net.Http; -using System.Net; -using System.Text; -using System.Globalization; -using Nito.AsyncEx; - -namespace Discord.Net.Rest -{ - internal class BuiltInEngine : IRestEngine - { - private const int HR_SECURECHANNELFAILED = -2146233079; - - private readonly DiscordConfig _config; - private readonly HttpClient _client; - private readonly string _baseUrl; - - private readonly AsyncLock _rateLimitLock; - private readonly ILogger _logger; - private DateTime _rateLimitTime; - - - public BuiltInEngine(DiscordConfig config, string baseUrl, ILogger logger) - { - _config = config; - _baseUrl = baseUrl; - _logger = logger; - - _rateLimitLock = new AsyncLock(); - _client = new HttpClient(new HttpClientHandler - { - AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip, - UseCookies = false, - UseProxy = false, - PreAuthenticate = false //We do auth ourselves - }); - _client.DefaultRequestHeaders.Add("accept", "*/*"); - _client.DefaultRequestHeaders.Add("accept-encoding", "gzip,deflate"); - _client.DefaultRequestHeaders.Add("user-agent", config.UserAgent); - } - - public void SetToken(string token) - { - _client.DefaultRequestHeaders.Remove("authorization"); - if (token != null) - _client.DefaultRequestHeaders.Add("authorization", token); - } - - public async Task Send(string method, string path, string json, CancellationToken cancelToken) - { - using (var request = new HttpRequestMessage(GetMethod(method), _baseUrl + path)) - { - if (json != null) - request.Content = new StringContent(json, Encoding.UTF8, "application/json"); - return await Send(request, cancelToken).ConfigureAwait(false); - } - } - public async Task SendFile(string method, string path, string filename, Stream stream, CancellationToken cancelToken) - { - using (var request = new HttpRequestMessage(GetMethod(method), _baseUrl + path)) - { - var content = new MultipartFormDataContent("Upload----" + DateTime.Now.ToString(CultureInfo.InvariantCulture)); - content.Add(new StreamContent(File.OpenRead(path)), "file", filename); - request.Content = content; - return await Send(request, cancelToken).ConfigureAwait(false); - } - } - private async Task Send(HttpRequestMessage request, CancellationToken cancelToken) - { - int retryCount = 0; - while (true) - { - HttpResponseMessage response; - try - { - response = await _client.SendAsync(request, cancelToken).ConfigureAwait(false); - } - catch (WebException ex) - { - //The request was aborted: Could not create SSL/TLS secure channel. - if (ex.HResult == HR_SECURECHANNELFAILED && retryCount++ < 5) - continue; //Retrying seems to fix this somehow? - throw; - } - - int statusCode = (int)response.StatusCode; - if (statusCode == 429) //Rate limit - { - var retryAfter = response.Headers - .Where(x => x.Key.Equals("Retry-After", StringComparison.OrdinalIgnoreCase)) - .Select(x => x.Value.FirstOrDefault()) - .FirstOrDefault(); - - int milliseconds; - if (retryAfter != null && int.TryParse(retryAfter, out milliseconds)) - { - if (_logger != null) - { - var now = DateTime.UtcNow; - if (now >= _rateLimitTime) - { - using (await _rateLimitLock.LockAsync().ConfigureAwait(false)) - { - if (now >= _rateLimitTime) - { - _rateLimitTime = now.AddMilliseconds(milliseconds); - _logger.Warning($"Rate limit hit, waiting {Math.Round(milliseconds / 1000.0f, 2)} seconds"); - } - } - } - } - await Task.Delay(milliseconds, cancelToken).ConfigureAwait(false); - continue; - } - throw new HttpException(response.StatusCode); - } - else if (statusCode < 200 || statusCode >= 300) //2xx = Success - throw new HttpException(response.StatusCode); - else - return await response.Content.ReadAsStringAsync().ConfigureAwait(false); - } - } - - private static readonly HttpMethod _patch = new HttpMethod("PATCH"); - private HttpMethod GetMethod(string method) - { - switch (method) - { - case "DELETE": return HttpMethod.Delete; - case "GET": return HttpMethod.Get; - case "PATCH": return _patch; - case "POST": return HttpMethod.Post; - case "PUT": return HttpMethod.Put; - default: throw new InvalidOperationException($"Unknown HttpMethod: {method}"); - } - } - } -} -#endif \ No newline at end of file diff --git a/src/Discord.Net/Net/Rest/CompletedRequestEventArgs.cs b/src/Discord.Net/Net/Rest/CompletedRequestEventArgs.cs deleted file mode 100644 index 1bee431b0..000000000 --- a/src/Discord.Net/Net/Rest/CompletedRequestEventArgs.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Discord.API; - -namespace Discord.Net.Rest -{ - public class CompletedRequestEventArgs : RequestEventArgs - { - public object Response { get; set; } - public string ResponseJson { get; set; } - public double Milliseconds { get; set; } - - public CompletedRequestEventArgs(IRestRequest request, object response, string responseJson, double milliseconds) - : base(request) - { - Response = response; - ResponseJson = responseJson; - Milliseconds = milliseconds; - } - } -} diff --git a/src/Discord.Net/Net/Rest/DefaultRestEngine.cs b/src/Discord.Net/Net/Rest/DefaultRestEngine.cs new file mode 100644 index 000000000..84ddfbbeb --- /dev/null +++ b/src/Discord.Net/Net/Rest/DefaultRestEngine.cs @@ -0,0 +1,127 @@ +using Newtonsoft.Json; +using System; +using System.Globalization; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord.Net.Rest +{ + public class DefaultRestEngine : IRestEngine + { + private const int HR_SECURECHANNELFAILED = -2146233079; + + protected readonly HttpClient _client; + protected readonly string _baseUrl; + protected readonly CancellationToken _cancelToken; + protected bool _isDisposed; + + public DefaultRestEngine(string baseUrl, CancellationToken cancelToken) + { + _baseUrl = baseUrl; + _cancelToken = cancelToken; + + _client = new HttpClient(new HttpClientHandler + { + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, + UseCookies = false, + UseProxy = false, + PreAuthenticate = false + }); + SetHeader("accept-encoding", "gzip,deflate"); + } + protected virtual void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + _client.Dispose(); + _isDisposed = true; + } + } + public void Dispose() + { + Dispose(true); + } + + public void SetHeader(string key, string value) + { + _client.DefaultRequestHeaders.Remove(key); + _client.DefaultRequestHeaders.Add(key, value); + } + + public async Task Send(IRestRequest request) + { + string uri = Path.Combine(_baseUrl, request.Endpoint); + using (var restRequest = new HttpRequestMessage(GetMethod(request.Method), uri)) + { + object payload = request.Payload; + if (payload != null) + restRequest.Content = new StringContent(JsonConvert.SerializeObject(payload), Encoding.UTF8, "application/json"); + return await SendInternal(restRequest, _cancelToken).ConfigureAwait(false); + } + } + + public async Task Send(IRestFileRequest request) + { + string uri = Path.Combine(_baseUrl, request.Endpoint); + using (var restRequest = new HttpRequestMessage(GetMethod(request.Method), uri)) + { + var content = new MultipartFormDataContent("Upload----" + DateTime.Now.ToString(CultureInfo.InvariantCulture)); + var mpParameters = request.MultipartParameters; + if (mpParameters != null) + { + foreach (var p in mpParameters) + content.Add(new StringContent(p.Value), p.Key); + + } + content.Add(new StreamContent(request.Stream), "file", request.Filename); + restRequest.Content = content; + return await SendInternal(restRequest, _cancelToken).ConfigureAwait(false); + } + } + + private async Task SendInternal(HttpRequestMessage request, CancellationToken cancelToken) + { + int retryCount = 0; + while (true) + { + HttpResponseMessage response; + try + { + response = await _client.SendAsync(request, cancelToken).ConfigureAwait(false); + } + catch (WebException ex) + { + //The request was aborted: Could not create SSL/TLS secure channel. + if (ex.HResult == HR_SECURECHANNELFAILED && retryCount++ < 5) + continue; //Retrying seems to fix this somehow? + throw; + } + + int statusCode = (int)response.StatusCode; + if (statusCode < 200 || statusCode >= 300) //2xx = Success + throw new HttpException(response.StatusCode); + + return await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + } + } + + private static readonly HttpMethod _patch = new HttpMethod("PATCH"); + private HttpMethod GetMethod(string method) + { + switch (method) + { + case "DELETE": return HttpMethod.Delete; + case "GET": return HttpMethod.Get; + case "PATCH": return _patch; + case "POST": return HttpMethod.Post; + case "PUT": return HttpMethod.Put; + default: throw new ArgumentOutOfRangeException(nameof(method), $"Unknown HttpMethod: {method}"); + } + } + } +} diff --git a/src/Discord.Net/Net/Rest/ETFRestClient.cs b/src/Discord.Net/Net/Rest/ETFRestClient.cs deleted file mode 100644 index 5a2620e89..000000000 --- a/src/Discord.Net/Net/Rest/ETFRestClient.cs +++ /dev/null @@ -1,27 +0,0 @@ -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, ILogger logger = null) - : 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/IRestEngine.cs b/src/Discord.Net/Net/Rest/IRestEngine.cs index faba37086..e086d79a0 100644 --- a/src/Discord.Net/Net/Rest/IRestEngine.cs +++ b/src/Discord.Net/Net/Rest/IRestEngine.cs @@ -1,13 +1,14 @@ -using System.IO; -using System.Threading; +using System; +using System.IO; using System.Threading.Tasks; namespace Discord.Net.Rest { - internal interface IRestEngine - { - void SetToken(string token); - Task Send(string method, string path, string json, CancellationToken cancelToken); - Task SendFile(string method, string path, string filename, Stream stream, CancellationToken cancelToken); - } + public interface IRestEngine : IDisposable + { + void SetHeader(string key, string value); + + Task Send(IRestRequest request); + Task Send(IRestFileRequest request); + } } diff --git a/ref/Net/Rest/IRestRequest.cs b/src/Discord.Net/Net/Rest/IRestRequest.cs similarity index 53% rename from ref/Net/Rest/IRestRequest.cs rename to src/Discord.Net/Net/Rest/IRestRequest.cs index 9d46e645f..0ec7b9a32 100644 --- a/ref/Net/Rest/IRestRequest.cs +++ b/src/Discord.Net/Net/Rest/IRestRequest.cs @@ -1,4 +1,5 @@ -using System.IO; +using System.Collections.Generic; +using System.IO; namespace Discord.Net.Rest { @@ -8,8 +9,8 @@ namespace Discord.Net.Rest string Endpoint { get; } object Payload { get; } } - public interface IRestRequest : IRestRequest - where ResponseT : class + public interface IRestRequest : IRestRequest + where TResponse : class { } @@ -17,9 +18,10 @@ namespace Discord.Net.Rest { string Filename { get; } Stream Stream { get; } + IReadOnlyList MultipartParameters { get; } } - public interface IRestFileRequest : IRestFileRequest, IRestRequest - where ResponseT : class + public interface IRestFileRequest : IRestFileRequest, IRestRequest + where TResponse : class { } } diff --git a/src/Discord.Net/Net/Rest/JsonRestClient.cs b/src/Discord.Net/Net/Rest/JsonRestClient.cs deleted file mode 100644 index ac18ac823..000000000 --- a/src/Discord.Net/Net/Rest/JsonRestClient.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Discord.Logging; -using Newtonsoft.Json; -#if TEST_RESPONSES -using System; -#endif -using System.IO; - -namespace Discord.Net.Rest -{ - public class JsonRestClient : RestClient - { - private JsonSerializer _serializer; - - public JsonRestClient(DiscordConfig config, string baseUrl, ILogger logger = null) - : base(config, baseUrl, logger) - { - _serializer = new JsonSerializer(); -#if TEST_RESPONSES - _serializer.CheckAdditionalContent = true; - _serializer.MissingMemberHandling = MissingMemberHandling.Error; -#else - _serializer.CheckAdditionalContent = false; - _serializer.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 - using (var reader = new JsonTextReader(new StringReader(json))) - return (T)_serializer.Deserialize(reader, typeof(T)); - } - } -} diff --git a/src/Discord.Net/Net/Rest/RequestEventArgs.cs b/src/Discord.Net/Net/Rest/RequestEventArgs.cs deleted file mode 100644 index ce6dadd83..000000000 --- a/src/Discord.Net/Net/Rest/RequestEventArgs.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Discord.API; -using System; - -namespace Discord.Net.Rest -{ - public class RequestEventArgs : EventArgs - { - public IRestRequest Request { get; set; } - public bool Cancel { get; set; } - - public RequestEventArgs(IRestRequest request) - { - Request = request; - } - } -} diff --git a/src/Discord.Net/Net/Rest/RestClient.cs b/src/Discord.Net/Net/Rest/RestClient.cs index 47853f8a4..592a99d3d 100644 --- a/src/Discord.Net/Net/Rest/RestClient.cs +++ b/src/Discord.Net/Net/Rest/RestClient.cs @@ -1,140 +1,98 @@ -using Discord.API; -using Discord.ETF; -using Discord.Logging; +using Discord.Net.JsonConverters; +using Newtonsoft.Json; using System; using System.Diagnostics; -using System.Threading; +using System.IO; using System.Threading.Tasks; namespace Discord.Net.Rest { - public abstract partial class RestClient - { - private struct RestResults - { - public string Response { get; set; } - public double Milliseconds { get; set; } - - public RestResults(string response, double milliseconds) - { - Response = response; - Milliseconds = milliseconds; - } - } - - public event EventHandler SendingRequest = delegate { }; - public event EventHandler SentRequest = delegate { }; + public class RestClient + { + internal event EventHandler SentRequest; + + private readonly IRestEngine _engine; + private readonly JsonSerializer _serializer; - private bool OnSendingRequest(IRestRequest request) + internal RestClient(IRestEngine engine) { - var eventArgs = new RequestEventArgs(request); - SendingRequest(this, eventArgs); - return !eventArgs.Cancel; + _engine = engine; + _serializer = new JsonSerializer(); + _serializer.Converters.Add(new ChannelTypeConverter()); + _serializer.Converters.Add(new ImageConverter()); + _serializer.Converters.Add(new NullableUInt64Converter()); + _serializer.Converters.Add(new PermissionTargetConverter()); + _serializer.Converters.Add(new StringEntityConverter()); + _serializer.Converters.Add(new UInt64ArrayConverter()); + _serializer.Converters.Add(new UInt64Converter()); + _serializer.Converters.Add(new UInt64EntityConverter()); + _serializer.Converters.Add(new UserStatusConverter()); } - private void OnSentRequest(IRestRequest request, object response, string responseJson, double milliseconds) - => SentRequest(this, new CompletedRequestEventArgs(request, response, responseJson, milliseconds)); - - private readonly DiscordConfig _config; - private readonly IRestEngine _engine; - private readonly ETFWriter _serializer; - private readonly ILogger _logger; - private string _token; + public void Dispose() => _engine.Dispose(); - public CancellationToken CancelToken { get; set; } + public void SetHeader(string key, string value) => _engine.SetHeader(key, value); - public string Token + public async Task Send(IRestRequest request) + where TResponse : class { - get { return _token; } - set - { - _token = value; - _engine.SetToken(value); - } - } - - protected RestClient(DiscordConfig config, string baseUrl, ILogger logger = null) - { - _config = config; - _logger = logger; - -#if !DOTNET5_4 - _engine = new RestSharpEngine(config, baseUrl, logger); -#else - _engine = new BuiltInEngine(config, baseUrl, logger); -#endif - - if (logger != null && logger.Level >= LogSeverity.Verbose) - SentRequest += (s, e) => _logger.Verbose($"{e.Request.Method} {e.Request.Endpoint}: {e.Milliseconds} ms"); - } - - public async Task Send(IRestRequest request) - where ResponseT : class - { if (request == null) throw new ArgumentNullException(nameof(request)); + + var stopwatch = Stopwatch.StartNew(); + Stream response = await _engine.Send(request).ConfigureAwait(false); + TResponse responseObj = Deserialize(response); + stopwatch.Stop(); - if (!OnSendingRequest(request)) throw new OperationCanceledException(); - var results = await Send(request, true).ConfigureAwait(false); - var response = Deserialize(results.Response); - OnSentRequest(request, response, results.Response, results.Milliseconds); - - return response; + SentRequest(this, new SentRequestEventArgs(request, responseObj, ToMilliseconds(stopwatch))); + return responseObj; } public async Task Send(IRestRequest request) { if (request == null) throw new ArgumentNullException(nameof(request)); - if (!OnSendingRequest(request)) throw new OperationCanceledException(); - var results = await Send(request, false).ConfigureAwait(false); - OnSentRequest(request, null, null, results.Milliseconds); + var stopwatch = Stopwatch.StartNew(); + await _engine.Send(request).ConfigureAwait(false); + stopwatch.Stop(); + + SentRequest(this, new SentRequestEventArgs(request, null, ToMilliseconds(stopwatch))); } - public async Task Send(IRestFileRequest request) - where ResponseT : class + public async Task Send(IRestFileRequest request) + where TResponse : class { if (request == null) throw new ArgumentNullException(nameof(request)); - if (!OnSendingRequest(request)) throw new OperationCanceledException(); - var results = await SendFile(request, true).ConfigureAwait(false); - var response = Deserialize(results.Response); - OnSentRequest(request, response, results.Response, results.Milliseconds); + var stopwatch = Stopwatch.StartNew(); + Stream response = await _engine.Send(request).ConfigureAwait(false); + TResponse responseObj = Deserialize(response); + stopwatch.Stop(); - return response; + SentRequest(this, new SentRequestEventArgs(request, responseObj, ToMilliseconds(stopwatch))); + return responseObj; } public async Task Send(IRestFileRequest request) { if (request == null) throw new ArgumentNullException(nameof(request)); - if (!OnSendingRequest(request)) throw new OperationCanceledException(); - var results = await SendFile(request, false).ConfigureAwait(false); - OnSentRequest(request, null, null, results.Milliseconds); - } - - private async Task Send(IRestRequest request, bool hasResponse) - { - object payload = request.Payload; - string requestJson = null; - if (payload != null) - requestJson = Serialize(payload); - - Stopwatch stopwatch = Stopwatch.StartNew(); - string responseJson = await _engine.Send(request.Method, request.Endpoint, requestJson, CancelToken).ConfigureAwait(false); + var stopwatch = Stopwatch.StartNew(); + await _engine.Send(request).ConfigureAwait(false); stopwatch.Stop(); - double milliseconds = Math.Round((double)stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0, 2); - return new RestResults(responseJson, milliseconds); - } - - private async Task SendFile(IRestFileRequest request, bool hasResponse) - { - Stopwatch stopwatch = Stopwatch.StartNew(); - string responseJson = await _engine.SendFile(request.Method, request.Endpoint, request.Filename, request.Stream, CancelToken).ConfigureAwait(false); - stopwatch.Stop(); + SentRequest(this, new SentRequestEventArgs(request, null, ToMilliseconds(stopwatch))); + } - double milliseconds = Math.Round((double)stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0, 2); - return new RestResults(responseJson, milliseconds); + private void Serialize(Stream stream, T value) + { + using (TextWriter text = new StreamWriter(stream)) + using (JsonWriter writer = new JsonTextWriter(text)) + _serializer.Serialize(writer, value, typeof(T)); + } + private T Deserialize(Stream stream) + { + using (TextReader text = new StreamReader(stream)) + using (JsonReader reader = new JsonTextReader(text)) + return _serializer.Deserialize(reader); } - protected abstract string Serialize(T obj); - protected abstract T Deserialize(string json); - } + private static double ToMilliseconds(Stopwatch stopwatch) => Math.Round((double)stopwatch.ElapsedTicks / (double)Stopwatch.Frequency * 1000.0, 2); + } } diff --git a/src/Discord.Net/Net/Rest/RestClientProvider.cs b/src/Discord.Net/Net/Rest/RestClientProvider.cs new file mode 100644 index 000000000..942720ffb --- /dev/null +++ b/src/Discord.Net/Net/Rest/RestClientProvider.cs @@ -0,0 +1,6 @@ +using System.Threading; + +namespace Discord.Net.Rest +{ + public delegate IRestEngine RestClientProvider(string baseUrl, CancellationToken cancelToken); +} diff --git a/src/Discord.Net/Net/Rest/RestParameter.cs b/src/Discord.Net/Net/Rest/RestParameter.cs new file mode 100644 index 000000000..5fac47bf8 --- /dev/null +++ b/src/Discord.Net/Net/Rest/RestParameter.cs @@ -0,0 +1,19 @@ +namespace Discord.Net.Rest +{ + public struct RestParameter + { + public string Key { get; } + public string Value { get; } + + public RestParameter(string key, string value) + { + Key = key; + Value = value; + } + public RestParameter(string key, object value) + { + Key = key; + Value = value.ToString(); + } + } +} diff --git a/src/Discord.Net/Net/Rest/SentRequestEventArgs.cs b/src/Discord.Net/Net/Rest/SentRequestEventArgs.cs new file mode 100644 index 000000000..25f433d25 --- /dev/null +++ b/src/Discord.Net/Net/Rest/SentRequestEventArgs.cs @@ -0,0 +1,16 @@ +namespace Discord.Net.Rest +{ + public class SentRequestEventArgs + { + public IRestRequest Request { get; } + public object Response { get; } + public double Milliseconds { get; } + + public SentRequestEventArgs(IRestRequest request, object response, double milliseconds) + { + Request = request; + Response = response; + Milliseconds = milliseconds; + } + } +} diff --git a/src/Discord.Net/Net/Rest/SharpRestEngine.cs b/src/Discord.Net/Net/Rest/SharpRestEngine.cs deleted file mode 100644 index f9c54064b..000000000 --- a/src/Discord.Net/Net/Rest/SharpRestEngine.cs +++ /dev/null @@ -1,132 +0,0 @@ -#if !DOTNET5_4 -using Discord.Logging; -using Nito.AsyncEx; -using RestSharp; -using System; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using RestSharpClient = RestSharp.RestClient; - -namespace Discord.Net.Rest -{ - internal class RestSharpEngine : IRestEngine - { - private const int HR_SECURECHANNELFAILED = -2146233079; - - private readonly DiscordConfig _config; - private readonly RestSharpClient _client; - - private readonly AsyncLock _rateLimitLock; - private readonly ILogger _logger; - private DateTime _rateLimitTime; - - public RestSharpEngine(DiscordConfig config, string baseUrl, ILogger logger) - { - _config = config; - _logger = logger; - - _rateLimitLock = new AsyncLock(); - _client = new RestSharpClient(baseUrl) - { - PreAuthenticate = false, - ReadWriteTimeout = DiscordConfig.RestTimeout, - UserAgent = config.UserAgent - }; - _client.Proxy = null; - _client.RemoveDefaultParameter("Accept"); - _client.AddDefaultHeader("accept", "*/*"); - _client.AddDefaultHeader("accept-encoding", "gzip,deflate"); - } - - public void SetToken(string token) - { - _client.RemoveDefaultParameter("authorization"); - if (token != null) - _client.AddDefaultHeader("authorization", token); - } - - public Task Send(string method, string path, string json, CancellationToken cancelToken) - { - var request = new RestRequest(path, GetMethod(method)); - if (json != null) - request.AddParameter("application/json", json, ParameterType.RequestBody); - return Send(request, cancelToken); - } - public Task SendFile(string method, string path, string filename, Stream stream, CancellationToken cancelToken) - { - var request = new RestRequest(path, GetMethod(method)); - request.AddHeader("content-length", (stream.Length - stream.Position).ToString()); - - byte[] bytes = new byte[stream.Length - stream.Position]; - stream.Read(bytes, 0, bytes.Length); - request.AddFileBytes("file", bytes, filename); - //request.AddFile("file", x => stream.CopyTo(x), filename); (Broken in latest ver) - - return Send(request, cancelToken); - } - private async Task Send(RestRequest request, CancellationToken cancelToken) - { - int retryCount = 0; - while (true) - { - var response = await _client.ExecuteTaskAsync(request, cancelToken).ConfigureAwait(false); - int statusCode = (int)response.StatusCode; - if (statusCode == 0) //Internal Error - { - //The request was aborted: Could not create SSL/TLS secure channel. - if (response.ErrorException.HResult == HR_SECURECHANNELFAILED && retryCount++ < 5) - continue; //Retrying seems to fix this somehow? - throw response.ErrorException; - } - else if (statusCode == 429) //Rate limit - { - var retryAfter = response.Headers - .FirstOrDefault(x => x.Name.Equals("Retry-After", StringComparison.OrdinalIgnoreCase)); - - int milliseconds; - if (retryAfter != null && int.TryParse((string)retryAfter.Value, out milliseconds)) - { - if (_logger != null) - { - var now = DateTime.UtcNow; - if (now >= _rateLimitTime) - { - using (await _rateLimitLock.LockAsync().ConfigureAwait(false)) - { - if (now >= _rateLimitTime) - { - _rateLimitTime = now.AddMilliseconds(milliseconds); - _logger.Warning($"Rate limit hit, waiting {Math.Round(milliseconds / 1000.0f, 2)} seconds"); - } - } - } - } - await Task.Delay(milliseconds, cancelToken).ConfigureAwait(false); - continue; - } - throw new HttpException(response.StatusCode); - } - else if (statusCode < 200 || statusCode >= 300) //2xx = Success - throw new HttpException(response.StatusCode); - else - return response.Content; - } - } - - private Method GetMethod(string method) - { - switch (method) - { - case "DELETE": return Method.DELETE; - case "GET": return Method.GET; - case "PATCH": return Method.PATCH; - case "POST": return Method.POST; - case "PUT": return Method.PUT; - default: throw new InvalidOperationException($"Unknown HttpMethod: {method}"); - } - } - } -} -#endif \ No newline at end of file diff --git a/src/Discord.Net/Net/TimeoutException.cs b/src/Discord.Net/Net/TimeoutException.cs deleted file mode 100644 index 051eeb263..000000000 --- a/src/Discord.Net/Net/TimeoutException.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; - -namespace Discord.Net -{ -#if NET46 - [Serializable] -#endif - 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 deleted file mode 100644 index b845d90c4..000000000 --- a/src/Discord.Net/Net/WebSocketException.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; - -namespace Discord.Net -{ - public class WebSocketException : Exception - { - public int Code { get; } - public string Reason { get; } - - public WebSocketException(int code, string reason) - : base(GenerateMessage(code, reason)) - { - Code = code; - Reason = reason; - } - - private static string GenerateMessage(int? code, string reason) - { - if (!String.IsNullOrEmpty(reason)) - return $"Received close code {code}: {reason}"; - else - return $"Received close code {code}"; - } - } -} diff --git a/src/Discord.Net/Net/WebSockets/BinaryMessageEventArgs.cs b/src/Discord.Net/Net/WebSockets/BinaryMessageEventArgs.cs index 7c6f633e9..3fd4425fa 100644 --- a/src/Discord.Net/Net/WebSockets/BinaryMessageEventArgs.cs +++ b/src/Discord.Net/Net/WebSockets/BinaryMessageEventArgs.cs @@ -6,6 +6,6 @@ namespace Discord.Net.WebSockets { public byte[] Data { get; } - public BinaryMessageEventArgs(byte[] data) { Data = data; } + public BinaryMessageEventArgs(byte[] data) { } } } diff --git a/src/Discord.Net/Net/WebSockets/BuiltInEngine.cs b/src/Discord.Net/Net/WebSockets/DefaultWebSocketEngine.cs similarity index 59% rename from src/Discord.Net/Net/WebSockets/BuiltInEngine.cs rename to src/Discord.Net/Net/WebSockets/DefaultWebSocketEngine.cs index bda5ccb15..d9486c751 100644 --- a/src/Discord.Net/Net/WebSockets/BuiltInEngine.cs +++ b/src/Discord.Net/Net/WebSockets/DefaultWebSocketEngine.cs @@ -1,66 +1,87 @@ -#if DOTNET5_4 -using System; +using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Net.WebSockets; using System.Text; using System.Threading; using System.Threading.Tasks; -using WebSocketClient = System.Net.WebSockets.ClientWebSocket; namespace Discord.Net.WebSockets { - internal class BuiltInEngine : IWebSocketEngine + public class DefaultWebSocketEngine : IWebSocketEngine { - private const int ReceiveChunkSize = 12 * 1024; //12KB - private const int SendChunkSize = 4 * 1024; //4KB - private const int HR_TIMEOUT = -2147012894; - - private readonly DiscordConfig _config; - private readonly ConcurrentQueue _sendQueue; - private WebSocketClient _webSocket; - private Task _tempTask; + public const int ReceiveChunkSize = 12 * 1024; //12KB + public const int SendChunkSize = 4 * 1024; //4KB + protected const int HR_TIMEOUT = -2147012894; public event EventHandler BinaryMessage = delegate { }; public event EventHandler TextMessage = delegate { }; - private void OnBinaryMessage(byte[] data) - => BinaryMessage(this, new BinaryMessageEventArgs(data)); - private void OnTextMessage(string msg) - => TextMessage(this, new TextMessageEventArgs(msg)); - internal BuiltInEngine(DiscordConfig config) + protected readonly ConcurrentQueue _sendQueue; + protected readonly ClientWebSocket _client; + protected Task _receiveTask, _sendTask; + protected CancellationTokenSource _cancelToken; + protected bool _isDisposed; + + public DefaultWebSocketEngine() { - _config = config; _sendQueue = new ConcurrentQueue(); + + _client = new ClientWebSocket(); + _client.Options.Proxy = null; + _client.Options.KeepAliveInterval = TimeSpan.Zero; + } + protected virtual void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + _client.Dispose(); + _isDisposed = true; + } + } + public void Dispose() + { + Dispose(true); } public async Task Connect(string host, CancellationToken cancelToken) { - _webSocket = new WebSocketClient(); - _webSocket.Options.Proxy = null; - _webSocket.Options.SetRequestHeader("User-Agent", _config.UserAgent); - _webSocket.Options.KeepAliveInterval = TimeSpan.Zero; - _tempTask = await _webSocket.ConnectAsync(new Uri(host), cancelToken)//.ConfigureAwait(false); - .ContinueWith(t => ReceiveAsync(cancelToken)).ConfigureAwait(false); - //TODO: ContinueWith is a temporary hack, may be a bug related to https://github.com/dotnet/corefx/issues/4429 - } + await Disconnect().ConfigureAwait(false); - public Task Disconnect() + _cancelToken = new CancellationTokenSource(); + var combinedToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelToken.Token, cancelToken).Token; + + await _client.ConnectAsync(new Uri(host), combinedToken).ConfigureAwait(false); + _receiveTask = TaskHelper.CreateLongRunning(() => ReceiveAsync(combinedToken), combinedToken); + _sendTask = TaskHelper.CreateLongRunning(() => SendAsync(combinedToken), combinedToken); + } + public async Task Disconnect() { + _cancelToken.Cancel(); + string ignored; while (_sendQueue.TryDequeue(out ignored)) { } - var socket = _webSocket; - _webSocket = null; + _client.Abort(); - return TaskHelper.CompletedTask; + var receiveTask = _receiveTask ?? TaskHelper.CompletedTask; + var sendTask = _sendTask ?? TaskHelper.CompletedTask; + await Task.WhenAll(receiveTask, sendTask).ConfigureAwait(false); } - public IEnumerable GetTasks(CancellationToken cancelToken) - => new Task[] { /*ReceiveAsync(cancelToken),*/ _tempTask, SendAsync(cancelToken) }; + public void SetHeader(string key, string value) + { + _client.Options.SetRequestHeader(key, value); + } + + public void QueueMessage(string message) + { + _sendQueue.Enqueue(message); + } + //TODO: Check this code private Task ReceiveAsync(CancellationToken cancelToken) { return Task.Run(async () => @@ -79,7 +100,7 @@ namespace Discord.Net.WebSockets try { - result = await _webSocket.ReceiveAsync(buffer, cancelToken).ConfigureAwait(false); + result = await _client.ReceiveAsync(buffer, cancelToken).ConfigureAwait(false); } catch (Win32Exception ex) when (ex.HResult == HR_TIMEOUT) { @@ -96,9 +117,12 @@ namespace Discord.Net.WebSockets var array = stream.ToArray(); if (result.MessageType == WebSocketMessageType.Binary) - OnBinaryMessage(array); + BinaryMessage(this, new BinaryMessageEventArgs(array)); else if (result.MessageType == WebSocketMessageType.Text) - OnTextMessage(Encoding.UTF8.GetString(array, 0, array.Length)); + { + string text = Encoding.UTF8.GetString(array, 0, array.Length); + TextMessage(this, new TextMessageEventArgs(text)); + } stream.Position = 0; stream.SetLength(0); @@ -107,6 +131,8 @@ namespace Discord.Net.WebSockets catch (OperationCanceledException) { } }); } + + //TODO: Check this code private Task SendAsync(CancellationToken cancelToken) { return Task.Run(async () => @@ -124,7 +150,7 @@ namespace Discord.Net.WebSockets int frameCount = (int)Math.Ceiling((double)byteCount / SendChunkSize); int offset = 0; - for (var i = 0; i < frameCount; i++, offset += SendChunkSize) + for (int i = 0; i < frameCount; i++, offset += SendChunkSize) { bool isLast = i == (frameCount - 1); @@ -136,7 +162,7 @@ namespace Discord.Net.WebSockets try { - await _webSocket.SendAsync(new ArraySegment(bytes, offset, count), WebSocketMessageType.Text, isLast, cancelToken).ConfigureAwait(false); + await _client.SendAsync(new ArraySegment(bytes, offset, count), WebSocketMessageType.Text, isLast, cancelToken).ConfigureAwait(false); } catch (Win32Exception ex) when (ex.HResult == HR_TIMEOUT) { @@ -150,9 +176,5 @@ namespace Discord.Net.WebSockets catch (OperationCanceledException) { } }); } - - public void QueueMessage(string message) - => _sendQueue.Enqueue(message); } } -#endif \ No newline at end of file diff --git a/src/Discord.Net/Net/WebSockets/GatewaySocket.cs b/src/Discord.Net/Net/WebSockets/GatewaySocket.cs index 4afe5aae7..3e841de89 100644 --- a/src/Discord.Net/Net/WebSockets/GatewaySocket.cs +++ b/src/Discord.Net/Net/WebSockets/GatewaySocket.cs @@ -1,14 +1,5 @@ -using Discord.API.Client; -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; +using System; using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -16,175 +7,36 @@ namespace Discord.Net.WebSockets { public class GatewaySocket : WebSocket { - private RestClient _rest; - private uint _lastSequence; - private int _reconnects; - - //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(DiscordConfig config, JsonSerializer serializer, Logger logger) - : base(config, serializer, logger) - { - Disconnected += async (s, e) => - { - if (e.WasUnexpected) - await Reconnect().ConfigureAwait(false); - }; - } - - public async Task Connect(RestClient rest, CancellationToken parentCancelToken) - { - _rest = rest; - //Token = rest.Token; - - var gatewayResponse = await rest.Send(new GatewayRequest()).ConfigureAwait(false); - Logger.Verbose($"Login successful, gateway: {gatewayResponse.Url}"); - Host = gatewayResponse.Url; - await BeginConnect(parentCancelToken).ConfigureAwait(false); - if (SessionId == null) - SendIdentify(_rest.Token); - else - SendResume(); - } - private async Task Reconnect() - { - try - { - var cancelToken = _parentCancelToken; - if (_reconnects++ == 0) - await Task.Delay(_config.ReconnectDelay, cancelToken).ConfigureAwait(false); - else - await Task.Delay(_config.FailedReconnectDelay, cancelToken).ConfigureAwait(false); + public ConnectionState State { get; } + public string Host { get; } + public string SessionId { get; } - while (!cancelToken.IsCancellationRequested) - { - try - { - await Connect(_rest, _parentCancelToken).ConfigureAwait(false); - break; - } - catch (OperationCanceledException) { throw; } - catch (Exception ex) - { - Logger.Error("Reconnect failed", ex); - //Net is down? We can keep trying to reconnect until the user runs Disconnect() - await Task.Delay(_config.FailedReconnectDelay, cancelToken).ConfigureAwait(false); - } - } - } - catch (OperationCanceledException) { } - } - public async Task Disconnect() + internal GatewaySocket(IWebSocketEngine engine) + : base(engine) { - await _taskManager.Stop(true).ConfigureAwait(false); - //Token = null; - SessionId = null; } - protected override async Task Run() + public void SetHeader(string key, string value) => _engine.SetHeader(key, value); + + internal Task Connect(CancellationToken cancelToken) { - List tasks = new List(); - tasks.AddRange(_engine.GetTasks(CancelToken)); - tasks.Add(HeartbeatAsync(CancelToken)); - await _taskManager.Start(tasks, _cancelSource).ConfigureAwait(false); + return Task.Delay(0); } - protected override Task Cleanup() + internal Task Disconnect() { - var ex = _taskManager.Exception; - if (ex == null || (ex as WebSocketException)?.Code != 1012) //if (ex == null || (ex as WebSocketException)?.Code != 1012) - SessionId = null; //Reset session unless close code 1012 - return base.Cleanup(); + return Task.Delay(0); } - protected override async Task ProcessMessage(string json) - { - base.ProcessMessage(json).GetAwaiter().GetResult(); //This is just a CompletedTask, and we need to avoid asyncs in here + public void SendIdentify(string token) { } - WebSocketMessage msg; - using (var reader = new JsonTextReader(new StringReader(json))) - msg = _serializer.Deserialize(reader, typeof(WebSocketMessage)) as WebSocketMessage; - - if (msg.Sequence.HasValue) - _lastSequence = msg.Sequence.Value; - - var opCode = (OpCodes?)msg.Operation; - switch (opCode) - { - case OpCodes.Dispatch: - { - 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().ConfigureAwait(false); //Complete the connect - } - } - break; - case OpCodes.Redirect: - { - var payload = (msg.Payload as JToken).ToObject(_serializer); - if (payload.Url != null) - { - Host = payload.Url; - Logger.Info("Redirected to " + payload.Url); - await Reconnect().ConfigureAwait(false); - } - } - break; - default: - if (opCode != null) - Logger.Warning($"Unknown Opcode: {opCode}"); - else - Logger.Warning($"Received message with no opcode"); - break; - } - } - - public void SendIdentify(string token) - { - var props = new Dictionary - { - ["$device"] = "Discord.Net" - }; - var msg = new IdentifyCommand() - { - Version = 3, - Token = token, - Properties = props, - LargeThreshold = _config.LargeThreshold, - UseCompression = true - }; - QueueMessage(msg); - } - - public void SendResume() - => QueueMessage(new ResumeCommand { SessionId = SessionId, Sequence = _lastSequence }); - public override void SendHeartbeat() - => QueueMessage(new HeartbeatCommand()); - public void SendUpdateStatus(long? idleSince, string gameName) - => QueueMessage(new UpdateStatusCommand - { - IdleSince = idleSince, - Game = gameName != null ? new UpdateStatusCommand.GameInfo { Name = gameName } : null - }); - public void SendUpdateVoice(ulong? serverId, ulong? channelId, bool isSelfMuted, bool isSelfDeafened) - => QueueMessage(new UpdateVoiceCommand { GuildId = serverId, ChannelId = channelId, IsSelfMuted = isSelfMuted, IsSelfDeafened = isSelfDeafened }); - public void SendRequestMembers(IEnumerable serverId, string query, int limit) - => QueueMessage(new RequestMembersCommand { GuildId = serverId.ToArray(), Query = query, Limit = limit }); - - //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); + public void SendResume() { } + public void SendHeartbeat() { } + public void SendUpdateStatus(long? idleSince, string gameName) { } + public void SendUpdateVoice(ulong? guildId, ulong? channelId, bool isSelfMuted, bool isSelfDeafened) { } + public void SendRequestMembers(IEnumerable guildId, string query, int limit) { } + + public void WaitForConnection(CancellationToken cancelToken) { } } } diff --git a/src/Discord.Net/Net/WebSockets/IWebSocketEngine.cs b/src/Discord.Net/Net/WebSockets/IWebSocketEngine.cs index 68f31f12b..b43109276 100644 --- a/src/Discord.Net/Net/WebSockets/IWebSocketEngine.cs +++ b/src/Discord.Net/Net/WebSockets/IWebSocketEngine.cs @@ -1,18 +1,18 @@ using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; namespace Discord.Net.WebSockets { - public interface IWebSocketEngine - { - event EventHandler BinaryMessage; - event EventHandler TextMessage; + public interface IWebSocketEngine : IDisposable + { + event EventHandler BinaryMessage; + event EventHandler TextMessage; - Task Connect(string host, CancellationToken cancelToken); - Task Disconnect(); - void QueueMessage(string message); - IEnumerable GetTasks(CancellationToken cancelToken); - } + void SetHeader(string key, string value); + + Task Connect(string host, CancellationToken cancelToken); + Task Disconnect(); + void QueueMessage(string message); + } } diff --git a/src/Discord.Net/Net/WebSockets/WS4NetEngine.cs b/src/Discord.Net/Net/WebSockets/WS4NetEngine.cs deleted file mode 100644 index 420299d6b..000000000 --- a/src/Discord.Net/Net/WebSockets/WS4NetEngine.cs +++ /dev/null @@ -1,141 +0,0 @@ -#if !DOTNET5_4 -using SuperSocket.ClientEngine; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using WebSocket4Net; -using WebSocketClient = WebSocket4Net.WebSocket; - -namespace Discord.Net.WebSockets -{ - internal class WS4NetEngine : IWebSocketEngine - { - private readonly DiscordConfig _config; - private readonly ConcurrentQueue _sendQueue; - private readonly TaskManager _taskManager; - private WebSocketClient _webSocket; - private ManualResetEventSlim _waitUntilConnect, _waitUntilDisconnect; - - public event EventHandler BinaryMessage = delegate { }; - public event EventHandler TextMessage = delegate { }; - private void OnBinaryMessage(byte[] data) - => BinaryMessage(this, new BinaryMessageEventArgs(data)); - private void OnTextMessage(string msg) - => TextMessage(this, new TextMessageEventArgs(msg)); - - internal WS4NetEngine(DiscordConfig config, TaskManager taskManager) - { - _config = config; - _taskManager = taskManager; - _sendQueue = new ConcurrentQueue(); - _waitUntilConnect = new ManualResetEventSlim(); - _waitUntilDisconnect = new ManualResetEventSlim(true); - } - - public Task Connect(string host, CancellationToken cancelToken) - { - try - { - _webSocket = new WebSocketClient(host); - _webSocket.EnableAutoSendPing = false; - _webSocket.NoDelay = true; - _webSocket.Proxy = null; - - _webSocket.DataReceived += OnWebSocketBinary; - _webSocket.MessageReceived += OnWebSocketText; - _webSocket.Error += OnWebSocketError; - _webSocket.Closed += OnWebSocketClosed; - _webSocket.Opened += OnWebSocketOpened; - - _waitUntilConnect.Reset(); - _waitUntilDisconnect.Reset(); - _webSocket.Open(); - _waitUntilConnect.Wait(cancelToken); - _taskManager.ThrowException(); //In case our connection failed - } - catch - { - _waitUntilDisconnect.Set(); - throw; - } - return TaskHelper.CompletedTask; - } - - public Task Disconnect() - { - string ignored; - while (_sendQueue.TryDequeue(out ignored)) { } - - var socket = _webSocket; - _webSocket = null; - if (socket != null) - { - socket.Close(); - socket.Opened -= OnWebSocketOpened; - socket.DataReceived -= OnWebSocketBinary; - socket.MessageReceived -= OnWebSocketText; - - _waitUntilDisconnect.Wait(); //We need the next two events to raise this one - socket.Error -= OnWebSocketError; - socket.Closed -= OnWebSocketClosed; - socket.Dispose(); - } - - return TaskHelper.CompletedTask; - } - - private async void OnWebSocketError(object sender, ErrorEventArgs e) - { - await _taskManager.SignalError(e.Exception).ConfigureAwait(false); - _waitUntilConnect.Set(); - _waitUntilDisconnect.Set(); - } - private async void OnWebSocketClosed(object sender, EventArgs e) - { - Exception ex; - if (e is ClosedEventArgs) - ex = new WebSocketException((e as ClosedEventArgs).Code, (e as ClosedEventArgs).Reason); - else - ex = new Exception("Connection lost"); - await _taskManager.SignalError(ex).ConfigureAwait(false); - _waitUntilConnect.Set(); - _waitUntilDisconnect.Set(); - } - private void OnWebSocketOpened(object sender, EventArgs e) - { - _waitUntilConnect.Set(); - _waitUntilDisconnect.Reset(); - } - private void OnWebSocketText(object sender, MessageReceivedEventArgs e) - => OnTextMessage(e.Message); - private void OnWebSocketBinary(object sender, DataReceivedEventArgs e) - => OnBinaryMessage(e.Data); - - public IEnumerable GetTasks(CancellationToken cancelToken) - => new Task[] { SendAsync(cancelToken) }; - - private Task SendAsync(CancellationToken cancelToken) - { - return Task.Run(async () => - { - try - { - while (!cancelToken.IsCancellationRequested) - { - string json; - while (_sendQueue.TryDequeue(out json)) - _webSocket.Send(json); - await Task.Delay(DiscordConfig.WebSocketQueueInterval, cancelToken).ConfigureAwait(false); - } - } - catch (OperationCanceledException) { } - }); - } - - public void QueueMessage(string message) - => _sendQueue.Enqueue(message); - } -} -#endif \ No newline at end of file diff --git a/src/Discord.Net/Net/WebSockets/WebSocket.cs b/src/Discord.Net/Net/WebSockets/WebSocket.cs index aa2c9a98b..ed533883a 100644 --- a/src/Discord.Net/Net/WebSockets/WebSocket.cs +++ b/src/Discord.Net/Net/WebSockets/WebSocket.cs @@ -1,188 +1,27 @@ -using Discord.API.Client; -using Discord.Logging; -using Newtonsoft.Json; -using Nito.AsyncEx; -using System; -using System.IO; -using System.IO.Compression; -using System.Threading; -using System.Threading.Tasks; +using System; namespace Discord.Net.WebSockets { - public abstract partial class WebSocket - { - private readonly AsyncLock _lock; + public class WebSocket : IDisposable + { protected readonly IWebSocketEngine _engine; - protected readonly DiscordConfig _config; - protected readonly ManualResetEventSlim _connectedEvent; - protected readonly TaskManager _taskManager; - protected readonly JsonSerializer _serializer; - protected CancellationTokenSource _cancelSource; - protected CancellationToken _parentCancelToken; - protected int _heartbeatInterval; - private DateTime _lastHeartbeat; + protected bool _isDisposed; - /// Gets the logger used for this client. - protected internal Logger Logger { get; } - public CancellationToken CancelToken { get; private set; } - - public string Host { get; set; } - /// Gets the current connection state of this client. - public ConnectionState State { get; private set; } - - public event EventHandler Connected = delegate { }; - private void OnConnected() - => Connected(this, EventArgs.Empty); - public event EventHandler Disconnected = delegate { }; - private void OnDisconnected(bool wasUnexpected, Exception error) - => Disconnected(this, new DisconnectedEventArgs(wasUnexpected, error)); - - public WebSocket(DiscordConfig config, JsonSerializer serializer, Logger logger) - { - _config = config; - _serializer = serializer; - Logger = logger; - - _lock = new AsyncLock(); - _taskManager = new TaskManager(Cleanup); - CancelToken = new CancellationToken(true); - _connectedEvent = new ManualResetEventSlim(false); - -#if !DOTNET5_4 - _engine = new WS4NetEngine(config, _taskManager); -#else - _engine = new BuiltInEngine(config); -#endif - _engine.BinaryMessage += (s, e) => - { - using (var compressed = new MemoryStream(e.Data, 2, e.Data.Length - 2)) - using (var decompressed = new MemoryStream()) - { - using (var zlib = new DeflateStream(compressed, CompressionMode.Decompress)) - zlib.CopyTo(decompressed); - decompressed.Position = 0; - using (var reader = new StreamReader(decompressed)) - ProcessMessage(reader.ReadToEnd()).GetAwaiter().GetResult(); - } - }; - _engine.TextMessage += (s, e) => ProcessMessage(e.Message).Wait(); - } - - 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; - - _cancelSource = new CancellationTokenSource(); - CancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelSource.Token, parentCancelToken).Token; - _lastHeartbeat = DateTime.UtcNow; - - await _engine.Connect(Host, CancelToken).ConfigureAwait(false); - await Run().ConfigureAwait(false); - } - } - catch (Exception ex) - { - //TODO: Should this be inside the lock? - await _taskManager.SignalError(ex).ConfigureAwait(false); - throw; - } - } - protected async Task EndConnect() - { - try - { - State = ConnectionState.Connected; - Logger.Info($"Connected"); - - OnConnected(); - _connectedEvent.Set(); - } - catch (Exception ex) - { - await _taskManager.SignalError(ex).ConfigureAwait(false); - } - } - - protected abstract Task Run(); - protected virtual async Task Cleanup() - { - var oldState = State; - State = ConnectionState.Disconnecting; - - await _engine.Disconnect().ConfigureAwait(false); - _cancelSource = null; - _connectedEvent.Reset(); - - if (oldState == ConnectionState.Connecting || oldState == ConnectionState.Connected) - { - var ex = _taskManager.Exception; - if (ex == null) - Logger.Info("Disconnected"); - else - Logger.Error("Disconnected", ex); - State = ConnectionState.Disconnected; - OnDisconnected(!_taskManager.WasStopExpected, _taskManager.Exception); - } - else - State = ConnectionState.Disconnected; - } - - protected virtual Task ProcessMessage(string json) - { - return TaskHelper.CompletedTask; - } - protected void QueueMessage(IWebSocketMessage message) - { - string json = JsonConvert.SerializeObject(new WebSocketMessage(message)); - _engine.QueueMessage(json); - } - - protected Task HeartbeatAsync(CancellationToken cancelToken) - { - return Task.Run(async () => - { - try - { - while (!cancelToken.IsCancellationRequested) - { - if (this.State == ConnectionState.Connected && _heartbeatInterval > 0) - { - SendHeartbeat(); - await Task.Delay(_heartbeatInterval, cancelToken).ConfigureAwait(false); - } - else - await Task.Delay(1000, cancelToken).ConfigureAwait(false); - } - } - catch (OperationCanceledException) { } - }); - } - public abstract void SendHeartbeat(); + internal WebSocket(IWebSocketEngine engine) + { + _engine = engine; + } - public virtual void WaitForConnection(CancellationToken cancelToken) + protected virtual void Dispose(bool disposing) { - try + if (!_isDisposed) { - if (!_connectedEvent.Wait(_config.ConnectionTimeout, cancelToken)) - { - if (State != ConnectionState.Connected) - throw new TimeoutException(); - } - } - catch (OperationCanceledException) - { - _taskManager.ThrowException(); //Throws data socket's internal error if any occured - throw; + if (disposing) + _engine.Dispose(); + + _isDisposed = true; } } - } + public void Dispose() => Dispose(true); + } } diff --git a/src/Discord.Net/Net/WebSockets/WebSocketEventEventArgs.cs b/src/Discord.Net/Net/WebSockets/WebSocketEventEventArgs.cs index a0c60edcf..676c0ba6e 100644 --- a/src/Discord.Net/Net/WebSockets/WebSocketEventEventArgs.cs +++ b/src/Discord.Net/Net/WebSockets/WebSocketEventEventArgs.cs @@ -7,11 +7,5 @@ namespace Discord.Net.WebSockets { public string Type { get; } public JToken Payload { get; } - - internal WebSocketEventEventArgs(string type, JToken data) - { - Type = type; - Payload = data; - } } } diff --git a/src/Discord.Net/Net/WebSockets/WebSocketProvider.cs b/src/Discord.Net/Net/WebSockets/WebSocketProvider.cs new file mode 100644 index 000000000..7e7652dbc --- /dev/null +++ b/src/Discord.Net/Net/WebSockets/WebSocketProvider.cs @@ -0,0 +1,6 @@ +using System.Threading; + +namespace Discord.Net.WebSockets +{ + public delegate IWebSocketEngine WebSocketProvider(CancellationToken cancelToken); +} diff --git a/src/Discord.Net/Properties/AssemblyInfo.cs b/src/Discord.Net/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..b562e546b --- /dev/null +++ b/src/Discord.Net/Properties/AssemblyInfo.cs @@ -0,0 +1,18 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("Discord.Net")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Discord.Net")] +[assembly: AssemblyCopyright("Copyright © Rogue Exception 2015-2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +[assembly: ComVisible(false)] + +[assembly: Guid("c6a50d24-cbd3-4e76-852c-4dca60bbd608")] + +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/Discord.Net/ServiceCollection.cs b/src/Discord.Net/ServiceCollection.cs deleted file mode 100644 index 104f91dd4..000000000 --- a/src/Discord.Net/ServiceCollection.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; - -namespace Discord -{ - internal class ServiceCollection : IEnumerable - { - private readonly Dictionary _services; - - internal DiscordClient Client { get; } - - internal ServiceCollection(DiscordClient client) - { - Client = client; - _services = new Dictionary(); - } - - public T Add(T service) - where T : class, IService - { - _services.Add(typeof(T), service); - service.Install(Client); - return service; - } - - public T Get(bool isRequired = true) - where T : class, IService - { - IService service; - T singletonT = null; - - if (_services.TryGetValue(typeof(T), out service)) - singletonT = service as T; - - if (singletonT == null && isRequired) - throw new InvalidOperationException($"This operation requires {typeof(T).Name} to be added to {nameof(DiscordClient)}."); - return singletonT; - } - - public IEnumerator GetEnumerator() => _services.Values.GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => _services.Values.GetEnumerator(); - } -} diff --git a/src/Discord.Net/TaskHelper.cs b/src/Discord.Net/TaskHelper.cs new file mode 100644 index 000000000..17ecc4615 --- /dev/null +++ b/src/Discord.Net/TaskHelper.cs @@ -0,0 +1,18 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Discord +{ + internal static class TaskHelper + { +#if NETSTANDARD1_4 + public static Task CompletedTask => Task.CompletedTask; +#else + public static Task CompletedTask => Task.Delay(0); +#endif + + public static Task CreateLongRunning(Action action, CancellationToken cancelToken) + => Task.Factory.StartNew(action, cancelToken, TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); + } +} diff --git a/src/Discord.Net/TaskManager.cs b/src/Discord.Net/TaskManager.cs deleted file mode 100644 index d43372396..000000000 --- a/src/Discord.Net/TaskManager.cs +++ /dev/null @@ -1,179 +0,0 @@ -using Nito.AsyncEx; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.ExceptionServices; -using System.Threading; -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 class TaskManager - { - private readonly AsyncLock _lock; - private readonly Func _stopAction; - private ExceptionDispatchInfo _stopReason; - - private CancellationTokenSource _cancelSource; - private Task _task; - - public bool StopOnCompletion { get; } - public bool WasStopExpected { get; private set; } - - public Exception Exception => _stopReason?.SourceException; - - internal TaskManager(bool stopOnCompletion) - { - _lock = new AsyncLock(); - StopOnCompletion = stopOnCompletion; - } - public TaskManager(Action stopAction, bool stopOnCompletion = true) - : this(stopOnCompletion) - { - _stopAction = TaskHelper.ToAsync(stopAction); - } - 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; - if (task != null) - await Stop().ConfigureAwait(false); - - using (await _lock.LockAsync().ConfigureAwait(false)) - { - _cancelSource = cancelSource; - - if (_task != null) - continue; //Another thread sneaked in and started this manager before we got a lock, loop and try again - - _stopReason = null; - WasStopExpected = false; - - Task[] tasksArray = tasks.ToArray(); - - _task = Task.Run(async () => - { - 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) - await _stopAction().ConfigureAwait(false); - _task = null; - _cancelSource = null; - }); - return; - } - } - } - - public async Task SignalStop(bool isExpected = false) - { - using (await _lock.LockAsync().ConfigureAwait(false)) - { - if (isExpected) - WasStopExpected = true; - - Cancel(); - } - } - public async Task Stop(bool isExpected = false) - { - Task task; - using (await _lock.LockAsync().ConfigureAwait(false)) - { - if (isExpected) - WasStopExpected = true; - - //Cache the task so we still have something to await if Cleanup is run really quickly - task = _task ?? TaskHelper.CompletedTask; - Cancel(); - } - await task.ConfigureAwait(false); - } - - public async Task SignalError(Exception ex) - { - using (await _lock.LockAsync().ConfigureAwait(false)) - { - if (_stopReason != null) return; - - Cancel(ex); - } - } - public async Task Error(Exception ex) - { - Task task; - using (await _lock.LockAsync().ConfigureAwait(false)) - { - if (_stopReason != null) return; - - //Cache the task so we still have something to await if Cleanup is run really quickly - task = _task ?? TaskHelper.CompletedTask; - 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() - { - using (_lock.Lock()) - { - if (!WasStopExpected) - _stopReason?.Throw(); - } - } - public void ClearException() - { - using (_lock.Lock()) - { - _stopReason = null; - WasStopExpected = false; - } - } - } -} diff --git a/src/Discord.Net/project.json b/src/Discord.Net/project.json index 80a6fb51d..4aa7cd55c 100644 --- a/src/Discord.Net/project.json +++ b/src/Discord.Net/project.json @@ -2,78 +2,38 @@ "version": "1.0.0-alpha1", "description": "An unofficial .Net API wrapper for the Discord client.", "authors": [ "RogueException" ], - "tags": [ - "discord", - "discordapp" - ], + "tags": [ "discord", "discordapp" ], "projectUrl": "https://github.com/RogueException/Discord.Net", "licenseUrl": "http://opensource.org/licenses/MIT", "repository": { "type": "git", "url": "git://github.com/RogueException/Discord.Net" }, - "compile": [ "**/*.cs", "../Discord.Net.Shared/*.cs" ], "compilationOptions": { - "allowUnsafe": true, - "warningsAsErrors": true - }, - - "configurations": { - "TestResponses": { - "compilationOptions": { - "define": [ - "DEBUG", - "TRACE", - "TEST_RESPONSES" - ] - } - } + "allowUnsafe": true }, "dependencies": { - "Newtonsoft.Json": "8.0.1", - "Nito.AsyncEx": "3.0.1" + "Newtonsoft.Json": "8.0.3", + "System.Collections.Concurrent": "4.0.12-rc3-*", + "System.Collections.Immutable": "1.2.0-rc3-*", + "System.IO": "4.1.0-rc3-*", + "System.IO.FileSystem": "4.0.1-rc3-*", + "System.Reflection": "4.1.0-rc3-*", + "System.Runtime": "4.1.0-rc3-*", + "System.Net.Requests": "4.0.11-rc3-*", + "System.Net.Http": "4.0.1-rc3-*", + "System.Net.Sockets": "4.1.0-rc3-*", + "System.Net.WebSockets.Client": "4.0.0-rc3-*", + "System.Text.RegularExpressions": "4.0.12-rc3-*", + "System.Threading": "4.0.11-rc3-*", + "System.Threading.Tasks": "4.0.11-rc3-*" }, "frameworks": { - "dotnet5.4": { - "dependencies": { - "System.Collections": "4.0.11-beta-23516", - "System.Collections.Concurrent": "4.0.11-beta-23516", - "System.Dynamic.Runtime": "4.0.11-beta-23516", - "System.IO.FileSystem": "4.0.1-beta-23516", - "System.IO.Compression": "4.1.0-beta-23516", - "System.Linq": "4.0.1-beta-23516", - "System.Net.Http": "4.0.1-beta-23516", - "System.Net.NameResolution": "4.0.0-beta-23516", - "System.Net.Sockets": "4.1.0-beta-23409", - "System.Net.Requests": "4.0.11-beta-23516", - "System.Net.WebSockets.Client": "4.0.0-beta-23516", - "System.Reflection": "4.1.0-beta-23516", - "System.Reflection.Emit.Lightweight": "4.0.1-beta-23516", - "System.Runtime.InteropServices": "4.0.21-beta-23516", - "System.Runtime.Serialization.Primitives": "4.1.0-beta-23516", - "System.Security.Cryptography.Algorithms": "4.0.0-beta-23516", - "System.Text.RegularExpressions": "4.0.11-beta-23516", - "System.Threading": "4.0.11-beta-23516" - } - }, - "net45": { - "frameworkAssemblies": { - "System.Runtime": { - "type": "build", - "version": "" - }, - "System.Threading.Tasks": { - "type": "build", - "version": "" - } - }, - "dependencies": { - "WebSocket4Net": "0.14.1", - "RestSharp": "105.2.3" - } + "netstandard1.4": { + "imports": "dotnet5.5" } } } \ No newline at end of file diff --git a/test/Discord.Net.Tests/Discord.Net.Tests.csproj b/test/Discord.Net.Tests/Discord.Net.Tests.csproj index 0c13b7075..2a50610cc 100644 --- a/test/Discord.Net.Tests/Discord.Net.Tests.csproj +++ b/test/Discord.Net.Tests/Discord.Net.Tests.csproj @@ -1,5 +1,5 @@  - + Debug AnyCPU @@ -8,7 +8,7 @@ Properties Discord.Tests Discord.Net.Tests - v4.5 + v4.6.1 512 {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 10.0 @@ -56,17 +56,16 @@ - - - {8d71a857-879a-4a10-859e-5ff824ed6688} - Discord.Net - + - + + {c6a50d24-cbd3-4e76-852c-4dca60bbd608} + Discord.Net.Net45 + diff --git a/test/Discord.Net.Tests/Settings.cs b/test/Discord.Net.Tests/Settings.cs deleted file mode 100644 index 5aa37e184..000000000 --- a/test/Discord.Net.Tests/Settings.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Newtonsoft.Json; -using System.IO; - -namespace Discord.Tests -{ - internal class Settings - { - private const string path = "../../config.json"; - public static readonly Settings Instance; - static Settings() - { - if (!File.Exists(path)) - throw new FileNotFoundException("config.json is missing, rename config.json.example and add credentials for three separate unused accounts for testing."); - Instance = JsonConvert.DeserializeObject(File.ReadAllText(path)); - } - - public class Account - { - [JsonProperty("email")] - public string Email { get; set; } - [JsonProperty("password")] - public string Password { get; set; } - } - - [JsonProperty("user1")] - public Account User1 { get; set; } - [JsonProperty("user2")] - public Account User2 { get; set; } - [JsonProperty("user3")] - public Account User3 { get; set; } - } -} diff --git a/test/Discord.Net.Tests/Tests.cs b/test/Discord.Net.Tests/Tests.cs index 51c045d69..6cb0196d5 100644 --- a/test/Discord.Net.Tests/Tests.cs +++ b/test/Discord.Net.Tests/Tests.cs @@ -7,167 +7,484 @@ using System.Threading.Tasks; namespace Discord.Tests { - //TODO: Tests are massively incomplete and out of date, needing a full rewrite - - [TestClass] - public class Tests - { - private const int EventTimeout = 5000; //Max time in milliseconds to wait for an event response from our test actions - - private static DiscordClient _hostClient, _targetBot, _observerBot; - private static Server _testServer; - private static Channel _testServerChannel; - private static Random _random; - - [ClassInitialize] - public static void Initialize(TestContext testContext) - { - var settings = Settings.Instance; - _random = new Random(); - - _hostClient = new DiscordClient(); - _targetBot = new DiscordClient(); - _observerBot = new DiscordClient(); - - _hostClient.Connect(settings.User1.Email, settings.User1.Password).Wait(); - _targetBot.Connect(settings.User2.Email, settings.User2.Password).Wait(); - _observerBot.Connect(settings.User3.Email, settings.User3.Password).Wait(); - - //Cleanup existing servers - WaitMany( - _hostClient.Servers.Select(x => x.IsOwner ? x.Delete() : x.Leave()), - _targetBot.Servers.Select(x => x.IsOwner ? x.Delete() : x.Leave()), - _observerBot.Servers.Select(x => x.IsOwner ? x.Delete() : x.Leave())); - - //Create new server and invite the other bots to it - _testServer = _hostClient.CreateServer("Discord.Net Testing", _hostClient.Regions.First()).Result; - _testServerChannel = _testServer.DefaultChannel; - var invite = _testServer.CreateInvite(60, 2, false, false).Result; - WaitAll( - _targetBot.GetInvite(invite.Code).Result.Accept(), - _observerBot.GetInvite(invite.Code).Result.Accept()); - } - - //Channels - [TestMethod] - public void TestCreateTextChannel() - => TestCreateChannel(ChannelType.Text); - [TestMethod] - public void TestCreateVoiceChannel() - => TestCreateChannel(ChannelType.Voice); - private void TestCreateChannel(ChannelType type) - { - Channel channel = null; - string name = $"#test_{_random.Next()}"; - AssertEvent( - "ChannelCreated event never received", - async () => channel = await _testServer.CreateChannel(name.Substring(1), type), - x => _targetBot.ChannelCreated += x, - x => _targetBot.ChannelCreated -= x, - (s, e) => e.Channel.Name == name); - - AssertEvent( - "ChannelDestroyed event never received", - async () => await channel.Delete(), - x => _targetBot.ChannelDestroyed += x, - x => _targetBot.ChannelDestroyed -= x, - (s, e) => e.Channel.Name == name); - } - - [TestMethod] - [ExpectedException(typeof(InvalidOperationException))] - public async Task TestCreateChannel_NoName() - { - await _testServer.CreateChannel($"", ChannelType.Text); - } - [TestMethod] - [ExpectedException(typeof(InvalidOperationException))] - public async Task TestCreateChannel_NoType() - { - string name = $"#test_{_random.Next()}"; - await _testServer.CreateChannel($"", ChannelType.FromString("")); - } - [TestMethod] - [ExpectedException(typeof(InvalidOperationException))] - public async Task TestCreateChannel_BadType() - { - string name = $"#test_{_random.Next()}"; - await _testServer.CreateChannel($"", ChannelType.FromString("badtype")); - } - - //Messages - [TestMethod] - public void TestSendMessage() - { - string text = $"test_{_random.Next()}"; - AssertEvent( - "MessageCreated event never received", - () => _testServerChannel.SendMessage(text), - x => _targetBot.MessageReceived += x, - x => _targetBot.MessageReceived -= x, - (s, e) => e.Message.Text == text); - } - - [ClassCleanup] - public static void Cleanup() - { - WaitMany( - _hostClient.State == ConnectionState.Connected ? _hostClient.Servers.Select(x => x.IsOwner ? x.Delete() : x.Leave()) : null, - _targetBot.State == ConnectionState.Connected ? _targetBot.Servers.Select(x => x.IsOwner ? x.Delete() : x.Leave()) : null, - _observerBot.State == ConnectionState.Connected ? _observerBot.Servers.Select(x => x.IsOwner ? x.Delete() : x.Leave()) : null); - - WaitAll( - _hostClient.Disconnect(), - _targetBot.Disconnect(), - _observerBot.Disconnect()); - } - - // Unit Test Helpers - - private static void AssertEvent(string msg, Func action, Action> addEvent, Action> removeEvent, Func test = null) - { - AssertEvent(msg, action, addEvent, removeEvent, test, true); - } - private static void AssertNoEvent(string msg, Func action, Action> addEvent, Action> removeEvent, Func test = null) - { - AssertEvent(msg, action, addEvent, removeEvent, test, false); - } - private static void AssertEvent(string msg, Func action, Action> addEvent, Action> removeEvent, Func test, bool assertTrue) - { - ManualResetEventSlim trigger = new ManualResetEventSlim(false); - bool result = false; - - EventHandler handler = (s, e) => - { - if (test != null) - { - result |= test(s, e); - trigger.Set(); + //TODO: Tests are massively incomplete and out of date, needing a full rewrite + + [TestClass] + public class Tests + { + private const int EventTimeout = 10000; //Max time in milliseconds to wait for an event response from our test actions + + private static DiscordSocketClient _hostBot, _targetBot, _observerBot; + private static Guild _testGuild; + private static TextChannel _testGuildChannel; + private static Random _random; + private static PublicInvite _testGuildInvite; + + private static TestContext _context; + + private static string _hostToken; + private static string _observerToken; + private static string _targetToken; + + private static string GetRandomText() + { + lock (_random) + return $"test_{_random.Next()}"; + } + + #region Initialization + + [ClassInitialize] + public static void Initialize(TestContext testContext) + { + _context = testContext; + + _hostToken = Environment.GetEnvironmentVariable("discord-unit-host_token"); + _observerToken = Environment.GetEnvironmentVariable("discord-unit-observer_token"); + _targetToken = Environment.GetEnvironmentVariable("discord-unit-target_token"); + } + + [TestMethod] + [Priority(1)] + public async Task TestInitialize() + { + _context.WriteLine("Initializing."); + + _random = new Random(); + + _hostBot = new DiscordSocketClient(_hostToken); + _targetBot = new DiscordSocketClient(_targetToken); + _observerBot = new DiscordSocketClient(_observerToken); + + await _hostBot.Login(); + + await Task.Delay(3000); + + //Cleanup existing Guilds + (await _hostBot.GetGuilds()).Select(x => x.Owner.Id == _hostBot.CurrentUser.Id ? x.Delete() : x.Leave()); + + //Create new Guild and invite the other bots to it + + _testGuild = await _hostBot.CreateGuild("Discord.Net Testing", _hostBot.GetOptimalVoiceRegion()); + + await Task.Delay(1000); + + PublicInvite invite = await _testGuild.CreateInvite(60, 3, false, false); + _testGuildInvite = invite; + + _context.WriteLine($"Host: {_hostBot.CurrentUser.Username} in {(await _hostBot.GetGuilds()).Count()}"); + } + + [TestMethod] + [Priority(2)] + public async Task TestTokenLogin_Ready() + { + AssertEvent( + "READY never received", + async () => await _observerBot.Login(), + x => _observerBot.Connected += x, + x => _observerBot.Connected -= x, + null, + true); + (await _observerBot.GetGuilds()).Select(x => x.Owner.Id == _observerBot.CurrentUser.Id ? x.Delete() : x.Leave()); + await _observerBot.RestClient.Send(new API.Rest.AcceptInviteRequest(_testGuildInvite.Code)); + } + + [TestMethod] + [Priority(2)] + public async Task TestReady() + { + AssertEvent( + "READY never received", + async () => await _targetBot.Login(), + x => _targetBot.Connected += x, + x => _targetBot.Connected -= x, + null, + true); + + (await _targetBot.GetGuilds()).Select(x => x.Owner.Id == _targetBot.CurrentUser.Id ? x.Delete() : x.Leave()); + _testGuildChannel = _testGuild.DefaultChannel; + } + + #endregion + + // Guilds + + #region Guild Tests + + [TestMethod] + [Priority(3)] + public void TestJoinedGuild() + { + AssertEvent( + "Never Got JoinedGuild", + async () => await _targetBot.RestClient.Send(new API.Rest.AcceptInviteRequest(_testGuildInvite.Code)), + x => _targetBot.JoinedGuild += x, + x => _targetBot.JoinedGuild -= x); + } + + #endregion + + #region Channel Tests + + //Channels + [TestMethod] + public void TestCreateTextChannel() + { + GuildChannel channel = null; + string name = GetRandomText(); + AssertEvent( + "ChannelCreated event never received", + async () => channel = await _testGuild.CreateTextChannel(name), + x => _targetBot.ChannelCreated += x, + x => _targetBot.ChannelCreated -= x, + (s, e) => e.Channel.Id == channel.Id); + + AssertEvent( + "ChannelDestroyed event never received", + async () => await channel.Delete(), + x => _targetBot.ChannelDestroyed += x, + x => _targetBot.ChannelDestroyed -= x, + (s, e) => e.Channel.Id == channel.Id); + } + [TestMethod] + public void TestCreateVoiceChannel() + { + GuildChannel channel = null; + string name = GetRandomText(); + AssertEvent( + "ChannelCreated event never received", + async () => channel = await _testGuild.CreateVoiceChannel(name), + x => _targetBot.ChannelCreated += x, + x => _targetBot.ChannelCreated -= x, + (s, e) => e.Channel.Id == channel.Id); + + AssertEvent( + "ChannelDestroyed event never received", + async () => await channel.Delete(), + x => _targetBot.ChannelDestroyed += x, + x => _targetBot.ChannelDestroyed -= x, + (s, e) => e.Channel.Id == channel.Id); + } + + [TestMethod] + [ExpectedException(typeof(Net.HttpException))] + public async Task TestCreateChannel_NoName() + { + await _testGuild.CreateTextChannel($""); + } + [TestMethod] + public async Task Test_CreateGetChannel() + { + var name = GetRandomText(); + var channel = await _testGuild.CreateTextChannel(name); + var get_channel = _testGuild.GetChannel(channel.Id); + Assert.AreEqual(channel.Id, get_channel.Id, "ID of Channel and GetChannel were not equal."); + } + [TestMethod] + public void TestSendTyping() + { + var channel = _testGuildChannel; + AssertEvent( + "UserUpdated event never fired.", + async () => await channel.TriggerTyping(), + x => _targetBot.UserIsTyping += x, + x => _targetBot.UserIsTyping -= x); + } + [TestMethod] + public void TestEditChannel() + { + var channel = _testGuildChannel; + AssertEvent( + "ChannelUpdated Never Received", + async () => await channel.Modify(x => + { + x.Name = GetRandomText(); + x.Topic = $"topic - {GetRandomText()}"; + x.Position = 26; + }), + x => _targetBot.ChannelUpdated += x, + x => _targetBot.ChannelUpdated -= x); + } + [TestMethod] + public void TestChannelMention() + { + var channel = _testGuildChannel; + Assert.AreEqual($"<#{channel.Id}>", channel.Mention, "Generated channel mention was not the expected channel mention."); + } + [TestMethod] + public void TestChannelUserCount() + { + Assert.AreEqual(3, _testGuildChannel.Users.Count(), "Read an incorrect number of users in a channel"); + } + + #endregion + + #region Message Tests + + //Messages + [TestMethod] + public async Task TestMessageEvents() + { + string name = GetRandomText(); + var channel = await _testGuild.CreateTextChannel(name); + _context.WriteLine($"Channel Name: {channel.Name} / {channel.Guild.Name}"); + string text = GetRandomText(); + Message message = null; + AssertEvent( + "MessageCreated event never received", + async () => message = await channel.SendMessage(text), + x => _targetBot.MessageReceived += x, + x => _targetBot.MessageReceived -= x, + (s, e) => e.Message.Text == text); + + AssertEvent( + "MessageUpdated event never received", + async () => await message.Modify(x => + { + x.Text = text + " updated"; + }), + x => _targetBot.MessageUpdated += x, + x => _targetBot.MessageUpdated -= x, + (s, e) => e.Before.Text == text && e.After.Text == text + " updated"); + + AssertEvent( + "MessageDeleted event never received", + async () => await message.Delete(), + x => _targetBot.MessageDeleted += x, + x => _targetBot.MessageDeleted -= x, + (s, e) => e.Message.Id == message.Id); + } + [TestMethod] + public async Task TestDownloadMessages() + { + string name = GetRandomText(); + var channel = await _testGuild.CreateTextChannel(name); + for (var i = 0; i < 10; i++) await channel.SendMessage(GetRandomText()); + while (channel.Discord.MessageQueue.Count > 0) await Task.Delay(100); + var messages = await channel.GetMessages(10); + Assert.AreEqual(10, messages.Count(), "Expected 10 messages in downloaded array, did not see 10."); + } + [TestMethod] + public async Task TestSendTTSMessage() + { + var channel = await _testGuild.CreateTextChannel(GetRandomText()); + AssertEvent( + "MessageCreated event never fired", + async () => await channel.SendMessage(GetRandomText(), true), + x => _targetBot.MessageReceived += x, + x => _targetBot.MessageReceived -= x, + (s, e) => e.Message.IsTTS); + } + + #endregion + + #region User Tests + + [TestMethod] + public async Task TestUserMentions() + { + var user = (await _targetBot.GetGuild(_testGuild.Id)).CurrentUser; + Assert.AreEqual($"<@{user.Id}>", user.Mention); + } + [TestMethod] + public void TestUserEdit() + { + var user = _testGuild.GetUser(_targetBot.CurrentUser.Id); + AssertEvent( + "UserUpdated never fired", + async () => await user.Modify(true, true, null, null), + x => _targetBot.UserUpdated += x, + x => _targetBot.UserUpdated -= x); + } + [TestMethod] + public void TestEditSelf() + { + throw new NotImplementedException(); + /*var name = RandomText + AssertEvent( + "UserUpdated never fired", + async () => await _targetBot.CurrentUser.Modify(TargetPassword, name), + x => _obGuildBot.UserUpdated += x, + x => _obGuildBot.UserUpdated -= x, + (s, e) => e.After.Username == name);*/ + } + [TestMethod] + public void TestSetStatus() + { + AssertEvent( + "UserUpdated never fired", + async () => await SetStatus(_targetBot, UserStatus.Idle), + x => _observerBot.UserUpdated += x, + x => _observerBot.UserUpdated -= x, + (s, e) => e.After.Status == UserStatus.Idle); + } + private async Task SetStatus(DiscordClient _client, UserStatus status) + { + throw new NotImplementedException(); + /*_client.SetStatus(status); + await Task.Delay(50);*/ + } + [TestMethod] + public void TestSetGame() + { + AssertEvent( + "UserUpdated never fired", + async () => await SetGame(_targetBot, "test game"), + x => _observerBot.UserUpdated += x, + x => _observerBot.UserUpdated -= x, + (s, e) => _targetBot.CurrentUser.CurrentGame == "test game"); + + } + private async Task SetGame(DiscordClient _client, string game) + { + throw new NotImplementedException(); + //_client.SetGame(game); + //await Task.Delay(5); + } + + #endregion + + #region Permission Tests + + // Permissions + [TestMethod] + public async Task Test_AddGet_PermissionsRule() + { + var channel = await _testGuild.CreateTextChannel(GetRandomText()); + var user = _testGuild.GetUser(_targetBot.CurrentUser.Id); + var perms = new OverwritePermissions(sendMessages: PermValue.Deny); + await channel.UpdatePermissionOverwrite(user, perms); + var resultPerms = channel.GetPermissionOverwrite(user); + Assert.IsNotNull(resultPerms, "Perms retrieved from Guild were null."); + } + [TestMethod] + public async Task Test_AddRemove_PermissionsRule() + { + var channel = await _testGuild.CreateTextChannel(GetRandomText()); + var user = _testGuild.GetUser(_targetBot.CurrentUser.Id); + var perms = new OverwritePermissions(sendMessages: PermValue.Deny); + await channel.UpdatePermissionOverwrite(user, perms); + await channel.RemovePermissionOverwrite(user); + await Task.Delay(200); + Assert.AreEqual(PermValue.Inherit, channel.GetPermissionOverwrite(user)?.SendMessages); + } + [TestMethod] + public async Task Test_Permissions_Event() + { + var channel = await _testGuild.CreateTextChannel(GetRandomText()); + var user = _testGuild.GetUser(_targetBot.CurrentUser.Id); + var perms = new OverwritePermissions(sendMessages: PermValue.Deny); + AssertEvent + ("ChannelUpdatedEvent never fired.", + async () => await channel.UpdatePermissionOverwrite(user, perms), + x => _targetBot.ChannelUpdated += x, + x => _targetBot.ChannelUpdated -= x, + (s, e) => e.Channel == channel && (e.After as GuildChannel).PermissionOverwrites.Count() != (e.Before as GuildChannel).PermissionOverwrites.Count()); + } + [TestMethod] + [ExpectedException(typeof(Net.HttpException))] + public async Task Test_Affect_Permissions_Invalid_Channel() + { + var channel = await _testGuild.CreateTextChannel(GetRandomText()); + var user = _testGuild.GetUser(_targetBot.CurrentUser.Id); + var perms = new OverwritePermissions(sendMessages: PermValue.Deny); + await channel.Delete(); + await channel.UpdatePermissionOverwrite(user, perms); + } + + #endregion + + + [ClassCleanup] + public static async Task Cleanup() + { + WaitMany( + (await _hostBot.GetGuilds()).Select(x => x.Owner.Id == _hostBot.CurrentUser.Id ? x.Delete() : x.Leave()), + (await _targetBot.GetGuilds()).Select(x => x.Owner.Id == _targetBot.CurrentUser.Id ? x.Delete() : x.Leave()), + (await _observerBot.GetGuilds()).Select(x => x.Owner.Id == _observerBot.CurrentUser.Id ? x.Delete() : x.Leave())); + + WaitAll( + _hostBot.Disconnect(), + _targetBot.Disconnect(), + _observerBot.Disconnect()); + } + + #region Helpers + + // Task Helpers + + private static void AssertEvent(string msg, Func action, Action> addEvent, Action> removeEvent, Func test = null) + { + AssertEvent(msg, action, addEvent, removeEvent, test, true); + } + private static void AssertNoEvent(string msg, Func action, Action> addEvent, Action> removeEvent, Func test = null) + { + AssertEvent(msg, action, addEvent, removeEvent, test, false); + } + private static void AssertEvent(string msg, Func action, Action> addEvent, Action> removeEvent, Func test, bool assertTrue) + { + ManualResetEventSlim trigger = new ManualResetEventSlim(false); + bool result = false; + + EventHandler handler = (s, e) => + { + if (test != null) + { + result |= test(s, e); + trigger.Set(); + } + else + result = true; + }; + + addEvent(handler); + var task = action(); + trigger.Wait(EventTimeout); + task.Wait(); + removeEvent(handler); + + Assert.AreEqual(assertTrue, result, msg); + } + + private static void AssertEvent(string msg, Func action, Action addEvent, Action removeEvent, Func test, bool assertTrue) + { + ManualResetEventSlim trigger = new ManualResetEventSlim(false); + bool result = false; + + EventHandler handler = (s, e) => + { + if (test != null) + { + result |= test(s); + trigger.Set(); } - else - result = true; - }; - - addEvent(handler); - var task = action(); - trigger.Wait(EventTimeout); - task.Wait(); - removeEvent(handler); - - Assert.AreEqual(assertTrue, result, msg); - } - - private static void WaitAll(params Task[] tasks) - { - Task.WaitAll(tasks); - } - private static void WaitAll(IEnumerable tasks) - { - Task.WaitAll(tasks.ToArray()); - } - private static void WaitMany(params IEnumerable[] tasks) - { - Task.WaitAll(tasks.Where(x => x != null).SelectMany(x => x).ToArray()); - } - } + else + result = true; + }; + + addEvent(handler); + var task = action(); + trigger.Wait(EventTimeout); + task.Wait(); + removeEvent(handler); + + Assert.AreEqual(assertTrue, result, msg); + } + + private static void WaitAll(params Task[] tasks) + { + Task.WaitAll(tasks); + } + private static void WaitAll(IEnumerable tasks) + { + Task.WaitAll(tasks.ToArray()); + } + private static void WaitMany(params IEnumerable[] tasks) + { + Task.WaitAll(tasks.Where(x => x != null).SelectMany(x => x).ToArray()); + } + + #endregion + } }