diff --git a/Discord.Net.sln b/Discord.Net.sln index 48d0dbf93..206c5b54a 100644 --- a/Discord.Net.sln +++ b/Discord.Net.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 -VisualStudioVersion = 14.0.25420.1 +# Visual Studio 15 +VisualStudioVersion = 15.0.25914.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F7F3E124-93C7-4846-AE87-9CE12BD82859}" ProjectSection(SolutionItems) = preProject @@ -9,57 +9,109 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution README.md = README.md EndProjectSection EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Discord.Net", "src\Discord.Net\Discord.Net.xproj", "{496DB20A-A455-4D01-B6BC-90FE6D7C6B81}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net", "src\Discord.Net\Discord.Net.csproj", "{496DB20A-A455-4D01-B6BC-90FE6D7C6B81}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Discord.Net.Core", "src\Discord.Net.Core\Discord.Net.Core.xproj", "{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Core", "src\Discord.Net.Core\Discord.Net.Core.csproj", "{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Impls", "Impls", "{288C363D-A636-4EAE-9AC1-4698B641B26E}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Discord.Net.Rest", "src\Discord.Net.Rest\Discord.Net.Rest.xproj", "{BFC6DC28-0351-4573-926A-D4124244C04F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Rest", "src\Discord.Net.Rest\Discord.Net.Rest.csproj", "{BFC6DC28-0351-4573-926A-D4124244C04F}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Discord.Net.WebSocket", "src\Discord.Net.WebSocket\Discord.Net.WebSocket.xproj", "{22AB6C66-536C-4AC2-BBDB-A8BC4EB6B14D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Rpc", "src\Discord.Net.Rpc\Discord.Net.Rpc.csproj", "{5688A353-121E-40A1-8BFA-B17B91FB48FB}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Discord.Net.Rpc", "src\Discord.Net.Rpc\Discord.Net.Rpc.xproj", "{5688A353-121E-40A1-8BFA-B17B91FB48FB}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Commands", "src\Discord.Net.Commands\Discord.Net.Commands.csproj", "{078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Discord.Net.Commands", "src\Discord.Net.Commands\Discord.Net.Commands.xproj", "{078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.WebSocket", "src\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj", "{688FD1D8-7F01-4539-B2E9-F473C5D699C7}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {496DB20A-A455-4D01-B6BC-90FE6D7C6B81}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {496DB20A-A455-4D01-B6BC-90FE6D7C6B81}.Debug|Any CPU.Build.0 = Debug|Any CPU - {496DB20A-A455-4D01-B6BC-90FE6D7C6B81}.Release|Any CPU.ActiveCfg = Release|Any CPU - {496DB20A-A455-4D01-B6BC-90FE6D7C6B81}.Release|Any CPU.Build.0 = Release|Any CPU + {496DB20A-A455-4D01-B6BC-90FE6D7C6B81}.Debug|x64.ActiveCfg = Debug|Any CPU + {496DB20A-A455-4D01-B6BC-90FE6D7C6B81}.Debug|x64.Build.0 = Debug|Any CPU + {496DB20A-A455-4D01-B6BC-90FE6D7C6B81}.Debug|x86.ActiveCfg = Debug|Any CPU + {496DB20A-A455-4D01-B6BC-90FE6D7C6B81}.Debug|x86.Build.0 = Debug|Any CPU + {496DB20A-A455-4D01-B6BC-90FE6D7C6B81}.Release|Any CPU.ActiveCfg = Debug|Any CPU + {496DB20A-A455-4D01-B6BC-90FE6D7C6B81}.Release|Any CPU.Build.0 = Debug|Any CPU + {496DB20A-A455-4D01-B6BC-90FE6D7C6B81}.Release|x64.ActiveCfg = Debug|Any CPU + {496DB20A-A455-4D01-B6BC-90FE6D7C6B81}.Release|x64.Build.0 = Debug|Any CPU + {496DB20A-A455-4D01-B6BC-90FE6D7C6B81}.Release|x86.ActiveCfg = Debug|Any CPU + {496DB20A-A455-4D01-B6BC-90FE6D7C6B81}.Release|x86.Build.0 = Debug|Any CPU {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|Any CPU.Build.0 = Release|Any CPU + {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|x64.ActiveCfg = Debug|x64 + {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|x64.Build.0 = Debug|x64 + {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|x86.ActiveCfg = Debug|x86 + {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|x86.Build.0 = Debug|x86 + {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|Any CPU.ActiveCfg = Debug|Any CPU + {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|Any CPU.Build.0 = Debug|Any CPU + {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|x64.ActiveCfg = Release|x64 + {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|x64.Build.0 = Release|x64 + {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|x86.ActiveCfg = Release|x86 + {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|x86.Build.0 = Release|x86 {BFC6DC28-0351-4573-926A-D4124244C04F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BFC6DC28-0351-4573-926A-D4124244C04F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {BFC6DC28-0351-4573-926A-D4124244C04F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {BFC6DC28-0351-4573-926A-D4124244C04F}.Release|Any CPU.Build.0 = Release|Any CPU - {22AB6C66-536C-4AC2-BBDB-A8BC4EB6B14D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {22AB6C66-536C-4AC2-BBDB-A8BC4EB6B14D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {22AB6C66-536C-4AC2-BBDB-A8BC4EB6B14D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {22AB6C66-536C-4AC2-BBDB-A8BC4EB6B14D}.Release|Any CPU.Build.0 = Release|Any CPU + {BFC6DC28-0351-4573-926A-D4124244C04F}.Debug|x64.ActiveCfg = Debug|Any CPU + {BFC6DC28-0351-4573-926A-D4124244C04F}.Debug|x64.Build.0 = Debug|Any CPU + {BFC6DC28-0351-4573-926A-D4124244C04F}.Debug|x86.ActiveCfg = Debug|Any CPU + {BFC6DC28-0351-4573-926A-D4124244C04F}.Debug|x86.Build.0 = Debug|Any CPU + {BFC6DC28-0351-4573-926A-D4124244C04F}.Release|Any CPU.ActiveCfg = Debug|Any CPU + {BFC6DC28-0351-4573-926A-D4124244C04F}.Release|Any CPU.Build.0 = Debug|Any CPU + {BFC6DC28-0351-4573-926A-D4124244C04F}.Release|x64.ActiveCfg = Debug|Any CPU + {BFC6DC28-0351-4573-926A-D4124244C04F}.Release|x64.Build.0 = Debug|Any CPU + {BFC6DC28-0351-4573-926A-D4124244C04F}.Release|x86.ActiveCfg = Debug|Any CPU + {BFC6DC28-0351-4573-926A-D4124244C04F}.Release|x86.Build.0 = Debug|Any CPU {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Release|Any CPU.Build.0 = Release|Any CPU + {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Debug|x64.ActiveCfg = Debug|Any CPU + {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Debug|x64.Build.0 = Debug|Any CPU + {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Debug|x86.ActiveCfg = Debug|Any CPU + {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Debug|x86.Build.0 = Debug|Any CPU + {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Release|Any CPU.ActiveCfg = Debug|Any CPU + {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Release|Any CPU.Build.0 = Debug|Any CPU + {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Release|x64.ActiveCfg = Debug|Any CPU + {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Release|x64.Build.0 = Debug|Any CPU + {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Release|x86.ActiveCfg = Debug|Any CPU + {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Release|x86.Build.0 = Debug|Any CPU {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Release|Any CPU.Build.0 = Release|Any CPU + {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Debug|x64.ActiveCfg = Debug|Any CPU + {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Debug|x64.Build.0 = Debug|Any CPU + {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Debug|x86.ActiveCfg = Debug|Any CPU + {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Debug|x86.Build.0 = Debug|Any CPU + {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Release|Any CPU.ActiveCfg = Debug|Any CPU + {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Release|Any CPU.Build.0 = Debug|Any CPU + {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Release|x64.ActiveCfg = Debug|Any CPU + {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Release|x64.Build.0 = Debug|Any CPU + {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Release|x86.ActiveCfg = Debug|Any CPU + {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Release|x86.Build.0 = Debug|Any CPU + {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Debug|x64.ActiveCfg = Debug|x64 + {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Debug|x64.Build.0 = Debug|x64 + {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Debug|x86.ActiveCfg = Debug|x86 + {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Debug|x86.Build.0 = Debug|x86 + {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Release|Any CPU.Build.0 = Release|Any CPU + {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Release|x64.ActiveCfg = Release|x64 + {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Release|x64.Build.0 = Release|x64 + {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Release|x86.ActiveCfg = Release|x86 + {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {BFC6DC28-0351-4573-926A-D4124244C04F} = {288C363D-A636-4EAE-9AC1-4698B641B26E} - {22AB6C66-536C-4AC2-BBDB-A8BC4EB6B14D} = {288C363D-A636-4EAE-9AC1-4698B641B26E} {5688A353-121E-40A1-8BFA-B17B91FB48FB} = {288C363D-A636-4EAE-9AC1-4698B641B26E} + {688FD1D8-7F01-4539-B2E9-F473C5D699C7} = {288C363D-A636-4EAE-9AC1-4698B641B26E} EndGlobalSection EndGlobal diff --git a/README.md b/README.md index 8bc4f4394..268a6d81d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Discord.Net v1.0.0-beta +# Discord.Net v1.0.0-beta2 [![MyGet](https://img.shields.io/myget/discord-net/vpre/Discord.Net.svg)](https://www.myget.org/feed/Packages/discord-net) [![MyGet Build Status](https://www.myget.org/BuildSource/Badge/discord-net?identifier=15bf7c42-22dd-4406-93e5-3cafc62bbc85)](https://www.myget.org/) [![Discord](https://discordapp.com/api/guilds/81384788765712384/widget.png)](https://discord.gg/0SBTUU1wZTYLhAAW) @@ -19,20 +19,15 @@ Bleeding edge builds are available using our MyGet feed (`https://www.myget.org/ ## Compiling In order to compile Discord.Net, you require the following: -### Using Visual Studio 2015 -- [VS2015 Update 3](https://www.microsoft.com/net/core#windows) -- [.Net Core 1.0 VS Plugin](https://www.microsoft.com/net/core#windows) +### Using Visual Studio +- [Visual Studio 2017 RC](https://www.microsoft.com/net/core#windowsvs2017) -### Using CLI -- [.Net Core 1.0 SDK](https://www.microsoft.com/net/core) +The .NET Core and Docker (Preview) workload is required during Visual Studio installation. -## Known Issues - -### WebSockets -The current stable .Net Core websocket package does not support Linux, or pre-Win8. +### Using Command Line +- [.Net Core 1.1 SDK](https://www.microsoft.com/net/download/core) -#### Linux -Add the latest version of `System.Net.WebSockets.Client` from the .Net Core MyGet feed (`https://dotnet.myget.org/F/dotnet-core/api/v3/index.json`) to your project. +## Known Issues -#### Windows 7 and earlier -There is currently no workaround, track the issue [here](https://github.com/dotnet/corefx/issues/9503). +### WebSockets (Win7 and earlier) +.Net Core 1.1 does not support WebSockets on Win7 and earlier. Track the issue [here](https://github.com/dotnet/corefx/issues/9503). diff --git a/build.bat b/build.bat index 26df9c77a..95a7c5d9a 100644 --- a/build.bat +++ b/build.bat @@ -1,8 +1,15 @@ @echo Off dotnet restore -dotnet pack "src\Discord.Net" -c "%Configuration%" -o "artifacts" -dotnet pack "src\Discord.Net.Core" -c "%Configuration%" -o "artifacts" -dotnet pack "src\Discord.Net.Rest" -c "%Configuration%" -o "artifacts" -dotnet pack "src\Discord.Net.WebSocket" -c "%Configuration%" -o "artifacts" -dotnet pack "src\Discord.Net.Rpc" -c "%Configuration%" -o "artifacts" -dotnet pack "src\Discord.Net.Commands" -c "%Configuration%" -o "artifacts" \ No newline at end of file +dotnet pack "src\Discord.Net" -c "%Configuration%" -o "artifacts" --version-suffix "%PrereleaseTag%" +dotnet pack "src\Discord.Net.Core" -c "%Configuration%" -o "artifacts" --version-suffix "%PrereleaseTag%" +dotnet pack "src\Discord.Net.Commands" -c "%Configuration%" -o "artifacts" --version-suffix "%PrereleaseTag%" +dotnet pack "src\Discord.Net.Rest" -c "%Configuration%" -o "artifacts" --version-suffix "%PrereleaseTag%" +dotnet pack "src\Discord.Net.WebSocket" -c "%Configuration%" -o "artifacts" --version-suffix "%PrereleaseTag%" +dotnet pack "src\Discord.Net.Rpc" -c "%Configuration%" -o "artifacts" --version-suffix "%PrereleaseTag%" + +REM dotnet pack "src\Discord.Net\Discord.Net.csproj" -c "%Configuration%" -o "artifacts" --version-suffix "%PrereleaseTag%" +REM dotnet pack "src\Discord.Net.Core\Discord.Net.Core.csproj" -c "%Configuration%" -o "artifacts" --version-suffix "%PrereleaseTag%" +REM dotnet pack "src\Discord.Net.Commands\Discord.Net.Commands.csproj" -c "%Configuration%" -o "artifacts" --version-suffix "%PrereleaseTag%" +REM dotnet pack "src\Discord.Net.Rest\Discord.Net.Rest.csproj" -c "%Configuration%" -o "artifacts" --version-suffix "%PrereleaseTag%" +REM dotnet pack "src\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj" -c "%Configuration%" -o "artifacts" --version-suffix "%PrereleaseTag%" +REM dotnet pack "src\Discord.Net.Rpc\Discord.Net.Rpc.csproj" -c "%Configuration%" -o "artifacts" --version-suffix "%PrereleaseTag%" \ No newline at end of file diff --git a/docs/guides/samples.md b/docs/guides/samples.md index ef85e4898..4406f2f1e 100644 --- a/docs/guides/samples.md +++ b/docs/guides/samples.md @@ -13,8 +13,8 @@ title: Samples #### Changing the bot's status -[!code-sharp[Bot Status](samples/faq/status.cs)] +[!code-csharp[Bot Status](samples/faq/status.cs)] #### Sending a message to a channel -[!code-csharp[Message to Channel](samples/faq/send_message.cs)] \ No newline at end of file +[!code-csharp[Message to Channel](samples/faq/send_message.cs)] diff --git a/docs/guides/samples/faq/status.cs b/docs/guides/samples/faq/status.cs index 4d6a29924..8025dd7fd 100644 --- a/docs/guides/samples/faq/status.cs +++ b/docs/guides/samples/faq/status.cs @@ -1,8 +1,5 @@ public async Task ModifyStatus() { - await (await _client.GetCurrentUserAsync()).ModifyStatusAsync(x => - { - x.Status = UserStatus.Idle; - x.Game = new Game("Type !help for help"); - }); -} \ No newline at end of file + await _client.SetStatus(UserStatus.Idle); + await _client.SetGame("Type !help for help"); +} diff --git a/global.json b/global.json deleted file mode 100644 index 9a66d5edc..000000000 --- a/global.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "projects": [ "src", "test" ] -} diff --git a/src/Discord.Net.Commands/Attributes/CommandAttribute.cs b/src/Discord.Net.Commands/Attributes/CommandAttribute.cs index baac75ff9..5ae6092eb 100644 --- a/src/Discord.Net.Commands/Attributes/CommandAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/CommandAttribute.cs @@ -6,7 +6,7 @@ namespace Discord.Commands public class CommandAttribute : Attribute { public string Text { get; } - public RunMode RunMode { get; set; } = RunMode.Sync; + public RunMode RunMode { get; set; } = RunMode.Default; public CommandAttribute() { diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs new file mode 100644 index 000000000..914978192 --- /dev/null +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace Discord.Commands +{ + /// + /// This attribute requires that the bot has a speicifed permission in the channel a command is invoked in. + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public class RequireBotPermissionAttribute : PreconditionAttribute + { + public GuildPermission? GuildPermission { get; } + public ChannelPermission? ChannelPermission { get; } + + /// + /// Require that the bot account has a specified GuildPermission + /// + /// This precondition will always fail if the command is being invoked in a private channel. + /// The GuildPermission that the bot must have. Multiple permissions can be specified by ORing or ANDing the permissions together. + public RequireBotPermissionAttribute(GuildPermission permission) + { + GuildPermission = permission; + ChannelPermission = null; + } + /// + /// Require that the bot account has a specified ChannelPermission. + /// + /// The ChannelPermission that the bot must have. Multiple permissions can be specified by ORing or ANDing the permissions together. + /// + /// + /// [Command("permission")] + /// [RequireBotPermission(ChannelPermission.ManageMessages)] + /// public async Task Purge() + /// { + /// } + /// + /// + public RequireBotPermissionAttribute(ChannelPermission permission) + { + ChannelPermission = permission; + GuildPermission = null; + } + + public override async Task CheckPermissions(CommandContext context, CommandInfo command, IDependencyMap map) + { + var guildUser = await context.Guild.GetCurrentUserAsync(); + + if (GuildPermission.HasValue) + { + if (guildUser == null) + return PreconditionResult.FromError("Command must be used in a guild channel"); + if (!guildUser.GuildPermissions.Has(GuildPermission.Value)) + return PreconditionResult.FromError($"Command requires guild permission {GuildPermission.Value}"); + } + + if (ChannelPermission.HasValue) + { + var guildChannel = context.Channel as IGuildChannel; + + ChannelPermissions perms; + if (guildChannel != null) + perms = guildUser.GetPermissions(guildChannel); + else + perms = ChannelPermissions.All(guildChannel); + + if (!perms.Has(ChannelPermission.Value)) + return PreconditionResult.FromError($"Command requires channel permission {ChannelPermission.Value}"); + } + + return PreconditionResult.FromSuccess(); + } + } +} diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs index 1cd32e72e..beadbbc89 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs @@ -11,11 +11,27 @@ namespace Discord.Commands Group = 0x04 } + /// + /// Require that the command be invoked in a specified context. + /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public class RequireContextAttribute : PreconditionAttribute { public ContextType Contexts { get; } + /// + /// Require that the command be invoked in a specified context. + /// + /// The type of context the command can be invoked in. Multiple contexts can be speicifed by ORing the contexts together. + /// + /// + /// [Command("private_only")] + /// [RequireContext(ContextType.DM | ContextType.Group)] + /// public async Task PrivateOnly() + /// { + /// } + /// + /// public RequireContextAttribute(ContextType contexts) { Contexts = contexts; diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs new file mode 100644 index 000000000..8992cd115 --- /dev/null +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Discord; + +namespace Discord.Commands +{ + /// + /// Require that the command is invoked by the owner of the bot. + /// + /// This precondition will only work if the bot is a bot account. + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public class RequireOwnerAttribute : PreconditionAttribute + { + public override async Task CheckPermissions(CommandContext context, CommandInfo command, IDependencyMap map) + { + var application = await context.Client.GetApplicationInfoAsync(); + if (context.User.Id == application.Owner.Id) return PreconditionResult.FromSuccess(); + return PreconditionResult.FromError("Command can only be run by the owner of the bot"); + } + } +} diff --git a/src/Discord.Net.Commands/Attributes/Preconditions/RequirePermissionAttribute.cs b/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs similarity index 54% rename from src/Discord.Net.Commands/Attributes/Preconditions/RequirePermissionAttribute.cs rename to src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs index 26aeac5ec..17cf234aa 100644 --- a/src/Discord.Net.Commands/Attributes/Preconditions/RequirePermissionAttribute.cs +++ b/src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs @@ -3,18 +3,40 @@ using System.Threading.Tasks; namespace Discord.Commands { + /// + /// This attribute requires that the user invoking the command has a specified permission. + /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] - public class RequirePermissionAttribute : PreconditionAttribute + public class RequireUserPermissionAttribute : PreconditionAttribute { public GuildPermission? GuildPermission { get; } public ChannelPermission? ChannelPermission { get; } - public RequirePermissionAttribute(GuildPermission permission) + /// + /// Require that the user invoking the command has a specified GuildPermission + /// + /// This precondition will always fail if the command is being invoked in a private channel. + /// The GuildPermission that the user must have. Multiple permissions can be specified by ORing or ANDing the permissions together. + public RequireUserPermissionAttribute(GuildPermission permission) { GuildPermission = permission; ChannelPermission = null; } - public RequirePermissionAttribute(ChannelPermission permission) + /// + /// Require that the user invoking the command has a specified ChannelPermission. + /// + /// The ChannelPermission that the user must have. Multiple permissions can be specified by ORing or ANDing the permissions together. + /// + /// + /// [Command("permission")] + /// [RequireUserPermission(ChannelPermission.ReadMessageHistory & ChannelPermission.ReadMessages)] + /// public async Task HasPermission() + /// { + /// await ReplyAsync("You can read messages and the message history!"); + /// } + /// + /// + public RequireUserPermissionAttribute(ChannelPermission permission) { ChannelPermission = permission; GuildPermission = null; diff --git a/src/Discord.Net.Commands/Builders/CommandBuilder.cs b/src/Discord.Net.Commands/Builders/CommandBuilder.cs new file mode 100644 index 000000000..9b983fd1f --- /dev/null +++ b/src/Discord.Net.Commands/Builders/CommandBuilder.cs @@ -0,0 +1,120 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using System.Collections.Generic; + +namespace Discord.Commands.Builders +{ + public class CommandBuilder + { + private readonly List _preconditions; + private readonly List _parameters; + private readonly List _aliases; + + public ModuleBuilder Module { get; } + internal Func Callback { get; set; } + + public string Name { get; set; } + public string Summary { get; set; } + public string Remarks { get; set; } + public RunMode RunMode { get; set; } + public int Priority { get; set; } + + public IReadOnlyList Preconditions => _preconditions; + public IReadOnlyList Parameters => _parameters; + public IReadOnlyList Aliases => _aliases; + + //Automatic + internal CommandBuilder(ModuleBuilder module) + { + Module = module; + + _preconditions = new List(); + _parameters = new List(); + _aliases = new List(); + } + //User-defined + internal CommandBuilder(ModuleBuilder module, string primaryAlias, Func callback) + : this(module) + { + Discord.Preconditions.NotNull(primaryAlias, nameof(primaryAlias)); + Discord.Preconditions.NotNull(callback, nameof(callback)); + + Callback = callback; + _aliases.Add(primaryAlias); + } + + public CommandBuilder WithName(string name) + { + Name = name; + return this; + } + public CommandBuilder WithSummary(string summary) + { + Summary = summary; + return this; + } + public CommandBuilder WithRemarks(string remarks) + { + Remarks = remarks; + return this; + } + public CommandBuilder WithRunMode(RunMode runMode) + { + RunMode = runMode; + return this; + } + public CommandBuilder WithPriority(int priority) + { + Priority = priority; + return this; + } + + public CommandBuilder AddAliases(params string[] aliases) + { + _aliases.AddRange(aliases); + return this; + } + public CommandBuilder AddPrecondition(PreconditionAttribute precondition) + { + _preconditions.Add(precondition); + return this; + } + public CommandBuilder AddParameter(string name, Type type, Action createFunc) + { + var param = new ParameterBuilder(this, name, type); + createFunc(param); + _parameters.Add(param); + return this; + } + internal CommandBuilder AddParameter(Action createFunc) + { + var param = new ParameterBuilder(this); + createFunc(param); + _parameters.Add(param); + return this; + } + + internal CommandInfo Build(ModuleInfo info, CommandService service) + { + //Default name to first alias + if (Name == null) + Name = _aliases[0]; + + if (_parameters.Count > 0) + { + var lastParam = _parameters[_parameters.Count - 1]; + + var firstMultipleParam = _parameters.FirstOrDefault(x => x.IsMultiple); + if ((firstMultipleParam != null) && (firstMultipleParam != lastParam)) + throw new InvalidOperationException("Only the last parameter in a command may have the Multiple flag."); + + var firstRemainderParam = _parameters.FirstOrDefault(x => x.IsRemainder); + if ((firstRemainderParam != null) && (firstRemainderParam != lastParam)) + throw new InvalidOperationException("Only the last parameter in a command may have the Remainder flag."); + } + + return new CommandInfo(this, info, service); + } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Commands/Builders/ModuleBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleBuilder.cs new file mode 100644 index 000000000..083de8e81 --- /dev/null +++ b/src/Discord.Net.Commands/Builders/ModuleBuilder.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Discord.Commands.Builders +{ + public class ModuleBuilder + { + private readonly List _commands; + private readonly List _submodules; + private readonly List _preconditions; + private readonly List _aliases; + + public CommandService Service { get; } + public ModuleBuilder Parent { get; } + public string Name { get; set; } + public string Summary { get; set; } + public string Remarks { get; set; } + + public IReadOnlyList Commands => _commands; + public IReadOnlyList Modules => _submodules; + public IReadOnlyList Preconditions => _preconditions; + public IReadOnlyList Aliases => _aliases; + + //Automatic + internal ModuleBuilder(CommandService service, ModuleBuilder parent) + { + Service = service; + Parent = parent; + + _commands = new List(); + _submodules = new List(); + _preconditions = new List(); + _aliases = new List(); + } + //User-defined + internal ModuleBuilder(CommandService service, ModuleBuilder parent, string primaryAlias) + : this(service, parent) + { + Discord.Preconditions.NotNull(primaryAlias, nameof(primaryAlias)); + + _aliases = new List { primaryAlias }; + } + + public ModuleBuilder WithName(string name) + { + Name = name; + return this; + } + public ModuleBuilder WithSummary(string summary) + { + Summary = summary; + return this; + } + public ModuleBuilder WithRemarks(string remarks) + { + Remarks = remarks; + return this; + } + + public ModuleBuilder AddAlias(params string[] newAliases) + { + _aliases.AddRange(newAliases); + return this; + } + public ModuleBuilder AddPrecondition(PreconditionAttribute precondition) + { + _preconditions.Add(precondition); + return this; + } + public ModuleBuilder AddCommand(string primaryAlias, Func callback, Action createFunc) + { + var builder = new CommandBuilder(this, primaryAlias, callback); + createFunc(builder); + _commands.Add(builder); + return this; + } + internal ModuleBuilder AddCommand(Action createFunc) + { + var builder = new CommandBuilder(this); + createFunc(builder); + _commands.Add(builder); + return this; + } + public ModuleBuilder AddModule(string primaryAlias, Action createFunc) + { + var builder = new ModuleBuilder(Service, this, primaryAlias); + createFunc(builder); + _submodules.Add(builder); + return this; + } + internal ModuleBuilder AddModule(Action createFunc) + { + var builder = new ModuleBuilder(Service, this); + createFunc(builder); + _submodules.Add(builder); + return this; + } + + public ModuleInfo Build(CommandService service) + { + //Default name to first alias + if (Name == null) + Name = _aliases[0]; + + return new ModuleInfo(this, service); + } + } +} diff --git a/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs new file mode 100644 index 000000000..aaec43161 --- /dev/null +++ b/src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs @@ -0,0 +1,232 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Reflection; +using System.Threading.Tasks; + +using Discord.Commands.Builders; + +namespace Discord.Commands +{ + internal static class ModuleClassBuilder + { + private static readonly TypeInfo _moduleTypeInfo = typeof(ModuleBase).GetTypeInfo(); + + public static IEnumerable Search(Assembly assembly) + { + foreach (var type in assembly.ExportedTypes) + { + var typeInfo = type.GetTypeInfo(); + if (IsValidModuleDefinition(typeInfo) && + !typeInfo.IsDefined(typeof(DontAutoLoadAttribute))) + { + yield return typeInfo; + } + } + } + + public static Dictionary Build(CommandService service, params TypeInfo[] validTypes) => Build(validTypes, service); + public static Dictionary Build(IEnumerable validTypes, CommandService service) + { + if (!validTypes.Any()) + throw new InvalidOperationException("Could not find any valid modules from the given selection"); + + var topLevelGroups = validTypes.Where(x => x.DeclaringType == null); + var subGroups = validTypes.Intersect(topLevelGroups); + + var builtTypes = new List(); + + var result = new Dictionary(); + + foreach (var typeInfo in topLevelGroups) + { + // TODO: This shouldn't be the case; may be safe to remove? + if (result.ContainsKey(typeInfo.AsType())) + continue; + + var module = new ModuleBuilder(service, null); + + BuildModule(module, typeInfo, service); + BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service); + + result[typeInfo.AsType()] = module.Build(service); + } + + return result; + } + + private static void BuildSubTypes(ModuleBuilder builder, IEnumerable subTypes, List builtTypes, CommandService service) + { + foreach (var typeInfo in subTypes) + { + if (!IsValidModuleDefinition(typeInfo)) + continue; + + if (builtTypes.Contains(typeInfo)) + continue; + + builder.AddModule((module) => { + BuildModule(module, typeInfo, service); + BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service); + }); + + builtTypes.Add(typeInfo); + } + } + + private static void BuildModule(ModuleBuilder builder, TypeInfo typeInfo, CommandService service) + { + var attributes = typeInfo.GetCustomAttributes(); + + foreach (var attribute in attributes) + { + // TODO: C#7 type switch + if (attribute is NameAttribute) + builder.Name = (attribute as NameAttribute).Text; + else if (attribute is SummaryAttribute) + builder.Summary = (attribute as SummaryAttribute).Text; + else if (attribute is RemarksAttribute) + builder.Remarks = (attribute as RemarksAttribute).Text; + else if (attribute is AliasAttribute) + builder.AddAlias((attribute as AliasAttribute).Aliases); + else if (attribute is GroupAttribute) + { + var groupAttr = attribute as GroupAttribute; + builder.Name = builder.Name ?? groupAttr.Prefix; + builder.AddAlias(groupAttr.Prefix); + } + else if (attribute is PreconditionAttribute) + builder.AddPrecondition(attribute as PreconditionAttribute); + } + + if (builder.Name == null) + builder.Name = typeInfo.Name; + + var validCommands = typeInfo.DeclaredMethods.Where(x => IsValidCommandDefinition(x)); + + foreach (var method in validCommands) + { + builder.AddCommand((command) => { + BuildCommand(command, typeInfo, method, service); + }); + } + } + + private static void BuildCommand(CommandBuilder builder, TypeInfo typeInfo, MethodInfo method, CommandService service) + { + var attributes = method.GetCustomAttributes(); + + foreach (var attribute in attributes) + { + // TODO: C#7 type switch + if (attribute is CommandAttribute) + { + var cmdAttr = attribute as CommandAttribute; + builder.AddAliases(cmdAttr.Text); + builder.RunMode = cmdAttr.RunMode; + builder.Name = builder.Name ?? cmdAttr.Text; + } + else if (attribute is NameAttribute) + builder.Name = (attribute as NameAttribute).Text; + else if (attribute is PriorityAttribute) + builder.Priority = (attribute as PriorityAttribute).Priority; + else if (attribute is SummaryAttribute) + builder.Summary = (attribute as SummaryAttribute).Text; + else if (attribute is RemarksAttribute) + builder.Remarks = (attribute as RemarksAttribute).Text; + else if (attribute is AliasAttribute) + builder.AddAliases((attribute as AliasAttribute).Aliases); + else if (attribute is PreconditionAttribute) + builder.AddPrecondition(attribute as PreconditionAttribute); + } + + if (builder.Name == null) + builder.Name = method.Name; + + var parameters = method.GetParameters(); + int pos = 0, count = parameters.Length; + foreach (var paramInfo in parameters) + { + builder.AddParameter((parameter) => { + BuildParameter(parameter, paramInfo, pos++, count, service); + }); + } + + var createInstance = ReflectionUtils.CreateBuilder(typeInfo, service); + + builder.Callback = (ctx, args, map) => { + var instance = createInstance(map); + instance.Context = ctx; + try + { + return method.Invoke(instance, args) as Task ?? Task.CompletedTask; + } + finally{ + (instance as IDisposable)?.Dispose(); + } + }; + } + + private static void BuildParameter(ParameterBuilder builder, System.Reflection.ParameterInfo paramInfo, int position, int count, CommandService service) + { + var attributes = paramInfo.GetCustomAttributes(); + var paramType = paramInfo.ParameterType; + + builder.Name = paramInfo.Name; + + builder.IsOptional = paramInfo.IsOptional; + builder.DefaultValue = paramInfo.HasDefaultValue ? paramInfo.DefaultValue : null; + + foreach (var attribute in attributes) + { + // TODO: C#7 type switch + if (attribute is SummaryAttribute) + builder.Summary = (attribute as SummaryAttribute).Text; + else if (attribute is ParamArrayAttribute) + { + builder.IsMultiple = true; + paramType = paramType.GetElementType(); + } + else if (attribute is RemainderAttribute) + { + if (position != count-1) + throw new InvalidOperationException("Remainder parameters must be the last parameter in a command."); + + builder.IsRemainder = true; + } + } + + var reader = service.GetTypeReader(paramType); + if (reader == null) + { + var paramTypeInfo = paramType.GetTypeInfo(); + if (paramTypeInfo.IsEnum) + { + reader = EnumTypeReader.GetReader(paramType); + service.AddTypeReader(paramType, reader); + } + else + { + throw new InvalidOperationException($"{paramType.FullName} is not supported as a command parameter, are you missing a TypeReader?"); + } + } + + builder.ParameterType = paramType; + builder.TypeReader = reader; + } + + private static bool IsValidModuleDefinition(TypeInfo typeInfo) + { + return _moduleTypeInfo.IsAssignableFrom(typeInfo) && + !typeInfo.IsAbstract; + } + + private static bool IsValidCommandDefinition(MethodInfo methodInfo) + { + return methodInfo.IsDefined(typeof(CommandAttribute)) && + methodInfo.ReturnType == typeof(Task) && + !methodInfo.IsStatic && + !methodInfo.IsGenericMethod; + } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Commands/Builders/ParameterBuilder.cs b/src/Discord.Net.Commands/Builders/ParameterBuilder.cs new file mode 100644 index 000000000..801a10080 --- /dev/null +++ b/src/Discord.Net.Commands/Builders/ParameterBuilder.cs @@ -0,0 +1,79 @@ +using System; +using System.Reflection; + +namespace Discord.Commands.Builders +{ + public class ParameterBuilder + { + public CommandBuilder Command { get; } + public string Name { get; internal set; } + public Type ParameterType { get; internal set; } + + public TypeReader TypeReader { get; set; } + public bool IsOptional { get; set; } + public bool IsRemainder { get; set; } + public bool IsMultiple { get; set; } + public object DefaultValue { get; set; } + public string Summary { get; set; } + + //Automatic + internal ParameterBuilder(CommandBuilder command) + { + Command = command; + } + //User-defined + internal ParameterBuilder(CommandBuilder command, string name, Type type) + : this(command) + { + Preconditions.NotNull(name, nameof(name)); + + Name = name; + SetType(type); + } + + internal void SetType(Type type) + { + TypeReader = Command.Module.Service.GetTypeReader(type); + + if (type.GetTypeInfo().IsValueType) + DefaultValue = Activator.CreateInstance(type); + else if (type.IsArray) + type = ParameterType.GetElementType(); + ParameterType = type; + } + + public ParameterBuilder WithSummary(string summary) + { + Summary = summary; + return this; + } + public ParameterBuilder WithDefault(object defaultValue) + { + DefaultValue = defaultValue; + return this; + } + public ParameterBuilder WithIsOptional(bool isOptional) + { + IsOptional = isOptional; + return this; + } + public ParameterBuilder WithIsRemainder(bool isRemainder) + { + IsRemainder = isRemainder; + return this; + } + public ParameterBuilder WithIsMultiple(bool isMultiple) + { + IsMultiple = isMultiple; + return this; + } + + internal ParameterInfo Build(CommandInfo info) + { + if (TypeReader == null) + throw new InvalidOperationException($"No default TypeReader found, one must be specified"); + + return new ParameterInfo(this, info, Command.Module.Service); + } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Commands/CommandInfo.cs b/src/Discord.Net.Commands/CommandInfo.cs deleted file mode 100644 index 47aae1ae2..000000000 --- a/src/Discord.Net.Commands/CommandInfo.cs +++ /dev/null @@ -1,284 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics; -using System.Linq; -using System.Reflection; -using System.Threading.Tasks; - -namespace Discord.Commands -{ - [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class CommandInfo - { - private static readonly MethodInfo _convertParamsMethod = typeof(CommandInfo).GetTypeInfo().GetDeclaredMethod(nameof(ConvertParamsList)); - private static readonly ConcurrentDictionary, object>> _arrayConverters = new ConcurrentDictionary, object>>(); - - private readonly Func _action; - - public MethodInfo Source { get; } - public ModuleInfo Module { get; } - public string Name { get; } - public string Summary { get; } - public string Remarks { get; } - public string Text { get; } - public int Priority { get; } - public bool HasVarArgs { get; } - public RunMode RunMode { get; } - public IReadOnlyList Aliases { get; } - public IReadOnlyList Parameters { get; } - public IReadOnlyList Preconditions { get; } - - internal CommandInfo(MethodInfo source, ModuleInfo module, CommandAttribute attribute, string groupPrefix) - { - try - { - Source = source; - Module = module; - - Name = source.Name; - - if (attribute.Text == null) - Text = groupPrefix; - RunMode = attribute.RunMode; - - if (groupPrefix != "") - groupPrefix += " "; - - if (attribute.Text != null) - Text = groupPrefix + attribute.Text; - - var aliasesBuilder = ImmutableArray.CreateBuilder(); - - aliasesBuilder.Add(Text); - - var aliasesAttr = source.GetCustomAttribute(); - if (aliasesAttr != null) - aliasesBuilder.AddRange(aliasesAttr.Aliases.Select(x => groupPrefix + x)); - - Aliases = aliasesBuilder.ToImmutable(); - - var nameAttr = source.GetCustomAttribute(); - if (nameAttr != null) - Name = nameAttr.Text; - - var summary = source.GetCustomAttribute(); - if (summary != null) - Summary = summary.Text; - - var remarksAttr = source.GetCustomAttribute(); - if (remarksAttr != null) - Remarks = remarksAttr.Text; - - var priorityAttr = source.GetCustomAttribute(); - Priority = priorityAttr?.Priority ?? 0; - - Parameters = BuildParameters(source); - HasVarArgs = Parameters.Count > 0 ? Parameters[Parameters.Count - 1].IsMultiple : false; - Preconditions = BuildPreconditions(source); - _action = BuildAction(source); - } - catch (Exception ex) - { - throw new Exception($"Failed to build command {source.DeclaringType.FullName}.{source.Name}", ex); - } - } - - public async Task CheckPreconditions(CommandContext context, IDependencyMap map = null) - { - if (map == null) - map = DependencyMap.Empty; - - foreach (PreconditionAttribute precondition in Module.Preconditions) - { - var result = await precondition.CheckPermissions(context, this, map).ConfigureAwait(false); - if (!result.IsSuccess) - return result; - } - - foreach (PreconditionAttribute precondition in Preconditions) - { - var result = await precondition.CheckPermissions(context, this, map).ConfigureAwait(false); - if (!result.IsSuccess) - return result; - } - - return PreconditionResult.FromSuccess(); - } - - public async Task Parse(CommandContext context, SearchResult searchResult, PreconditionResult? preconditionResult = null) - { - if (!searchResult.IsSuccess) - return ParseResult.FromError(searchResult); - if (preconditionResult != null && !preconditionResult.Value.IsSuccess) - return ParseResult.FromError(preconditionResult.Value); - - string input = searchResult.Text; - var matchingAliases = Aliases.Where(alias => input.StartsWith(alias)); - - string matchingAlias = ""; - foreach (string alias in matchingAliases) - { - if (alias.Length > matchingAlias.Length) - matchingAlias = alias; - } - - input = input.Substring(matchingAlias.Length); - - return await CommandParser.ParseArgs(this, context, input, 0).ConfigureAwait(false); - } - public Task Execute(CommandContext context, ParseResult parseResult, IDependencyMap map) - { - if (!parseResult.IsSuccess) - return Task.FromResult(ExecuteResult.FromError(parseResult)); - - var argList = new object[parseResult.ArgValues.Count]; - for (int i = 0; i < parseResult.ArgValues.Count; i++) - { - if (!parseResult.ArgValues[i].IsSuccess) - return Task.FromResult(ExecuteResult.FromError(parseResult.ArgValues[i])); - argList[i] = parseResult.ArgValues[i].Values.First().Value; - } - - var paramList = new object[parseResult.ParamValues.Count]; - for (int i = 0; i < parseResult.ParamValues.Count; i++) - { - if (!parseResult.ParamValues[i].IsSuccess) - return Task.FromResult(ExecuteResult.FromError(parseResult.ParamValues[i])); - paramList[i] = parseResult.ParamValues[i].Values.First().Value; - } - - return Execute(context, argList, paramList, map); - } - public async Task Execute(CommandContext context, IEnumerable argList, IEnumerable paramList, IDependencyMap map) - { - if (map == null) - map = DependencyMap.Empty; - - try - { - var args = GenerateArgs(argList, paramList); - switch (RunMode) - { - case RunMode.Sync: //Always sync - await _action(context, args, map).ConfigureAwait(false); - break; - case RunMode.Mixed: //Sync until first await statement - var t1 = _action(context, args, map); - break; - case RunMode.Async: //Always async - var t2 = Task.Run(() => _action(context, args, map)); - break; - } - return ExecuteResult.FromSuccess(); - } - catch (Exception ex) - { - return ExecuteResult.FromError(ex); - } - } - - private IReadOnlyList BuildPreconditions(MethodInfo methodInfo) - { - return methodInfo.GetCustomAttributes().ToImmutableArray(); - } - - private IReadOnlyList BuildParameters(MethodInfo methodInfo) - { - var parameters = methodInfo.GetParameters(); - - var paramBuilder = ImmutableArray.CreateBuilder(parameters.Length); - for (int i = 0; i < parameters.Length; i++) - { - var parameter = parameters[i]; - var type = parameter.ParameterType; - - //Detect 'params' - bool isMultiple = parameter.GetCustomAttribute() != null; - if (isMultiple) - type = type.GetElementType(); - - var reader = Module.Service.GetTypeReader(type); - var typeInfo = type.GetTypeInfo(); - - //Detect enums - if (reader == null && typeInfo.IsEnum) - { - reader = EnumTypeReader.GetReader(type); - Module.Service.AddTypeReader(type, reader); - } - - if (reader == null) - throw new InvalidOperationException($"{type.FullName} is not supported as a command parameter, are you missing a TypeReader?"); - - bool isRemainder = parameter.GetCustomAttribute() != null; - if (isRemainder && i != parameters.Length - 1) - throw new InvalidOperationException("Remainder parameters must be the last parameter in a command."); - - string name = parameter.Name; - string summary = parameter.GetCustomAttribute()?.Text; - bool isOptional = parameter.IsOptional; - object defaultValue = parameter.HasDefaultValue ? parameter.DefaultValue : null; - - paramBuilder.Add(new CommandParameter(parameters[i], name, summary, type, reader, isOptional, isRemainder, isMultiple, defaultValue)); - } - return paramBuilder.ToImmutable(); - } - private Func BuildAction(MethodInfo methodInfo) - { - if (methodInfo.ReturnType != typeof(Task)) - throw new InvalidOperationException("Commands must return a non-generic Task."); - - return (context, args, map) => - { - var instance = Module.CreateInstance(map); - instance.Context = context; - try - { - return methodInfo.Invoke(instance, args) as Task ?? Task.CompletedTask; - } - finally - { - (instance as IDisposable)?.Dispose(); - } - }; - } - - private object[] GenerateArgs(IEnumerable argList, IEnumerable paramsList) - { - int argCount = Parameters.Count; - var array = new object[Parameters.Count]; - if (HasVarArgs) - argCount--; - - int i = 0; - foreach (var arg in argList) - { - if (i == argCount) - throw new InvalidOperationException("Command was invoked with too many parameters"); - array[i++] = arg; - } - if (i < argCount) - throw new InvalidOperationException("Command was invoked with too few parameters"); - - if (HasVarArgs) - { - var func = _arrayConverters.GetOrAdd(Parameters[Parameters.Count - 1].ElementType, t => - { - var method = _convertParamsMethod.MakeGenericMethod(t); - return (Func, object>)method.CreateDelegate(typeof(Func, object>)); - }); - array[i] = func(paramsList); - } - - return array; - } - - private static T[] ConvertParamsList(IEnumerable paramsList) - => paramsList.Cast().ToArray(); - - public override string ToString() => Name; - private string DebuggerDisplay => $"{Module.Name}.{Name} ({Text})"; - } -} diff --git a/src/Discord.Net.Commands/CommandParser.cs b/src/Discord.Net.Commands/CommandParser.cs index 1808b705d..53ea1330f 100644 --- a/src/Discord.Net.Commands/CommandParser.cs +++ b/src/Discord.Net.Commands/CommandParser.cs @@ -15,7 +15,7 @@ namespace Discord.Commands public static async Task ParseArgs(CommandInfo command, CommandContext context, string input, int startPos) { - CommandParameter curParam = null; + ParameterInfo curParam = null; StringBuilder argBuilder = new StringBuilder(input.Length); int endPos = input.Length; var curPart = ParserPart.None; @@ -65,7 +65,9 @@ namespace Discord.Commands return ParseResult.FromError(CommandError.ParseFailed, "There must be at least one character of whitespace between arguments."); else { - curParam = command.Parameters.Count > argList.Count ? command.Parameters[argList.Count] : null; + if (curParam == null) + curParam = command.Parameters.Count > argList.Count ? command.Parameters[argList.Count] : null; + if (curParam != null && curParam.IsRemainder) { argBuilder.Append(c); @@ -116,11 +118,7 @@ namespace Discord.Commands { paramList.Add(typeReaderResult); - if (curPos == endPos) - { - curParam = null; - curPart = ParserPart.None; - } + curPart = ParserPart.None; } else { diff --git a/src/Discord.Net.Commands/CommandService.cs b/src/Discord.Net.Commands/CommandService.cs index ef0dba7e7..26d811440 100644 --- a/src/Discord.Net.Commands/CommandService.cs +++ b/src/Discord.Net.Commands/CommandService.cs @@ -7,24 +7,30 @@ using System.Reflection; using System.Threading; using System.Threading.Tasks; +using Discord.Commands.Builders; + namespace Discord.Commands { public class CommandService { - private static readonly TypeInfo _moduleTypeInfo = typeof(ModuleBase).GetTypeInfo(); - private readonly SemaphoreSlim _moduleLock; - private readonly ConcurrentDictionary _moduleDefs; + private readonly ConcurrentDictionary _typedModuleDefs; private readonly ConcurrentDictionary _typeReaders; + private readonly ConcurrentBag _moduleDefs; private readonly CommandMap _map; - public IEnumerable Modules => _moduleDefs.Select(x => x.Value); - public IEnumerable Commands => _moduleDefs.SelectMany(x => x.Value.Commands); + internal readonly bool _caseSensitive; + internal readonly RunMode _defaultRunMode; - public CommandService() + public IEnumerable Modules => _moduleDefs.Select(x => x); + public IEnumerable Commands => _moduleDefs.SelectMany(x => x.Commands); + + public CommandService() : this(new CommandServiceConfig()) { } + public CommandService(CommandServiceConfig config) { _moduleLock = new SemaphoreSlim(1, 1); - _moduleDefs = new ConcurrentDictionary(); + _typedModuleDefs = new ConcurrentDictionary(); + _moduleDefs = new ConcurrentBag(); _map = new CommandMap(); _typeReaders = new ConcurrentDictionary { @@ -62,103 +68,129 @@ namespace Discord.Commands [typeof(IGroupUser)] = new UserTypeReader(), [typeof(IGuildUser)] = new UserTypeReader(), }; + _caseSensitive = config.CaseSensitiveCommands; + _defaultRunMode = config.DefaultRunMode; } //Modules - public async Task AddModule() + public async Task CreateModuleAsync(string primaryAlias, Action buildFunc) { await _moduleLock.WaitAsync().ConfigureAwait(false); try { - var typeInfo = typeof(T).GetTypeInfo(); - if (!_moduleTypeInfo.IsAssignableFrom(typeInfo)) - throw new ArgumentException($"Modules must inherit ModuleBase."); + var builder = new ModuleBuilder(this, null, primaryAlias); + buildFunc(builder); - if (typeInfo.IsAbstract) - throw new InvalidOperationException("Modules must not be abstract."); + var module = builder.Build(this); + return LoadModuleInternal(module); + } + finally + { + _moduleLock.Release(); + } + } + public async Task AddModuleAsync() + { + await _moduleLock.WaitAsync().ConfigureAwait(false); + try + { + var typeInfo = typeof(T).GetTypeInfo(); - if (_moduleDefs.ContainsKey(typeof(T))) + if (_typedModuleDefs.ContainsKey(typeof(T))) throw new ArgumentException($"This module has already been added."); - return AddModuleInternal(typeInfo); + var module = ModuleClassBuilder.Build(this, typeInfo).FirstOrDefault(); + + if (module.Value == default(ModuleInfo)) + throw new InvalidOperationException($"Could not build the module {typeof(T).FullName}, did you pass an invalid type?"); + + _typedModuleDefs[module.Key] = module.Value; + + return LoadModuleInternal(module.Value); } finally { _moduleLock.Release(); } } - public async Task> AddModules(Assembly assembly) + public async Task> AddModulesAsync(Assembly assembly) { - var moduleDefs = ImmutableArray.CreateBuilder(); await _moduleLock.WaitAsync().ConfigureAwait(false); try { - foreach (var type in assembly.ExportedTypes) + var types = ModuleClassBuilder.Search(assembly).ToArray(); + var moduleDefs = ModuleClassBuilder.Build(types, this); + + foreach (var info in moduleDefs) { - if (!_moduleDefs.ContainsKey(type)) - { - var typeInfo = type.GetTypeInfo(); - if (_moduleTypeInfo.IsAssignableFrom(typeInfo)) - { - var dontAutoLoad = typeInfo.GetCustomAttribute(); - if (dontAutoLoad == null && !typeInfo.IsAbstract) - moduleDefs.Add(AddModuleInternal(typeInfo)); - } - } + _typedModuleDefs[info.Key] = info.Value; + LoadModuleInternal(info.Value); } - return moduleDefs.ToImmutable(); + + return moduleDefs.Select(x => x.Value).ToImmutableArray(); } finally { _moduleLock.Release(); } } - private ModuleInfo AddModuleInternal(TypeInfo typeInfo) + private ModuleInfo LoadModuleInternal(ModuleInfo module) { - var moduleDef = new ModuleInfo(typeInfo, this); - _moduleDefs[typeInfo.AsType()] = moduleDef; + _moduleDefs.Add(module); - foreach (var cmd in moduleDef.Commands) - _map.AddCommand(cmd); + foreach (var command in module.Commands) + _map.AddCommand(command); - return moduleDef; + foreach (var submodule in module.Submodules) + LoadModuleInternal(submodule); + + return module; } - public async Task RemoveModule(ModuleInfo module) + public async Task RemoveModuleAsync(ModuleInfo module) { await _moduleLock.WaitAsync().ConfigureAwait(false); try { - return RemoveModuleInternal(module.Source.BaseType); + return RemoveModuleInternal(module); } finally { _moduleLock.Release(); } } - public async Task RemoveModule() + public async Task RemoveModuleAsync() { await _moduleLock.WaitAsync().ConfigureAwait(false); try { - return RemoveModuleInternal(typeof(T)); + ModuleInfo module; + _typedModuleDefs.TryGetValue(typeof(T), out module); + if (module == default(ModuleInfo)) + return false; + + return RemoveModuleInternal(module); } finally { _moduleLock.Release(); } } - private bool RemoveModuleInternal(Type type) + private bool RemoveModuleInternal(ModuleInfo module) { - ModuleInfo unloadedModule; - if (_moduleDefs.TryRemove(type, out unloadedModule)) + var defsRemove = module; + if (!_moduleDefs.TryTake(out defsRemove)) + return false; + + foreach (var cmd in module.Commands) + _map.RemoveCommand(cmd); + + foreach (var submodule in module.Submodules) { - foreach (var cmd in unloadedModule.Commands) - _map.RemoveCommand(cmd); - return true; + RemoveModuleInternal(submodule); } - else - return false; + + return true; } //Type Readers @@ -182,7 +214,7 @@ namespace Discord.Commands public SearchResult Search(CommandContext context, int argPos) => Search(context, context.Message.Content.Substring(argPos)); public SearchResult Search(CommandContext context, string input) { - string lowerInput = input.ToLowerInvariant(); + input = _caseSensitive ? input : input.ToLowerInvariant(); var matches = _map.GetCommands(input).OrderByDescending(x => x.Priority).ToImmutableArray(); if (matches.Length > 0) @@ -191,9 +223,9 @@ namespace Discord.Commands return SearchResult.FromError(CommandError.UnknownCommand, "Unknown command."); } - public Task Execute(CommandContext context, int argPos, IDependencyMap dependencyMap = null, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) - => Execute(context, context.Message.Content.Substring(argPos), dependencyMap, multiMatchHandling); - public async Task Execute(CommandContext context, string input, IDependencyMap dependencyMap = null, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) + public Task ExecuteAsync(CommandContext context, int argPos, IDependencyMap dependencyMap = null, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) + => ExecuteAsync(context, context.Message.Content.Substring(argPos), dependencyMap, multiMatchHandling); + public async Task ExecuteAsync(CommandContext context, string input, IDependencyMap dependencyMap = null, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception) { dependencyMap = dependencyMap ?? DependencyMap.Empty; @@ -204,7 +236,7 @@ namespace Discord.Commands var commands = searchResult.Commands; for (int i = commands.Count - 1; i >= 0; i--) { - var preconditionResult = await commands[i].CheckPreconditions(context, dependencyMap).ConfigureAwait(false); + var preconditionResult = await commands[i].CheckPreconditionsAsync(context, dependencyMap).ConfigureAwait(false); if (!preconditionResult.IsSuccess) { if (commands.Count == 1) @@ -213,7 +245,7 @@ namespace Discord.Commands continue; } - var parseResult = await commands[i].Parse(context, searchResult, preconditionResult).ConfigureAwait(false); + var parseResult = await commands[i].ParseAsync(context, searchResult, preconditionResult).ConfigureAwait(false); if (!parseResult.IsSuccess) { if (parseResult.Error == CommandError.MultipleMatches) diff --git a/src/Discord.Net.Commands/CommandServiceConfig.cs b/src/Discord.Net.Commands/CommandServiceConfig.cs new file mode 100644 index 000000000..4ac79fe8f --- /dev/null +++ b/src/Discord.Net.Commands/CommandServiceConfig.cs @@ -0,0 +1,10 @@ +namespace Discord.Commands +{ + public class CommandServiceConfig + { + /// The default RunMode commands should have, if one is not specified on the Command attribute or builder. + public RunMode DefaultRunMode { get; set; } = RunMode.Mixed; + /// Should commands be case-sensitive? + public bool CaseSensitiveCommands { get; set; } = false; + } +} diff --git a/src/Discord.Net.Commands/Discord.Net.Commands.csproj b/src/Discord.Net.Commands/Discord.Net.Commands.csproj new file mode 100644 index 000000000..63ffafd2c --- /dev/null +++ b/src/Discord.Net.Commands/Discord.Net.Commands.csproj @@ -0,0 +1,41 @@ + + + + A Discord.Net extension adding support for bot commands. + 1.0.0-beta2 + netstandard1.3 + Discord.Net.Commands + discord;discordapp + https://github.com/RogueException/Discord.Net + http://opensource.org/licenses/MIT + git + git://github.com/RogueException/Discord.Net + $(PackageTargetFallback);dotnet5.4;dnxcore50;portable-net45+win8 + + + + + + + + + + + + + 1.0.0-alpha-20161104-2 + All + + + + + False + + + $(DefineConstants);RELEASE + $(NoWarn);CS1573;CS1591 + true + true + + + \ No newline at end of file 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 597faf69c..000000000 --- a/src/Discord.Net.Commands/Discord.Net.Commands.xproj +++ /dev/null @@ -1,19 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - 078dd7e6-943d-4d09-afc2-d2ba58b76c9c - Discord.Commands - .\obj - .\bin\ - v4.5.2 - - - 2.0 - - - \ No newline at end of file diff --git a/src/Discord.Net.Commands/Extensions/IEnumerableExtensions.cs b/src/Discord.Net.Commands/Extensions/IEnumerableExtensions.cs new file mode 100644 index 000000000..b922dd903 --- /dev/null +++ b/src/Discord.Net.Commands/Extensions/IEnumerableExtensions.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; + +namespace Discord.Commands +{ + public static class IEnumerableExtensions + { + public static IEnumerable Permutate( + this IEnumerable set, + IEnumerable others, + Func func) + { + foreach (TFirst elem in set) + { + foreach (TSecond elem2 in others) + { + yield return func(elem, elem2); + } + } + } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Commands/Info/CommandInfo.cs b/src/Discord.Net.Commands/Info/CommandInfo.cs new file mode 100644 index 000000000..571c47e13 --- /dev/null +++ b/src/Discord.Net.Commands/Info/CommandInfo.cs @@ -0,0 +1,194 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Collections.Concurrent; +using System.Threading.Tasks; +using System.Reflection; + +using Discord.Commands.Builders; +using System.Diagnostics; + +namespace Discord.Commands +{ + [DebuggerDisplay("{Name,nq}")] + public class CommandInfo + { + private static readonly System.Reflection.MethodInfo _convertParamsMethod = typeof(CommandInfo).GetTypeInfo().GetDeclaredMethod(nameof(ConvertParamsList)); + private static readonly ConcurrentDictionary, object>> _arrayConverters = new ConcurrentDictionary, object>>(); + + private readonly Func _action; + + public ModuleInfo Module { get; } + public string Name { get; } + public string Summary { get; } + public string Remarks { get; } + public int Priority { get; } + public bool HasVarArgs { get; } + public RunMode RunMode { get; } + + public IReadOnlyList Aliases { get; } + public IReadOnlyList Parameters { get; } + public IReadOnlyList Preconditions { get; } + + internal CommandInfo(CommandBuilder builder, ModuleInfo module, CommandService service) + { + Module = module; + + Name = builder.Name; + Summary = builder.Summary; + Remarks = builder.Remarks; + + RunMode = (builder.RunMode == RunMode.Default ? service._defaultRunMode : builder.RunMode); + Priority = builder.Priority; + + // both command and module provide aliases + if (module.Aliases.Count > 0 && builder.Aliases.Count > 0) + Aliases = module.Aliases.Permutate(builder.Aliases, (first, second) => second != null ? first + " " + second : first).Select(x => service._caseSensitive ? x : x.ToLowerInvariant()).ToImmutableArray(); + // only module provides aliases + else if (module.Aliases.Count > 0) + Aliases = module.Aliases.Select(x => service._caseSensitive ? x : x.ToLowerInvariant()).ToImmutableArray(); + // only command provides aliases + else if (builder.Aliases.Count > 0) + Aliases = builder.Aliases.Select(x => service._caseSensitive ? x : x.ToLowerInvariant()).ToImmutableArray(); + // neither provide aliases + else + throw new InvalidOperationException("Cannot build a command without any aliases"); + + Preconditions = builder.Preconditions.ToImmutableArray(); + + Parameters = builder.Parameters.Select(x => x.Build(this)).ToImmutableArray(); + HasVarArgs = builder.Parameters.Count > 0 ? builder.Parameters[builder.Parameters.Count - 1].IsMultiple : false; + + _action = builder.Callback; + } + + public async Task CheckPreconditionsAsync(CommandContext context, IDependencyMap map = null) + { + if (map == null) + map = DependencyMap.Empty; + + foreach (PreconditionAttribute precondition in Module.Preconditions) + { + var result = await precondition.CheckPermissions(context, this, map).ConfigureAwait(false); + if (!result.IsSuccess) + return result; + } + + foreach (PreconditionAttribute precondition in Preconditions) + { + var result = await precondition.CheckPermissions(context, this, map).ConfigureAwait(false); + if (!result.IsSuccess) + return result; + } + + return PreconditionResult.FromSuccess(); + } + + public async Task ParseAsync(CommandContext context, SearchResult searchResult, PreconditionResult? preconditionResult = null) + { + if (!searchResult.IsSuccess) + return ParseResult.FromError(searchResult); + if (preconditionResult != null && !preconditionResult.Value.IsSuccess) + return ParseResult.FromError(preconditionResult.Value); + + string input = searchResult.Text; + var matchingAliases = Aliases.Where(alias => input.StartsWith(alias)); + + string matchingAlias = ""; + foreach (string alias in matchingAliases) + { + if (alias.Length > matchingAlias.Length) + matchingAlias = alias; + } + + input = input.Substring(matchingAlias.Length); + + return await CommandParser.ParseArgs(this, context, input, 0).ConfigureAwait(false); + } + + public Task Execute(CommandContext context, ParseResult parseResult, IDependencyMap map) + { + if (!parseResult.IsSuccess) + return Task.FromResult(ExecuteResult.FromError(parseResult)); + + var argList = new object[parseResult.ArgValues.Count]; + for (int i = 0; i < parseResult.ArgValues.Count; i++) + { + if (!parseResult.ArgValues[i].IsSuccess) + return Task.FromResult(ExecuteResult.FromError(parseResult.ArgValues[i])); + argList[i] = parseResult.ArgValues[i].Values.First().Value; + } + + var paramList = new object[parseResult.ParamValues.Count]; + for (int i = 0; i < parseResult.ParamValues.Count; i++) + { + if (!parseResult.ParamValues[i].IsSuccess) + return Task.FromResult(ExecuteResult.FromError(parseResult.ParamValues[i])); + paramList[i] = parseResult.ParamValues[i].Values.First().Value; + } + + return ExecuteAsync(context, argList, paramList, map); + } + public async Task ExecuteAsync(CommandContext context, IEnumerable argList, IEnumerable paramList, IDependencyMap map) + { + if (map == null) + map = DependencyMap.Empty; + + try + { + var args = GenerateArgs(argList, paramList); + switch (RunMode) + { + case RunMode.Sync: //Always sync + await _action(context, args, map).ConfigureAwait(false); + break; + case RunMode.Mixed: //Sync until first await statement + var t1 = _action(context, args, map); + break; + case RunMode.Async: //Always async + var t2 = Task.Run(() => _action(context, args, map)); + break; + } + return ExecuteResult.FromSuccess(); + } + catch (Exception ex) + { + return ExecuteResult.FromError(ex); + } + } + + private object[] GenerateArgs(IEnumerable argList, IEnumerable paramsList) + { + int argCount = Parameters.Count; + var array = new object[Parameters.Count]; + if (HasVarArgs) + argCount--; + + int i = 0; + foreach (var arg in argList) + { + if (i == argCount) + throw new InvalidOperationException("Command was invoked with too many parameters"); + array[i++] = arg; + } + if (i < argCount) + throw new InvalidOperationException("Command was invoked with too few parameters"); + + if (HasVarArgs) + { + var func = _arrayConverters.GetOrAdd(Parameters[Parameters.Count - 1].Type, t => + { + var method = _convertParamsMethod.MakeGenericMethod(t); + return (Func, object>)method.CreateDelegate(typeof(Func, object>)); + }); + array[i] = func(paramsList); + } + + return array; + } + + private static T[] ConvertParamsList(IEnumerable paramsList) + => paramsList.Cast().ToArray(); + } +} \ No newline at end of file diff --git a/src/Discord.Net.Commands/Info/ModuleInfo.cs b/src/Discord.Net.Commands/Info/ModuleInfo.cs new file mode 100644 index 000000000..3baa2d34f --- /dev/null +++ b/src/Discord.Net.Commands/Info/ModuleInfo.cs @@ -0,0 +1,96 @@ +using System.Linq; +using System.Collections.Generic; +using System.Collections.Immutable; + +using Discord.Commands.Builders; + +namespace Discord.Commands +{ + public class ModuleInfo + { + public CommandService Service { get; } + public string Name { get; } + public string Summary { get; } + public string Remarks { get; } + + public IReadOnlyList Aliases { get; } + public IEnumerable Commands { get; } + public IReadOnlyList Preconditions { get; } + public IReadOnlyList Submodules { get; } + + internal ModuleInfo(ModuleBuilder builder, CommandService service) + { + Service = service; + + Name = builder.Name; + Summary = builder.Summary; + Remarks = builder.Remarks; + + Aliases = BuildAliases(builder).ToImmutableArray(); + Commands = builder.Commands.Select(x => x.Build(this, service)); + Preconditions = BuildPreconditions(builder).ToImmutableArray(); + + Submodules = BuildSubmodules(builder, service).ToImmutableArray(); + } + + private static IEnumerable BuildAliases(ModuleBuilder builder) + { + IEnumerable result = null; + + Stack builderStack = new Stack(); + builderStack.Push(builder); + + ModuleBuilder parent = builder.Parent; + while (parent != null) + { + builderStack.Push(parent); + parent = parent.Parent; + } + + while (builderStack.Count() > 0) + { + ModuleBuilder level = builderStack.Pop(); //get the topmost builder + if (result == null) + { + if (level.Aliases.Count > 0) + result = level.Aliases.ToList(); //create a shallow copy so we don't overwrite the builder unexpectedly + } + else if (result.Count() > level.Aliases.Count) + result = result.Permutate(level.Aliases, (first, second) => first + " " + second); + else + result = level.Aliases.Permutate(result, (second, first) => first + " " + second); + } + + if (result == null) //there were no aliases; default to an empty list + result = new List(); + + return result; + } + + private static List BuildSubmodules(ModuleBuilder parent, CommandService service) + { + var result = new List(); + + foreach (var submodule in parent.Modules) + { + result.Add(submodule.Build(service)); + } + + return result; + } + + private static List BuildPreconditions(ModuleBuilder builder) + { + var result = new List(); + + ModuleBuilder parent = builder; + while (parent != null) + { + result.AddRange(parent.Preconditions); + parent = parent.Parent; + } + + return result; + } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Commands/CommandParameter.cs b/src/Discord.Net.Commands/Info/ParameterInfo.cs similarity index 50% rename from src/Discord.Net.Commands/CommandParameter.cs rename to src/Discord.Net.Commands/Info/ParameterInfo.cs index 1edf42bf1..18c5e653c 100644 --- a/src/Discord.Net.Commands/CommandParameter.cs +++ b/src/Discord.Net.Commands/Info/ParameterInfo.cs @@ -1,37 +1,40 @@ -using System; -using System.Diagnostics; -using System.Reflection; +using System; +using System.Linq; using System.Threading.Tasks; +using Discord.Commands.Builders; + namespace Discord.Commands { - [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class CommandParameter + public class ParameterInfo { private readonly TypeReader _reader; - public ParameterInfo Source { get; } + internal ParameterInfo(ParameterBuilder builder, CommandInfo command, CommandService service) + { + Command = command; + + Name = builder.Name; + Summary = builder.Summary; + IsOptional = builder.IsOptional; + IsRemainder = builder.IsRemainder; + IsMultiple = builder.IsMultiple; + + Type = builder.ParameterType; + DefaultValue = builder.DefaultValue; + + _reader = builder.TypeReader; + } + + public CommandInfo Command { get; } public string Name { get; } public string Summary { get; } public bool IsOptional { get; } public bool IsRemainder { get; } public bool IsMultiple { get; } - public Type ElementType { get; } + public Type Type { get; } public object DefaultValue { get; } - public CommandParameter(ParameterInfo source, string name, string summary, Type type, TypeReader reader, bool isOptional, bool isRemainder, bool isMultiple, object defaultValue) - { - Source = source; - Name = name; - Summary = summary; - ElementType = type; - _reader = reader; - IsOptional = isOptional; - IsRemainder = isRemainder; - IsMultiple = isMultiple; - DefaultValue = defaultValue; - } - public async Task Parse(CommandContext context, string input) { return await _reader.Read(context, input).ConfigureAwait(false); @@ -40,4 +43,4 @@ namespace Discord.Commands public override string ToString() => Name; private string DebuggerDisplay => $"{Name}{(IsOptional ? " (Optional)" : "")}{(IsRemainder ? " (Remainder)" : "")}"; } -} +} \ No newline at end of file diff --git a/src/Discord.Net.Commands/Map/CommandMap.cs b/src/Discord.Net.Commands/Map/CommandMap.cs index e809c1b70..3a5239878 100644 --- a/src/Discord.Net.Commands/Map/CommandMap.cs +++ b/src/Discord.Net.Commands/Map/CommandMap.cs @@ -1,95 +1,39 @@ -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; +using System.Collections.Generic; namespace Discord.Commands { internal class CommandMap { - static readonly char[] _whitespaceChars = new char[] { ' ', '\r', '\n' }; - private readonly object _lockObj = new object(); - - private readonly ConcurrentDictionary _nodes; + private readonly CommandMapNode _root; + private static readonly string[] _blankAliases = new[] { "" }; public CommandMap() { - _nodes = new ConcurrentDictionary(); + _root = new CommandMapNode(""); } public void AddCommand(CommandInfo command) { - foreach (string text in command.Aliases) - { - int nextSpace = NextWhitespace(text); - string name; - - if (nextSpace == -1) - name = text; - else - name = text.Substring(0, nextSpace); - - lock (_lockObj) - { - var nextNode = _nodes.GetOrAdd(name, x => new CommandMapNode(x)); - nextNode.AddCommand(nextSpace == -1 ? "" : text, nextSpace + 1, command); - } - } + foreach (string text in GetAliases(command)) + _root.AddCommand(text, 0, command); } public void RemoveCommand(CommandInfo command) { - foreach (string text in command.Aliases) - { - int nextSpace = NextWhitespace(text); - string name; - - if (nextSpace == -1) - name = text; - else - name = text.Substring(0, nextSpace); - - lock (_lockObj) - { - CommandMapNode nextNode; - if (_nodes.TryGetValue(name, out nextNode)) - { - nextNode.RemoveCommand(nextSpace == -1 ? "" : text, nextSpace + 1, command); - if (nextNode.IsEmpty) - _nodes.TryRemove(name, out nextNode); - } - } - } + foreach (string text in GetAliases(command)) + _root.RemoveCommand(text, 0, command); } public IEnumerable GetCommands(string text) { - int nextSpace = NextWhitespace(text); - string name; - - if (nextSpace == -1) - name = text; - else - name = text.Substring(0, nextSpace); - - lock (_lockObj) - { - CommandMapNode nextNode; - if (_nodes.TryGetValue(name, out nextNode)) - return nextNode.GetCommands(text, nextSpace + 1); - else - return Enumerable.Empty(); - } + return _root.GetCommands(text, 0); } - private static int NextWhitespace(string text) + private IReadOnlyList GetAliases(CommandInfo command) { - int lowest = int.MaxValue; - for (int i = 0; i < _whitespaceChars.Length; i++) - { - int index = text.IndexOf(_whitespaceChars[i]); - if (index != -1 && index < lowest) - lowest = index; - } - return (lowest != int.MaxValue) ? lowest : -1; + var aliases = command.Aliases; + if (aliases.Count == 0) + return _blankAliases; + return aliases; } } } diff --git a/src/Discord.Net.Commands/Map/CommandMapNode.cs b/src/Discord.Net.Commands/Map/CommandMapNode.cs index a348a8cb4..a86c0643d 100644 --- a/src/Discord.Net.Commands/Map/CommandMapNode.cs +++ b/src/Discord.Net.Commands/Map/CommandMapNode.cs @@ -1,4 +1,5 @@ -using System.Collections.Concurrent; +using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; @@ -6,6 +7,8 @@ namespace Discord.Commands { internal class CommandMapNode { + private static readonly char[] _whitespaceChars = new char[] { ' ', '\r', '\n' }; + private readonly ConcurrentDictionary _nodes; private readonly string _name; private readonly object _lockObj = new object(); @@ -22,13 +25,17 @@ namespace Discord.Commands public void AddCommand(string text, int index, CommandInfo command) { - int nextSpace = text.IndexOf(' ', index); + int nextSpace = NextWhitespace(text, index); string name; lock (_lockObj) { if (text == "") + { + if (_name == "") + throw new InvalidOperationException("Cannot add commands to the root node."); _commands = _commands.Add(command); + } else { if (nextSpace == -1) @@ -43,7 +50,7 @@ namespace Discord.Commands } public void RemoveCommand(string text, int index, CommandInfo command) { - int nextSpace = text.IndexOf(' ', index); + int nextSpace = NextWhitespace(text, index); string name; lock (_lockObj) @@ -70,7 +77,7 @@ namespace Discord.Commands public IEnumerable GetCommands(string text, int index) { - int nextSpace = text.IndexOf(' ', index); + int nextSpace = NextWhitespace(text, index); string name; var commands = _commands; @@ -92,5 +99,17 @@ namespace Discord.Commands } } } + + private static int NextWhitespace(string text, int startIndex) + { + int lowest = int.MaxValue; + for (int i = 0; i < _whitespaceChars.Length; i++) + { + int index = text.IndexOf(_whitespaceChars[i], startIndex); + if (index != -1 && index < lowest) + lowest = index; + } + return (lowest != int.MaxValue) ? lowest : -1; + } } } diff --git a/src/Discord.Net.Commands/ModuleBase.cs b/src/Discord.Net.Commands/ModuleBase.cs index c5b6d7436..96dedcb23 100644 --- a/src/Discord.Net.Commands/ModuleBase.cs +++ b/src/Discord.Net.Commands/ModuleBase.cs @@ -6,9 +6,9 @@ namespace Discord.Commands { public CommandContext Context { get; internal set; } - protected virtual async Task ReplyAsync(string message, bool isTTS = false, RequestOptions options = null) + protected virtual async Task ReplyAsync(string message, bool isTTS = false, EmbedBuilder embed = null, RequestOptions options = null) { - return await Context.Channel.SendMessageAsync(message, isTTS, options).ConfigureAwait(false); + return await Context.Channel.SendMessageAsync(message, isTTS, embed, options).ConfigureAwait(false); } } } diff --git a/src/Discord.Net.Commands/ModuleInfo.cs b/src/Discord.Net.Commands/ModuleInfo.cs deleted file mode 100644 index b7471edb5..000000000 --- a/src/Discord.Net.Commands/ModuleInfo.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics; -using System.Reflection; - -namespace Discord.Commands -{ - [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public class ModuleInfo - { - internal readonly Func _builder; - - public TypeInfo Source { get; } - public CommandService Service { get; } - public string Name { get; } - public string Prefix { get; } - public string Summary { get; } - public string Remarks { get; } - public IEnumerable Commands { get; } - public IReadOnlyList Preconditions { get; } - - internal ModuleInfo(TypeInfo source, CommandService service) - { - Source = source; - Service = service; - Name = source.Name; - _builder = ReflectionUtils.CreateBuilder(source, Service); - - var groupAttr = source.GetCustomAttribute(); - if (groupAttr != null) - Prefix = groupAttr.Prefix; - else - Prefix = ""; - - var nameAttr = source.GetCustomAttribute(); - if (nameAttr != null) - Name = nameAttr.Text; - - var summaryAttr = source.GetCustomAttribute(); - if (summaryAttr != null) - Summary = summaryAttr.Text; - - var remarksAttr = source.GetCustomAttribute(); - if (remarksAttr != null) - Remarks = remarksAttr.Text; - - List commands = new List(); - SearchClass(source, commands, Prefix); - Commands = commands; - - Preconditions = Source.GetCustomAttributes().ToImmutableArray(); - } - private void SearchClass(TypeInfo parentType, List commands, string groupPrefix) - { - foreach (var method in parentType.DeclaredMethods) - { - var cmdAttr = method.GetCustomAttribute(); - if (cmdAttr != null) - commands.Add(new CommandInfo(method, this, cmdAttr, groupPrefix)); - } - foreach (var type in parentType.DeclaredNestedTypes) - { - var groupAttrib = type.GetCustomAttribute(); - if (groupAttrib != null) - { - string nextGroupPrefix; - - if (groupPrefix != "") - nextGroupPrefix = groupPrefix + " " + (groupAttrib.Prefix ?? type.Name.ToLowerInvariant()); - else - nextGroupPrefix = groupAttrib.Prefix ?? type.Name.ToLowerInvariant(); - - SearchClass(type, commands, nextGroupPrefix); - } - } - } - - internal ModuleBase CreateInstance(IDependencyMap map) - => _builder(map); - - public override string ToString() => Name; - private string DebuggerDisplay => Name; - } -} diff --git a/src/Discord.Net.Commands/Readers/EnumTypeReader.cs b/src/Discord.Net.Commands/Readers/EnumTypeReader.cs index dca845704..81870ecaf 100644 --- a/src/Discord.Net.Commands/Readers/EnumTypeReader.cs +++ b/src/Discord.Net.Commands/Readers/EnumTypeReader.cs @@ -31,11 +31,13 @@ namespace Discord.Commands var byNameBuilder = ImmutableDictionary.CreateBuilder(); var byValueBuilder = ImmutableDictionary.CreateBuilder(); - - foreach (var v in Enum.GetValues(_enumType)) - { - byNameBuilder.Add(v.ToString().ToLower(), v); - byValueBuilder.Add((T)v, v); + + foreach (var v in Enum.GetNames(_enumType)) + { + var parsedValue = Enum.Parse(_enumType, v); + byNameBuilder.Add(v.ToLower(), parsedValue); + if (!byValueBuilder.ContainsKey((T)parsedValue)) + byValueBuilder.Add((T)parsedValue, parsedValue); } _enumsByName = byNameBuilder.ToImmutable(); diff --git a/src/Discord.Net.Commands/RunMode.cs b/src/Discord.Net.Commands/RunMode.cs index 0799f825c..2bb5dbbf6 100644 --- a/src/Discord.Net.Commands/RunMode.cs +++ b/src/Discord.Net.Commands/RunMode.cs @@ -2,6 +2,7 @@ { public enum RunMode { + Default, Sync, Mixed, Async diff --git a/src/Discord.Net.Commands/ReflectionUtils.cs b/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs similarity index 95% rename from src/Discord.Net.Commands/ReflectionUtils.cs rename to src/Discord.Net.Commands/Utilities/ReflectionUtils.cs index 052e5fe98..27ea601bf 100644 --- a/src/Discord.Net.Commands/ReflectionUtils.cs +++ b/src/Discord.Net.Commands/Utilities/ReflectionUtils.cs @@ -18,7 +18,7 @@ namespace Discord.Commands throw new InvalidOperationException($"Multiple constructors found for \"{typeInfo.FullName}\""); var constructor = constructors[0]; - ParameterInfo[] parameters = constructor.GetParameters(); + System.Reflection.ParameterInfo[] parameters = constructor.GetParameters(); return (map) => { diff --git a/src/Discord.Net.Commands/project.json b/src/Discord.Net.Commands/project.json index e730296a8..a87591c03 100644 --- a/src/Discord.Net.Commands/project.json +++ b/src/Discord.Net.Commands/project.json @@ -1,5 +1,5 @@ { - "version": "1.0.0-beta2-*", + "version": "1.0.0-*", "description": "A Discord.Net extension adding support for bot commands.", "authors": [ "RogueException" ], @@ -40,4 +40,4 @@ ] } } -} +} \ No newline at end of file diff --git a/src/Discord.Net.Core/API/Common/Embed.cs b/src/Discord.Net.Core/API/Common/Embed.cs index 9cea24313..33b97e641 100644 --- a/src/Discord.Net.Core/API/Common/Embed.cs +++ b/src/Discord.Net.Core/API/Common/Embed.cs @@ -1,4 +1,5 @@ #pragma warning disable CS1591 +using System; using Newtonsoft.Json; namespace Discord.API @@ -13,9 +14,23 @@ namespace Discord.API public string Description { get; set; } [JsonProperty("url")] public string Url { get; set; } + [JsonProperty("color")] + public uint? Color { get; set; } + [JsonProperty("timestamp")] + public DateTimeOffset? Timestamp { get; set; } + [JsonProperty("author")] + public Optional Author { get; set; } + [JsonProperty("footer")] + public Optional Footer { get; set; } + [JsonProperty("video")] + public Optional Video { get; set; } [JsonProperty("thumbnail")] public Optional Thumbnail { get; set; } + [JsonProperty("image")] + public Optional Image { get; set; } [JsonProperty("provider")] public Optional Provider { get; set; } + [JsonProperty("fields")] + public Optional Fields { get; set; } } } diff --git a/src/Discord.Net.Core/API/Common/EmbedAuthor.cs b/src/Discord.Net.Core/API/Common/EmbedAuthor.cs new file mode 100644 index 000000000..973f7d5ea --- /dev/null +++ b/src/Discord.Net.Core/API/Common/EmbedAuthor.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class EmbedAuthor + { + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("url")] + public string Url { get; set; } + [JsonProperty("icon_url")] + public string IconUrl { get; set; } + [JsonProperty("proxy_icon_url")] + public string ProxyIconUrl { get; set; } + } +} diff --git a/src/Discord.Net.Core/API/Common/EmbedField.cs b/src/Discord.Net.Core/API/Common/EmbedField.cs new file mode 100644 index 000000000..12aa0137a --- /dev/null +++ b/src/Discord.Net.Core/API/Common/EmbedField.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class EmbedField + { + [JsonProperty("name")] + public string Name { get; set; } + [JsonProperty("value")] + public string Value { get; set; } + [JsonProperty("inline")] + public bool Inline { get; set; } + } +} diff --git a/src/Discord.Net.Core/API/Common/EmbedFooter.cs b/src/Discord.Net.Core/API/Common/EmbedFooter.cs new file mode 100644 index 000000000..2ad22cae7 --- /dev/null +++ b/src/Discord.Net.Core/API/Common/EmbedFooter.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + public class EmbedFooter + { + [JsonProperty("text")] + public string Text { get; set; } + [JsonProperty("icon_url")] + public string IconUrl { get; set; } + [JsonProperty("proxy_icon_url")] + public string ProxyIconUrl { get; set; } + } +} diff --git a/src/Discord.Net.Core/API/Common/EmbedImage.cs b/src/Discord.Net.Core/API/Common/EmbedImage.cs new file mode 100644 index 000000000..ab4941ae0 --- /dev/null +++ b/src/Discord.Net.Core/API/Common/EmbedImage.cs @@ -0,0 +1,17 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API +{ + public class EmbedImage + { + [JsonProperty("url")] + public string Url { get; set; } + [JsonProperty("proxy_url")] + public string ProxyUrl { get; set; } + [JsonProperty("height")] + public Optional Height { get; set; } + [JsonProperty("width")] + public Optional Width { get; set; } + } +} diff --git a/src/Discord.Net.Core/API/Common/EmbedVideo.cs b/src/Discord.Net.Core/API/Common/EmbedVideo.cs new file mode 100644 index 000000000..072004631 --- /dev/null +++ b/src/Discord.Net.Core/API/Common/EmbedVideo.cs @@ -0,0 +1,15 @@ +#pragma warning disable CS1591 +using Newtonsoft.Json; + +namespace Discord.API +{ + public class EmbedVideo + { + [JsonProperty("url")] + public string Url { get; set; } + [JsonProperty("height")] + public Optional Height { get; set; } + [JsonProperty("width")] + public Optional Width { get; set; } + } +} diff --git a/src/Discord.Net.Core/API/DiscordRestApiClient.cs b/src/Discord.Net.Core/API/DiscordRestApiClient.cs index 6b5cbb484..bc7436cd4 100644 --- a/src/Discord.Net.Core/API/DiscordRestApiClient.cs +++ b/src/Discord.Net.Core/API/DiscordRestApiClient.cs @@ -49,7 +49,7 @@ namespace Discord.API { _restClientProvider = restClientProvider; _userAgent = userAgent; - _serializer = serializer ?? new JsonSerializer { ContractResolver = new DiscordContractResolver() }; + _serializer = serializer ?? new JsonSerializer { DateFormatString = "yyyy-MM-ddTHH:mm:ssZ", ContractResolver = new DiscordContractResolver() }; RequestQueue = requestQueue; FetchCurrentUser = true; @@ -165,30 +165,30 @@ namespace Discord.API //Core internal Task SendAsync(string method, Expression> endpointExpr, BucketIds ids, - string clientBucketId = null, RequestOptions options = null, [CallerMemberName] string funcName = null) - => SendAsync(method, GetEndpoint(endpointExpr), GetBucketId(ids, endpointExpr, AuthTokenType, funcName), clientBucketId, options); + ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) + => SendAsync(method, GetEndpoint(endpointExpr), GetBucketId(ids, endpointExpr, AuthTokenType, funcName), clientBucket, options); public async Task SendAsync(string method, string endpoint, - string bucketId = null, string clientBucketId = null, RequestOptions options = null) + string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) { options = options ?? new RequestOptions(); options.HeaderOnly = true; - options.BucketId = bucketId; - options.ClientBucketId = AuthTokenType == TokenType.User ? clientBucketId : null; + options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId; + options.IsClientBucket = AuthTokenType == TokenType.User; var request = new RestRequest(_restClient, method, endpoint, options); await SendInternalAsync(method, endpoint, request).ConfigureAwait(false); } internal Task SendJsonAsync(string method, Expression> endpointExpr, object payload, BucketIds ids, - string clientBucketId = null, RequestOptions options = null, [CallerMemberName] string funcName = null) - => SendJsonAsync(method, GetEndpoint(endpointExpr), payload, GetBucketId(ids, endpointExpr, AuthTokenType, funcName), clientBucketId, options); + ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) + => SendJsonAsync(method, GetEndpoint(endpointExpr), payload, GetBucketId(ids, endpointExpr, AuthTokenType, funcName), clientBucket, options); public async Task SendJsonAsync(string method, string endpoint, object payload, - string bucketId = null, string clientBucketId = null, RequestOptions options = null) + string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) { options = options ?? new RequestOptions(); options.HeaderOnly = true; - options.BucketId = bucketId; - options.ClientBucketId = AuthTokenType == TokenType.User ? clientBucketId : null; + options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId; + options.IsClientBucket = AuthTokenType == TokenType.User; var json = payload != null ? SerializeJson(payload) : null; var request = new JsonRestRequest(_restClient, method, endpoint, json, options); @@ -196,43 +196,43 @@ namespace Discord.API } internal Task SendMultipartAsync(string method, Expression> endpointExpr, IReadOnlyDictionary multipartArgs, BucketIds ids, - string clientBucketId = null, RequestOptions options = null, [CallerMemberName] string funcName = null) - => SendMultipartAsync(method, GetEndpoint(endpointExpr), multipartArgs, GetBucketId(ids, endpointExpr, AuthTokenType, funcName), clientBucketId, options); + ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) + => SendMultipartAsync(method, GetEndpoint(endpointExpr), multipartArgs, GetBucketId(ids, endpointExpr, AuthTokenType, funcName), clientBucket, options); public async Task SendMultipartAsync(string method, string endpoint, IReadOnlyDictionary multipartArgs, - string bucketId = null, string clientBucketId = null, RequestOptions options = null) + string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) { options = options ?? new RequestOptions(); options.HeaderOnly = true; - options.BucketId = bucketId; - options.ClientBucketId = AuthTokenType == TokenType.User ? clientBucketId : null; + options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId; + options.IsClientBucket = AuthTokenType == TokenType.User; var request = new MultipartRestRequest(_restClient, method, endpoint, multipartArgs, options); await SendInternalAsync(method, endpoint, request).ConfigureAwait(false); } internal Task SendAsync(string method, Expression> endpointExpr, BucketIds ids, - string clientBucketId = null, RequestOptions options = null, [CallerMemberName] string funcName = null) where TResponse : class - => SendAsync(method, GetEndpoint(endpointExpr), GetBucketId(ids, endpointExpr, AuthTokenType, funcName), clientBucketId, options); + ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) where TResponse : class + => SendAsync(method, GetEndpoint(endpointExpr), GetBucketId(ids, endpointExpr, AuthTokenType, funcName), clientBucket, options); public async Task SendAsync(string method, string endpoint, - string bucketId = null, string clientBucketId = null, RequestOptions options = null) where TResponse : class + string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) where TResponse : class { options = options ?? new RequestOptions(); - options.BucketId = bucketId; - options.ClientBucketId = AuthTokenType == TokenType.User ? clientBucketId : null; + options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId; + options.IsClientBucket = AuthTokenType == TokenType.User; var request = new RestRequest(_restClient, method, endpoint, options); return DeserializeJson(await SendInternalAsync(method, endpoint, request).ConfigureAwait(false)); } internal Task SendJsonAsync(string method, Expression> endpointExpr, object payload, BucketIds ids, - string clientBucketId = null, RequestOptions options = null, [CallerMemberName] string funcName = null) where TResponse : class - => SendJsonAsync(method, GetEndpoint(endpointExpr), payload, GetBucketId(ids, endpointExpr, AuthTokenType, funcName), clientBucketId, options); + ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) where TResponse : class + => SendJsonAsync(method, GetEndpoint(endpointExpr), payload, GetBucketId(ids, endpointExpr, AuthTokenType, funcName), clientBucket, options); public async Task SendJsonAsync(string method, string endpoint, object payload, - string bucketId = null, string clientBucketId = null, RequestOptions options = null) where TResponse : class + string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) where TResponse : class { options = options ?? new RequestOptions(); - options.BucketId = bucketId; - options.ClientBucketId = AuthTokenType == TokenType.User ? clientBucketId : null; + options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId; + options.IsClientBucket = AuthTokenType == TokenType.User; var json = payload != null ? SerializeJson(payload) : null; var request = new JsonRestRequest(_restClient, method, endpoint, json, options); @@ -240,14 +240,14 @@ namespace Discord.API } internal Task SendMultipartAsync(string method, Expression> endpointExpr, IReadOnlyDictionary multipartArgs, BucketIds ids, - string clientBucketId = null, RequestOptions options = null, [CallerMemberName] string funcName = null) - => SendMultipartAsync(method, GetEndpoint(endpointExpr), multipartArgs, GetBucketId(ids, endpointExpr, AuthTokenType, funcName), clientBucketId, options); + ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null, [CallerMemberName] string funcName = null) + => SendMultipartAsync(method, GetEndpoint(endpointExpr), multipartArgs, GetBucketId(ids, endpointExpr, AuthTokenType, funcName), clientBucket, options); public async Task SendMultipartAsync(string method, string endpoint, IReadOnlyDictionary multipartArgs, - string bucketId = null, string clientBucketId = null, RequestOptions options = null) + string bucketId = null, ClientBucketType clientBucket = ClientBucketType.Unbucketed, RequestOptions options = null) { options = options ?? new RequestOptions(); - options.BucketId = bucketId; - options.ClientBucketId = AuthTokenType == TokenType.User ? clientBucketId : null; + options.BucketId = AuthTokenType == TokenType.User ? ClientBucket.Get(clientBucket).Id : bucketId; + options.IsClientBucket = AuthTokenType == TokenType.User; var request = new MultipartRestRequest(_restClient, method, endpoint, multipartArgs, options); return DeserializeJson(await SendInternalAsync(method, endpoint, request).ConfigureAwait(false)); @@ -432,20 +432,22 @@ namespace Discord.API if (relativeId != null) endpoint = () => $"channels/{channelId}/messages?limit={limit}&{relativeDir}={relativeId}"; else - endpoint = () =>$"channels/{channelId}/messages?limit={limit}"; + endpoint = () => $"channels/{channelId}/messages?limit={limit}"; return await SendAsync>("GET", endpoint, ids, options: options).ConfigureAwait(false); } public async Task CreateMessageAsync(ulong channelId, CreateMessageParams args, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotNull(args, nameof(args)); - Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); + if (!args.Embed.IsSpecified || args.Embed.Value == null) + Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); + if (args.Content.Length > DiscordConfig.MaxMessageSize) throw new ArgumentException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(channelId: channelId); - return await SendJsonAsync("POST", () => $"channels/{channelId}/messages", args, ids, clientBucketId: ClientBucket.SendEditId, options: options).ConfigureAwait(false); + return await SendJsonAsync("POST", () => $"channels/{channelId}/messages", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } public async Task UploadFileAsync(ulong channelId, UploadFileParams args, RequestOptions options = null) { @@ -464,7 +466,7 @@ namespace Discord.API } var ids = new BucketIds(channelId: channelId); - return await SendMultipartAsync("POST", () => $"channels/{channelId}/messages", args.ToDictionary(), ids, clientBucketId: ClientBucket.SendEditId, options: options).ConfigureAwait(false); + return await SendMultipartAsync("POST", () => $"channels/{channelId}/messages", args.ToDictionary(), ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } public async Task DeleteMessageAsync(ulong channelId, ulong messageId, RequestOptions options = null) { @@ -496,21 +498,22 @@ namespace Discord.API break; } } - public async Task ModifyMessageAsync(ulong channelId, ulong messageId, ModifyMessageParams args, RequestOptions options = null) + public async Task ModifyMessageAsync(ulong channelId, ulong messageId, Rest.ModifyMessageParams args, RequestOptions options = null) { Preconditions.NotEqual(channelId, 0, nameof(channelId)); Preconditions.NotEqual(messageId, 0, nameof(messageId)); Preconditions.NotNull(args, nameof(args)); if (args.Content.IsSpecified) { - Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); + if (!args.Embed.IsSpecified) + Preconditions.NotNullOrEmpty(args.Content, nameof(args.Content)); if (args.Content.Value.Length > DiscordConfig.MaxMessageSize) throw new ArgumentOutOfRangeException($"Message content is too long, length must be less or equal to {DiscordConfig.MaxMessageSize}.", nameof(args.Content)); } options = RequestOptions.CreateOrClone(options); var ids = new BucketIds(channelId: channelId); - return await SendJsonAsync("PATCH", () => $"channels/{channelId}/messages/{messageId}", args, ids, clientBucketId: ClientBucket.SendEditId, options: options).ConfigureAwait(false); + return await SendJsonAsync("PATCH", () => $"channels/{channelId}/messages/{messageId}", args, ids, clientBucket: ClientBucketType.SendEdit, options: options).ConfigureAwait(false); } public async Task AddReactionAsync(ulong channelId, ulong messageId, string emoji, RequestOptions options = null) { @@ -1112,19 +1115,6 @@ namespace Discord.API using (JsonReader reader = new JsonTextReader(text)) return _serializer.Deserialize(reader); } - internal string GetBucketId(ulong guildId = 0, ulong channelId = 0, [CallerMemberName] string methodName = "") - { - if (guildId != 0) - { - if (channelId != 0) - return $"{methodName}({guildId}/{channelId})"; - else - return $"{methodName}({guildId})"; - } - else if (channelId != 0) - return $"{methodName}({channelId})"; - return $"{methodName}()"; - } internal class BucketIds { diff --git a/src/Discord.Net.Core/API/Rest/CreateMessageParams.cs b/src/Discord.Net.Core/API/Rest/CreateMessageParams.cs index 9577ab579..a0dbb59dd 100644 --- a/src/Discord.Net.Core/API/Rest/CreateMessageParams.cs +++ b/src/Discord.Net.Core/API/Rest/CreateMessageParams.cs @@ -1,4 +1,5 @@ #pragma warning disable CS1591 +using System; using Newtonsoft.Json; namespace Discord.API.Rest @@ -13,6 +14,8 @@ namespace Discord.API.Rest public Optional Nonce { get; set; } [JsonProperty("tts")] public Optional IsTTS { get; set; } + [JsonProperty("embed")] + public Optional Embed { get; set; } public CreateMessageParams(string content) { diff --git a/src/Discord.Net.Core/API/Rest/ModifyMessageParams.cs b/src/Discord.Net.Core/API/Rest/ModifyMessageParams.cs index 4901ddc9d..c87d82c51 100644 --- a/src/Discord.Net.Core/API/Rest/ModifyMessageParams.cs +++ b/src/Discord.Net.Core/API/Rest/ModifyMessageParams.cs @@ -8,5 +8,7 @@ namespace Discord.API.Rest { [JsonProperty("content")] public Optional Content { get; set; } + [JsonProperty("embed")] + public Optional Embed { get; set; } } } diff --git a/src/Discord.Net.Core/Discord.Net.Core.csproj b/src/Discord.Net.Core/Discord.Net.Core.csproj new file mode 100644 index 000000000..b2d9fc870 --- /dev/null +++ b/src/Discord.Net.Core/Discord.Net.Core.csproj @@ -0,0 +1,60 @@ + + + + A .Net API wrapper and bot framework for Discord. + 1.0.0-beta2 + netstandard1.3 + Discord.Net.Core + discord;discordapp + https://github.com/RogueException/Discord.Net + http://opensource.org/licenses/MIT + git + git://github.com/RogueException/Discord.Net + $(PackageTargetFallback);dotnet5.4;dnxcore50;portable-net45+win8 + + + + + + + + + + 1.0.0-alpha-20161104-2 + All + + + 4.3.0 + + + 9.0.1 + + + 4.3.0 + + + 1.3.0 + + + 3.1.0 + + + 4.3.0 + + + 4.3.0 + All + + + + + False + + + $(DefineConstants);RELEASE + $(NoWarn);CS1573;CS1591 + true + true + + + \ No newline at end of file diff --git a/src/Discord.Net.Core/Discord.Net.Core.xproj b/src/Discord.Net.Core/Discord.Net.Core.xproj deleted file mode 100644 index 6759e09b4..000000000 --- a/src/Discord.Net.Core/Discord.Net.Core.xproj +++ /dev/null @@ -1,19 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - 91e9e7bd-75c9-4e98-84aa-2c271922e5c2 - Discord - .\obj - .\bin\ - v4.5.2 - - - 2.0 - - - \ No newline at end of file diff --git a/src/Discord.Net.Core/DiscordConfig.cs b/src/Discord.Net.Core/DiscordConfig.cs index 737cf0050..b35f0d745 100644 --- a/src/Discord.Net.Core/DiscordConfig.cs +++ b/src/Discord.Net.Core/DiscordConfig.cs @@ -11,9 +11,10 @@ namespace Discord "Unknown"; public static readonly string ClientAPIUrl = $"https://discordapp.com/api/v{APIVersion}/"; - public const string CDNUrl = "https://discordcdn.com/"; + public const string CDNUrl = "https://cdn.discordapp.com/"; public const string InviteUrl = "https://discord.gg/"; + public const int DefaultRequestTimeout = 15000; public const int MaxMessageSize = 2000; public const int MaxMessagesPerBatch = 100; public const int MaxUsersPerBatch = 1000; diff --git a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs index 7c13e4a6f..41bc79511 100644 --- a/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs +++ b/src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs @@ -8,7 +8,7 @@ namespace Discord public interface IMessageChannel : IChannel { /// Sends a message to this message channel. - Task SendMessageAsync(string text, bool isTTS = false, RequestOptions options = null); + Task SendMessageAsync(string text, bool isTTS = false, EmbedBuilder embed = null, RequestOptions options = null); /// Sends a file to this text channel, with an optional caption. Task SendFileAsync(string filePath, string text = null, bool isTTS = false, RequestOptions options = null); /// Sends a file to this text channel, with an optional caption. diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs b/src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs new file mode 100644 index 000000000..b0ed0f08f --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/EmbedAuthor.cs @@ -0,0 +1,29 @@ +using System.Diagnostics; +using Model = Discord.API.EmbedAuthor; + +namespace Discord +{ + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public struct EmbedAuthor + { + public string Name { get; set; } + public string Url { get; set; } + public string IconUrl { get; set; } + public string ProxyIconUrl { get; set; } + + private EmbedAuthor(string name, string url, string iconUrl, string proxyIconUrl) + { + Name = name; + Url = url; + IconUrl = iconUrl; + ProxyIconUrl = proxyIconUrl; + } + internal static EmbedAuthor Create(Model model) + { + return new EmbedAuthor(model.Name, model.Url, model.IconUrl, model.ProxyIconUrl); + } + + private string DebuggerDisplay => $"{Name} ({Url})"; + public override string ToString() => Name; + } +} diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs b/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs new file mode 100644 index 000000000..2fba6d47e --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/EmbedBuilder.cs @@ -0,0 +1,208 @@ +using System; +using System.Collections.Generic; +using Embed = Discord.API.Embed; +using Field = Discord.API.EmbedField; +using Author = Discord.API.EmbedAuthor; +using Footer = Discord.API.EmbedFooter; +using Thumbnail = Discord.API.EmbedThumbnail; +using Image = Discord.API.EmbedImage; + +namespace Discord +{ + public class EmbedBuilder + { + private readonly Embed _model; + private readonly List _fields; + + public EmbedBuilder() + { + _model = new Embed { Type = "rich" }; + _fields = new List(); + } + + public string Title { get { return _model.Title; } set { _model.Title = value; } } + public string Description { get { return _model.Description; } set { _model.Description = value; } } + public string Url { get { return _model.Url; } set { _model.Url = value; } } + public string ThumbnailUrl { get; set; } + public string ImageUrl { get; set; } + public DateTimeOffset? Timestamp { get; set; } + public Color? Color { get { return _model.Color.HasValue ? new Color(_model.Color.Value) : (Color?)null; } set { _model.Color = value?.RawValue; } } + public EmbedAuthorBuilder Author { get; set; } + public EmbedFooterBuilder Footer { get; set; } + + public EmbedBuilder WithTitle(string title) + { + Title = title; + return this; + } + public EmbedBuilder WithDescription(string description) + { + Description = description; + return this; + } + public EmbedBuilder WithUrl(string url) + { + Url = url; + return this; + } + public EmbedBuilder WithThumbnailUrl(string thumbnailUrl) + { + ThumbnailUrl = thumbnailUrl; + return this; + } + public EmbedBuilder WithImageUrl(string imageUrl) + { + ImageUrl = ImageUrl; + return this; + } + public EmbedBuilder WithCurrentTimestamp() + { + Timestamp = DateTimeOffset.UtcNow; + return this; + } + public EmbedBuilder WithTimestamp(DateTimeOffset dateTimeOffset) + { + Timestamp = dateTimeOffset; + return this; + } + public EmbedBuilder WithColor(Color color) + { + Color = color; + return this; + } + + public EmbedBuilder WithAuthor(EmbedAuthorBuilder author) + { + Author = author; + return this; + } + public EmbedBuilder WithAuthor(Action action) + { + var author = new EmbedAuthorBuilder(); + action(author); + Author = author; + return this; + } + public EmbedBuilder WithFooter(EmbedFooterBuilder footer) + { + Footer = footer; + return this; + } + public EmbedBuilder WithFooter(Action action) + { + var footer = new EmbedFooterBuilder(); + action(footer); + Footer = footer; + return this; + } + + public EmbedBuilder AddField(Action action) + { + var field = new EmbedFieldBuilder(); + action(field); + _fields.Add(field.ToModel()); + return this; + } + + internal Embed Build() + { + _model.Author = Author?.ToModel(); + _model.Footer = Footer?.ToModel(); + _model.Timestamp = Timestamp?.ToUniversalTime(); + _model.Thumbnail = ThumbnailUrl != null ? new Thumbnail { Url = ThumbnailUrl } : null; + _model.Image = ImageUrl != null ? new Image { Url = ImageUrl } : null; + _model.Fields = _fields.ToArray(); + return _model; + } + } + + public class EmbedFieldBuilder + { + private readonly Field _model; + + public string Name { get { return _model.Name; } set { _model.Name = value; } } + public string Value { get { return _model.Value; } set { _model.Value = value; } } + public bool IsInline { get { return _model.Inline; } set { _model.Inline = value; } } + + public EmbedFieldBuilder() + { + _model = new Field(); + } + + public EmbedFieldBuilder WithName(string name) + { + Name = name; + return this; + } + public EmbedFieldBuilder WithValue(string value) + { + Value = value; + return this; + } + public EmbedFieldBuilder WithIsInline(bool isInline) + { + IsInline = isInline; + return this; + } + + internal Field ToModel() => _model; + } + + public class EmbedAuthorBuilder + { + private readonly Author _model; + + public string Name { get { return _model.Name; } set { _model.Name = value; } } + public string Url { get { return _model.Url; } set { _model.Url = value; } } + public string IconUrl { get { return _model.IconUrl; } set { _model.IconUrl = value; } } + + public EmbedAuthorBuilder() + { + _model = new Author(); + } + + public EmbedAuthorBuilder WithName(string name) + { + Name = name; + return this; + } + public EmbedAuthorBuilder WithUrl(string url) + { + Url = url; + return this; + } + public EmbedAuthorBuilder WithIconUrl(string iconUrl) + { + IconUrl = iconUrl; + return this; + } + + internal Author ToModel() => _model; + } + + public class EmbedFooterBuilder + { + private readonly Footer _model; + + public string Text { get { return _model.Text; } set { _model.Text = value; } } + public string IconUrl { get { return _model.IconUrl; } set { _model.IconUrl = value; } } + + public EmbedFooterBuilder() + { + _model = new Footer(); + } + + public EmbedFooterBuilder WithText(string text) + { + Text = text; + return this; + } + public EmbedFooterBuilder WithIconUrl(string iconUrl) + { + IconUrl = iconUrl; + return this; + } + + internal Footer ToModel() => _model; + } +} diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedField.cs b/src/Discord.Net.Core/Entities/Messages/EmbedField.cs new file mode 100644 index 000000000..257074e41 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/EmbedField.cs @@ -0,0 +1,27 @@ +using System.Diagnostics; +using Model = Discord.API.EmbedField; + +namespace Discord +{ + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public struct EmbedField + { + public string Name { get; set; } + public string Value { get; set; } + public bool Inline { get; set; } + + private EmbedField(string name, string value, bool inline) + { + Name = name; + Value = value; + Inline = inline; + } + internal static EmbedField Create(Model model) + { + return new EmbedField(model.Name, model.Value, model.Inline); + } + + private string DebuggerDisplay => $"{Name} ({Value}"; + public override string ToString() => Name; + } +} diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs b/src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs new file mode 100644 index 000000000..a69e4b077 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/EmbedFooter.cs @@ -0,0 +1,27 @@ +using System.Diagnostics; +using Model = Discord.API.EmbedFooter; + +namespace Discord +{ + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public struct EmbedFooter + { + public string Text { get; set; } + public string IconUrl { get; set; } + public string ProxyUrl { get; set; } + + private EmbedFooter(string text, string iconUrl, string proxyUrl) + { + Text = text; + IconUrl = iconUrl; + ProxyUrl = proxyUrl; + } + internal static EmbedFooter Create(Model model) + { + return new EmbedFooter(model.Text, model.IconUrl, model.ProxyIconUrl); + } + + private string DebuggerDisplay => $"{Text} ({IconUrl})"; + public override string ToString() => Text; + } +} diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedImage.cs b/src/Discord.Net.Core/Entities/Messages/EmbedImage.cs new file mode 100644 index 000000000..0b606c11e --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/EmbedImage.cs @@ -0,0 +1,31 @@ +using System.Diagnostics; +using Model = Discord.API.EmbedImage; + +namespace Discord +{ + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public struct EmbedImage + { + public string Url { get; } + public string ProxyUrl { get; } + public int? Height { get; } + public int? Width { get; } + + private EmbedImage(string url, string proxyUrl, int? height, int? width) + { + Url = url; + ProxyUrl = proxyUrl; + Height = height; + Width = width; + } + internal static EmbedImage Create(Model model) + { + return new EmbedImage(model.Url, model.ProxyUrl, + model.Height.IsSpecified ? model.Height.Value : (int?)null, + model.Width.IsSpecified ? model.Width.Value : (int?)null); + } + + private string DebuggerDisplay => $"{Url} ({(Width != null && Height != null ? $"{Width}x{Height}" : "0x0")})"; + public override string ToString() => Url; + } +} diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs b/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs index 6a5fc4163..dac2f5c7c 100644 --- a/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs +++ b/src/Discord.Net.Core/Entities/Messages/EmbedThumbnail.cs @@ -25,7 +25,7 @@ namespace Discord model.Width.IsSpecified ? model.Width.Value : (int?)null); } - private string DebuggerDisplay => $"{ToString()} ({Url})"; - public override string ToString() => Width != null && Height != null ? $"{Width}x{Height}" : "0x0"; + private string DebuggerDisplay => $"{Url} ({(Width != null && Height != null ? $"{Width}x{Height}" : "0x0")})"; + public override string ToString() => Url; } } diff --git a/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs b/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs new file mode 100644 index 000000000..3c93416d0 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/EmbedVideo.cs @@ -0,0 +1,29 @@ +using System.Diagnostics; +using Model = Discord.API.EmbedVideo; + +namespace Discord +{ + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public struct EmbedVideo + { + public string Url { get; } + public int? Height { get; } + public int? Width { get; } + + private EmbedVideo(string url, int? height, int? width) + { + Url = url; + Height = height; + Width = width; + } + internal static EmbedVideo Create(Model model) + { + return new EmbedVideo(model.Url, + model.Height.IsSpecified ? model.Height.Value : (int?)null, + model.Width.IsSpecified ? model.Width.Value : (int?)null); + } + + private string DebuggerDisplay => $"{Url} ({(Width != null && Height != null ? $"{Width}x{Height}" : "0x0")})"; + public override string ToString() => Url; + } +} diff --git a/src/Discord.Net.Core/Entities/Messages/IEmbed.cs b/src/Discord.Net.Core/Entities/Messages/IEmbed.cs index 3bca85fd0..5eef5ec9b 100644 --- a/src/Discord.Net.Core/Entities/Messages/IEmbed.cs +++ b/src/Discord.Net.Core/Entities/Messages/IEmbed.cs @@ -1,4 +1,7 @@ -namespace Discord +using System; +using System.Collections.Immutable; + +namespace Discord { public interface IEmbed { @@ -6,7 +9,14 @@ string Type { get; } string Title { get; } string Description { get; } + DateTimeOffset? Timestamp { get; } + Color? Color { get; } + EmbedImage? Image { get; } + EmbedVideo? Video { get; } + EmbedAuthor? Author { get; } + EmbedFooter? Footer { get; } EmbedProvider? Provider { get; } EmbedThumbnail? Thumbnail { get; } + ImmutableArray Fields { get; } } } diff --git a/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs b/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs index ba2a00bd8..346f1a45e 100644 --- a/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs +++ b/src/Discord.Net.Core/Entities/Messages/IUserMessage.cs @@ -1,5 +1,4 @@ -using Discord.API.Rest; -using System; +using System; using System.Collections.Generic; using System.Threading.Tasks; diff --git a/src/Discord.Net.Core/Entities/Messages/ModifyMessageParams.cs b/src/Discord.Net.Core/Entities/Messages/ModifyMessageParams.cs new file mode 100644 index 000000000..cc05e620a --- /dev/null +++ b/src/Discord.Net.Core/Entities/Messages/ModifyMessageParams.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Discord +{ + public class ModifyMessageParams + { + public Optional Content { get; set; } + public Optional Embed { get; set; } + } +} diff --git a/src/Discord.Net.Core/Entities/Messages/TagHandling.cs b/src/Discord.Net.Core/Entities/Messages/TagHandling.cs index 3572a37a5..492f05879 100644 --- a/src/Discord.Net.Core/Entities/Messages/TagHandling.cs +++ b/src/Discord.Net.Core/Entities/Messages/TagHandling.cs @@ -2,10 +2,12 @@ { public enum TagHandling { - Ignore = 0, - Remove, - Name, - FullName, - Sanitize + Ignore = 0, //<@53905483156684800> -> <@53905483156684800> + Remove, //<@53905483156684800> -> + Name, //<@53905483156684800> -> @Voltana + NameNoPrefix, //<@53905483156684800> -> Voltana + FullName, //<@53905483156684800> -> @Voltana#8252 + FullNameNoPrefix, //<@53905483156684800> -> Voltana#8252 + Sanitize //<@53905483156684800> -> <@53905483156684800> (w/ nbsp) } } diff --git a/src/Discord.Net.Core/Entities/Roles/Color.cs b/src/Discord.Net.Core/Entities/Roles/Color.cs index 563917959..ead46fd8a 100644 --- a/src/Discord.Net.Core/Entities/Roles/Color.cs +++ b/src/Discord.Net.Core/Entities/Roles/Color.cs @@ -32,6 +32,12 @@ namespace Discord } public Color(float r, float g, float b) { + if (r < 0.0f || r > 1.0f) + throw new ArgumentOutOfRangeException(nameof(r), "A float value must be within [0,1]"); + if (g < 0.0f || g > 1.0f) + throw new ArgumentOutOfRangeException(nameof(g), "A float value must be within [0,1]"); + if (b < 0.0f || b > 1.0f) + throw new ArgumentOutOfRangeException(nameof(b), "A float value must be within [0,1]"); RawValue = ((uint)(r * 255.0f) << 16) | ((uint)(g * 255.0f) << 8) | diff --git a/src/Discord.Net.Core/Entities/Roles/IRole.cs b/src/Discord.Net.Core/Entities/Roles/IRole.cs index aa34fb019..2e3d63702 100644 --- a/src/Discord.Net.Core/Entities/Roles/IRole.cs +++ b/src/Discord.Net.Core/Entities/Roles/IRole.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; namespace Discord { - public interface IRole : ISnowflakeEntity, IDeletable, IMentionable + public interface IRole : ISnowflakeEntity, IDeletable, IMentionable, IComparable { /// Gets the guild owning this role. IGuild Guild { get; } @@ -27,4 +27,4 @@ namespace Discord ///// Modifies this role. Task ModifyAsync(Action func, RequestOptions options = null); } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Core/Extensions/GuildUserExtensions.cs b/src/Discord.Net.Core/Extensions/GuildUserExtensions.cs index 57d6a13dc..855551c3d 100644 --- a/src/Discord.Net.Core/Extensions/GuildUserExtensions.cs +++ b/src/Discord.Net.Core/Extensions/GuildUserExtensions.cs @@ -6,14 +6,23 @@ namespace Discord { public static class GuildUserExtensions { + //TODO: Should we remove Add/Remove? Encourages race conditions. public static Task AddRolesAsync(this IGuildUser user, params IRole[] roles) - => AddRolesAsync(user, (IEnumerable)roles); + => ChangeRolesAsync(user, add: roles); public static Task AddRolesAsync(this IGuildUser user, IEnumerable roles) - => user.ModifyAsync(x => x.RoleIds = user.RoleIds.Concat(roles.Select(y => y.Id)).ToArray()); - + => ChangeRolesAsync(user, add: roles); public static Task RemoveRolesAsync(this IGuildUser user, params IRole[] roles) - => RemoveRolesAsync(user, (IEnumerable)roles); + => ChangeRolesAsync(user, remove: roles); public static Task RemoveRolesAsync(this IGuildUser user, IEnumerable roles) - => user.ModifyAsync(x => x.RoleIds = user.RoleIds.Except(roles.Select(y => y.Id)).ToArray()); + => ChangeRolesAsync(user, remove: roles); + public static async Task ChangeRolesAsync(this IGuildUser user, IEnumerable add = null, IEnumerable remove = null) + { + IEnumerable roleIds = user.RoleIds; + if (remove != null) + roleIds = roleIds.Except(remove.Select(x => x.Id)); + if (add != null) + roleIds = roleIds.Concat(add.Select(x => x.Id)); + await user.ModifyAsync(x => x.RoleIds = roleIds.ToArray()).ConfigureAwait(false); + } } } diff --git a/src/Discord.Net.Core/Extensions/RoleExtensions.cs b/src/Discord.Net.Core/Extensions/RoleExtensions.cs new file mode 100644 index 000000000..bd6856501 --- /dev/null +++ b/src/Discord.Net.Core/Extensions/RoleExtensions.cs @@ -0,0 +1,18 @@ +namespace Discord +{ + internal static class RoleExtensions + { + internal static int Compare(this IRole left, IRole right) + { + if (left == null) + return -1; + if (right == null) + return 1; + var result = left.Position.CompareTo(right.Position); + // As per Discord's documentation, a tie is broken by ID + if (result != 0) + return result; + return left.Id.CompareTo(right.Id); + } + } +} diff --git a/src/Discord.Net.Core/Net/Queue/ClientBucket.cs b/src/Discord.Net.Core/Net/Queue/ClientBucket.cs index 14d3c3207..f32df1bcf 100644 --- a/src/Discord.Net.Core/Net/Queue/ClientBucket.cs +++ b/src/Discord.Net.Core/Net/Queue/ClientBucket.cs @@ -2,25 +2,47 @@ namespace Discord.Net.Queue { - public struct ClientBucket + public enum ClientBucketType { - public const string SendEditId = ""; + Unbucketed = 0, + SendEdit = 1 + } + internal struct ClientBucket + { + private static readonly ImmutableDictionary _defsByType; + private static readonly ImmutableDictionary _defsById; - private static readonly ImmutableDictionary _defs; static ClientBucket() { - var builder = ImmutableDictionary.CreateBuilder(); - builder.Add(SendEditId, new ClientBucket(10, 10)); - _defs = builder.ToImmutable(); - } + var buckets = new[] + { + new ClientBucket(ClientBucketType.Unbucketed, "", 10, 10), + new ClientBucket(ClientBucketType.SendEdit, "", 10, 10) + }; - public static ClientBucket Get(string id) =>_defs[id]; + var builder = ImmutableDictionary.CreateBuilder(); + foreach (var bucket in buckets) + builder.Add(bucket.Type, bucket); + _defsByType = builder.ToImmutable(); + + var builder2 = ImmutableDictionary.CreateBuilder(); + foreach (var bucket in buckets) + builder2.Add(bucket.Id, bucket); + _defsById = builder2.ToImmutable(); + } + public static ClientBucket Get(ClientBucketType type) => _defsByType[type]; + public static ClientBucket Get(string id) => _defsById[id]; + + public ClientBucketType Type { get; } + public string Id { get; } public int WindowCount { get; } public int WindowSeconds { get; } - public ClientBucket(int count, int seconds) + public ClientBucket(ClientBucketType type, string id, int count, int seconds) { + Type = type; + Id = id; WindowCount = count; WindowSeconds = seconds; } diff --git a/src/Discord.Net.Core/Net/Queue/RequestQueue.cs b/src/Discord.Net.Core/Net/Queue/RequestQueue.cs index ab20d7c18..1ea586481 100644 --- a/src/Discord.Net.Core/Net/Queue/RequestQueue.cs +++ b/src/Discord.Net.Core/Net/Queue/RequestQueue.cs @@ -79,7 +79,9 @@ namespace Discord.Net.Queue int millis = (int)Math.Ceiling((_waitUntil - DateTimeOffset.UtcNow).TotalMilliseconds); if (millis > 0) { +#if DEBUG_LIMITS Debug.WriteLine($"[{id}] Sleeping {millis} ms (Pre-emptive) [Global]"); +#endif await Task.Delay(millis).ConfigureAwait(false); } } diff --git a/src/Discord.Net.Core/Net/Queue/RequestQueueBucket.cs b/src/Discord.Net.Core/Net/Queue/RequestQueueBucket.cs index b750afbf6..78a8e63cb 100644 --- a/src/Discord.Net.Core/Net/Queue/RequestQueueBucket.cs +++ b/src/Discord.Net.Core/Net/Queue/RequestQueueBucket.cs @@ -1,7 +1,10 @@ -using Newtonsoft.Json; +using Discord.Net.Rest; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; +#if DEBUG_LIMITS using System.Diagnostics; +#endif using System.IO; using System.Net; using System.Threading; @@ -27,8 +30,8 @@ namespace Discord.Net.Queue _lock = new object(); - if (request.Options.ClientBucketId != null) - WindowCount = ClientBucket.Get(request.Options.ClientBucketId).WindowCount; + if (request.Options.IsClientBucket) + WindowCount = ClientBucket.Get(request.Options.BucketId).WindowCount; else WindowCount = 1; //Only allow one request until we get a header back _semaphore = WindowCount; @@ -40,62 +43,91 @@ namespace Discord.Net.Queue public async Task SendAsync(RestRequest request) { int id = Interlocked.Increment(ref nextId); +#if DEBUG_LIMITS Debug.WriteLine($"[{id}] Start"); +#endif LastAttemptAt = DateTimeOffset.UtcNow; while (true) { await _queue.EnterGlobalAsync(id, request).ConfigureAwait(false); await EnterAsync(id, request).ConfigureAwait(false); +#if DEBUG_LIMITS Debug.WriteLine($"[{id}] Sending..."); - var response = await request.SendAsync().ConfigureAwait(false); - TimeSpan lag = DateTimeOffset.UtcNow - DateTimeOffset.Parse(response.Headers["Date"]); - var info = new RateLimitInfo(response.Headers); - - if (response.StatusCode < (HttpStatusCode)200 || response.StatusCode >= (HttpStatusCode)300) +#endif + TimeSpan lag = default(TimeSpan); + RateLimitInfo info = default(RateLimitInfo); + try { - switch (response.StatusCode) + var response = await request.SendAsync().ConfigureAwait(false); + lag = DateTimeOffset.UtcNow - DateTimeOffset.Parse(response.Headers["Date"]); + info = new RateLimitInfo(response.Headers); + + if (response.StatusCode < (HttpStatusCode)200 || response.StatusCode >= (HttpStatusCode)300) { - case (HttpStatusCode)429: - if (info.IsGlobal) - { - Debug.WriteLine($"[{id}] (!) 429 [Global]"); - _queue.PauseGlobal(info, lag); - } - else - { - Debug.WriteLine($"[{id}] (!) 429"); - UpdateRateLimit(id, request, info, lag, true); - } - await _queue.RaiseRateLimitTriggered(Id, info).ConfigureAwait(false); - continue; //Retry - case HttpStatusCode.BadGateway: //502 - Debug.WriteLine($"[{id}] (!) 502"); - continue; //Continue - default: - string reason = null; - if (response.Stream != null) - { - try + switch (response.StatusCode) + { + case (HttpStatusCode)429: + if (info.IsGlobal) + { +#if DEBUG_LIMITS + Debug.WriteLine($"[{id}] (!) 429 [Global]"); +#endif + _queue.PauseGlobal(info, lag); + } + else + { +#if DEBUG_LIMITS + Debug.WriteLine($"[{id}] (!) 429"); +#endif + UpdateRateLimit(id, request, info, lag, true); + } + await _queue.RaiseRateLimitTriggered(Id, info).ConfigureAwait(false); + continue; //Retry + case HttpStatusCode.BadGateway: //502 +#if DEBUG_LIMITS + Debug.WriteLine($"[{id}] (!) 502"); +#endif + continue; //Continue + default: + string reason = null; + if (response.Stream != null) { - using (var reader = new StreamReader(response.Stream)) - using (var jsonReader = new JsonTextReader(reader)) + try { - var json = JToken.Load(jsonReader); - reason = json.Value("message"); + using (var reader = new StreamReader(response.Stream)) + using (var jsonReader = new JsonTextReader(reader)) + { + var json = JToken.Load(jsonReader); + reason = json.Value("message"); + } } + catch { } } - catch { } - } - throw new HttpException(response.StatusCode, reason); + throw new HttpException(response.StatusCode, reason); + } + } + else + { +#if DEBUG_LIMITS + Debug.WriteLine($"[{id}] Success"); +#endif + return response.Stream; } } - else +#if DEBUG_LIMITS + catch + { + Debug.WriteLine($"[{id}] Error"); + throw; + } +#endif + finally { - Debug.WriteLine($"[{id}] Success"); UpdateRateLimit(id, request, info, lag, false); +#if DEBUG_LIMITS Debug.WriteLine($"[{id}] Stop"); - return response.Stream; +#endif } } } @@ -135,7 +167,9 @@ namespace Discord.Net.Queue if (resetAt > timeoutAt) throw new RateLimitedException(); int millis = (int)Math.Ceiling((resetAt.Value - DateTimeOffset.UtcNow).TotalMilliseconds); +#if DEBUG_LIMITS Debug.WriteLine($"[{id}] Sleeping {millis} ms (Pre-emptive)"); +#endif if (millis > 0) await Task.Delay(millis, request.CancelToken).ConfigureAwait(false); } @@ -143,13 +177,17 @@ namespace Discord.Net.Queue { if ((timeoutAt.Value - DateTimeOffset.UtcNow).TotalMilliseconds < 500.0) throw new RateLimitedException(); +#if DEBUG_LIMITS Debug.WriteLine($"[{id}] Sleeping 500* ms (Pre-emptive)"); +#endif await Task.Delay(500, request.CancelToken).ConfigureAwait(false); } continue; } +#if DEBUG_LIMITS else Debug.WriteLine($"[{id}] Entered Semaphore ({_semaphore}/{WindowCount} remaining)"); +#endif break; } } @@ -166,7 +204,9 @@ namespace Discord.Net.Queue { WindowCount = info.Limit.Value; _semaphore = info.Remaining.Value; +#if DEBUG_LIMITS Debug.WriteLine($"[{id}] Upgraded Semaphore to {info.Remaining.Value}/{WindowCount}"); +#endif } var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); @@ -182,24 +222,32 @@ namespace Discord.Net.Queue { //RetryAfter is more accurate than Reset, where available resetTick = DateTimeOffset.UtcNow.AddMilliseconds(info.RetryAfter.Value); +#if DEBUG_LIMITS Debug.WriteLine($"[{id}] Retry-After: {info.RetryAfter.Value} ({info.RetryAfter.Value} ms)"); +#endif } else if (info.Reset.HasValue) { resetTick = info.Reset.Value.AddSeconds(/*1.0 +*/ lag.TotalSeconds); int diff = (int)(resetTick.Value - DateTimeOffset.UtcNow).TotalMilliseconds; +#if DEBUG_LIMITS Debug.WriteLine($"[{id}] X-RateLimit-Reset: {info.Reset.Value.ToUnixTimeSeconds()} ({diff} ms, {lag.TotalMilliseconds} ms lag)"); +#endif } - else if (request.Options.ClientBucketId != null) + else if (request.Options.IsClientBucket && request.Options.BucketId != null) { - resetTick = DateTimeOffset.UtcNow.AddSeconds(ClientBucket.Get(request.Options.ClientBucketId).WindowSeconds); - Debug.WriteLine($"[{id}] Client Bucket ({ClientBucket.Get(request.Options.ClientBucketId).WindowSeconds * 1000} ms)"); + resetTick = DateTimeOffset.UtcNow.AddSeconds(ClientBucket.Get(request.Options.BucketId).WindowSeconds); +#if DEBUG_LIMITS + Debug.WriteLine($"[{id}] Client Bucket ({ClientBucket.Get(request.Options.BucketId).WindowSeconds * 1000} ms)"); +#endif } if (resetTick == null) { WindowCount = 0; //No rate limit info, disable limits on this bucket (should only ever happen with a user token) +#if DEBUG_LIMITS Debug.WriteLine($"[{id}] Disabled Semaphore"); +#endif return; } @@ -207,7 +255,9 @@ namespace Discord.Net.Queue { _resetTick = resetTick; LastAttemptAt = resetTick.Value; //Make sure we dont destroy this until after its been reset +#if DEBUG_LIMITS Debug.WriteLine($"[{id}] Reset in {(int)Math.Ceiling((resetTick - DateTimeOffset.UtcNow).Value.TotalMilliseconds)} ms"); +#endif if (!hasQueuedReset) { @@ -227,7 +277,9 @@ namespace Discord.Net.Queue millis = (int)Math.Ceiling((_resetTick.Value - DateTimeOffset.UtcNow).TotalMilliseconds); if (millis <= 0) //Make sure we havent gotten a more accurate reset time { +#if DEBUG_LIMITS Debug.WriteLine($"[{id}] * Reset *"); +#endif _semaphore = WindowCount; _resetTick = null; return; @@ -236,4 +288,4 @@ namespace Discord.Net.Queue } } } -} \ No newline at end of file +} diff --git a/src/Discord.Net.Core/Net/Rest/DefaultRestClient.cs b/src/Discord.Net.Core/Net/Rest/DefaultRestClient.cs index 5ec30c750..588785230 100644 --- a/src/Discord.Net.Core/Net/Rest/DefaultRestClient.cs +++ b/src/Discord.Net.Core/Net/Rest/DefaultRestClient.cs @@ -120,7 +120,7 @@ namespace Discord.Net.Rest cancelToken = CancellationTokenSource.CreateLinkedTokenSource(_cancelToken, cancelToken).Token; HttpResponseMessage response = await _client.SendAsync(request, cancelToken).ConfigureAwait(false); - var headers = response.Headers.ToDictionary(x => x.Key, x => x.Value.FirstOrDefault()); + var headers = response.Headers.ToDictionary(x => x.Key, x => x.Value.FirstOrDefault(), StringComparer.OrdinalIgnoreCase); var stream = !headerOnly ? await response.Content.ReadAsStreamAsync().ConfigureAwait(false) : null; return new RestResponse(response.StatusCode, headers, stream); diff --git a/src/Discord.Net.Core/Net/WebSockets/DefaultWebSocketClient.cs b/src/Discord.Net.Core/Net/WebSockets/DefaultWebSocketClient.cs index 707f7663b..ffa96dba7 100644 --- a/src/Discord.Net.Core/Net/WebSockets/DefaultWebSocketClient.cs +++ b/src/Discord.Net.Core/Net/WebSockets/DefaultWebSocketClient.cs @@ -101,6 +101,8 @@ namespace Discord.Net.WebSockets if (_client != null && _client.State == WebSocketState.Open) { + var token = new CancellationToken(); + await _client.CloseAsync(WebSocketCloseStatus.NormalClosure, "", token); _client.Dispose(); _client = null; } diff --git a/src/Discord.Net.Core/RequestOptions.cs b/src/Discord.Net.Core/RequestOptions.cs index 3af6c929d..b82ec29c8 100644 --- a/src/Discord.Net.Core/RequestOptions.cs +++ b/src/Discord.Net.Core/RequestOptions.cs @@ -10,7 +10,7 @@ internal bool IgnoreState { get; set; } internal string BucketId { get; set; } - internal string ClientBucketId { get; set; } + internal bool IsClientBucket { get; set; } internal static RequestOptions CreateOrClone(RequestOptions options) { @@ -22,7 +22,7 @@ public RequestOptions() { - Timeout = 30000; + Timeout = DiscordConfig.DefaultRequestTimeout; } public RequestOptions Clone() => MemberwiseClone() as RequestOptions; diff --git a/src/Discord.Net.Core/Utils/MentionUtils.cs b/src/Discord.Net.Core/Utils/MentionUtils.cs index 1b1408852..4d9add8fd 100644 --- a/src/Discord.Net.Core/Utils/MentionUtils.cs +++ b/src/Discord.Net.Core/Utils/MentionUtils.cs @@ -85,14 +85,17 @@ namespace Discord return false; } - internal static string Resolve(IMessage msg, TagHandling userHandling, TagHandling channelHandling, TagHandling roleHandling, TagHandling everyoneHandling, TagHandling emojiHandling) + internal static string Resolve(IMessage msg, int startIndex, TagHandling userHandling, TagHandling channelHandling, TagHandling roleHandling, TagHandling everyoneHandling, TagHandling emojiHandling) { - var text = new StringBuilder(msg.Content); + var text = new StringBuilder(msg.Content.Substring(startIndex)); var tags = msg.Tags; - int indexOffset = 0; + int indexOffset = -startIndex; foreach (var tag in tags) { + if (tag.Index < startIndex) + continue; + string newText = ""; switch (tag.Type) { @@ -139,12 +142,22 @@ namespace Discord if (user != null) return $"@{guildUser?.Nickname ?? user?.Username}"; else - return $"@unknown-user"; + return $""; + case TagHandling.NameNoPrefix: + if (user != null) + return $"{guildUser?.Nickname ?? user?.Username}"; + else + return $""; case TagHandling.FullName: if (user != null) return $"@{user.Username}#{user.Discriminator}"; else - return $"@unknown-user"; + return $""; + case TagHandling.FullNameNoPrefix: + if (user != null) + return $"{user.Username}#{user.Discriminator}"; + else + return $""; case TagHandling.Sanitize: if (guildUser != null && guildUser.Nickname == null) return MentionUser($"{SanitizeChar}{tag.Key}", false); @@ -166,7 +179,13 @@ namespace Discord if (channel != null) return $"#{channel.Name}"; else - return $"#deleted-channel"; + return $""; + case TagHandling.NameNoPrefix: + case TagHandling.FullNameNoPrefix: + if (channel != null) + return $"{channel.Name}"; + else + return $""; case TagHandling.Sanitize: return MentionChannel($"{SanitizeChar}{tag.Key}"); } @@ -185,7 +204,13 @@ namespace Discord if (role != null) return $"@{role.Name}"; else - return $"@deleted-role"; + return $""; + case TagHandling.NameNoPrefix: + case TagHandling.FullNameNoPrefix: + if (role != null) + return $"{role.Name}"; + else + return $""; case TagHandling.Sanitize: return MentionRole($"{SanitizeChar}{tag.Key}"); } @@ -200,7 +225,9 @@ namespace Discord { case TagHandling.Name: case TagHandling.FullName: - return "@everyone"; + case TagHandling.NameNoPrefix: + case TagHandling.FullNameNoPrefix: + return "everyone"; case TagHandling.Sanitize: return $"@{SanitizeChar}everyone"; } @@ -215,9 +242,11 @@ namespace Discord { case TagHandling.Name: case TagHandling.FullName: - return "@everyone"; + case TagHandling.NameNoPrefix: + case TagHandling.FullNameNoPrefix: + return "here"; case TagHandling.Sanitize: - return $"@{SanitizeChar}everyone"; + return $"@{SanitizeChar}here"; } } return ""; @@ -232,8 +261,11 @@ namespace Discord case TagHandling.Name: case TagHandling.FullName: return $":{emoji.Name}:"; + case TagHandling.NameNoPrefix: + case TagHandling.FullNameNoPrefix: + return $"{emoji.Name}"; case TagHandling.Sanitize: - return $"<@{SanitizeChar}everyone"; + return $"<{emoji.Id}{SanitizeChar}:{SanitizeChar}{emoji.Name}>"; } } return ""; diff --git a/src/Discord.Net.Core/Utils/Preconditions.cs b/src/Discord.Net.Core/Utils/Preconditions.cs index 2c6ea2417..e88a006b8 100644 --- a/src/Discord.Net.Core/Utils/Preconditions.cs +++ b/src/Discord.Net.Core/Utils/Preconditions.cs @@ -5,148 +5,182 @@ namespace Discord internal static class Preconditions { //Objects - public static void NotNull(T obj, string name) where T : class { if (obj == null) throw new ArgumentNullException(name); } - public static void NotNull(Optional obj, string name) where T : class { if (obj.IsSpecified && obj.Value == null) throw new ArgumentNullException(name); } + public static void NotNull(T obj, string name, string msg = null) where T : class { if (obj == null) throw CreateNotNullException(name, msg); } + public static void NotNull(Optional obj, string name, string msg = null) where T : class { if (obj.IsSpecified && obj.Value == null) throw CreateNotNullException(name, msg); } + + private static ArgumentNullException CreateNotNullException(string name, string msg) + { + if (msg == null) return new ArgumentNullException(name); + else return new ArgumentNullException(name, msg); + } //Strings - public static void NotEmpty(string obj, string name) { if (obj.Length == 0) throw new ArgumentException("Argument cannot be empty.", name); } - public static void NotEmpty(Optional obj, string name) { if (obj.IsSpecified && obj.Value.Length == 0) throw new ArgumentException("Argument cannot be empty.", name); } - public static void NotNullOrEmpty(string obj, string name) + public static void NotEmpty(string obj, string name, string msg = null) { if (obj.Length == 0) throw CreateNotEmptyException(name, msg); } + public static void NotEmpty(Optional obj, string name, string msg = null) { if (obj.IsSpecified && obj.Value.Length == 0) throw CreateNotEmptyException(name, msg); } + public static void NotNullOrEmpty(string obj, string name, string msg = null) { - if (obj == null) - throw new ArgumentNullException(name); - if (obj.Length == 0) - throw new ArgumentException("Argument cannot be empty.", name); + if (obj == null) throw CreateNotNullException(name, msg); + if (obj.Length == 0) throw CreateNotEmptyException(name, msg); } - public static void NotNullOrEmpty(Optional obj, string name) + public static void NotNullOrEmpty(Optional obj, string name, string msg = null) { if (obj.IsSpecified) { - if (obj.Value == null) - throw new ArgumentNullException(name); - if (obj.Value.Length == 0) - throw new ArgumentException("Argument cannot be empty.", name); + if (obj.Value == null) throw CreateNotNullException(name, msg); + if (obj.Value.Length == 0) throw CreateNotEmptyException(name, msg); } } - public static void NotNullOrWhitespace(string obj, string name) + public static void NotNullOrWhitespace(string obj, string name, string msg = null) { - if (obj == null) - throw new ArgumentNullException(name); - if (obj.Trim().Length == 0) - throw new ArgumentException("Argument cannot be blank.", name); + if (obj == null) throw CreateNotNullException(name, msg); + if (obj.Trim().Length == 0) throw CreateNotEmptyException(name, msg); } - public static void NotNullOrWhitespace(Optional obj, string name) + public static void NotNullOrWhitespace(Optional obj, string name, string msg = null) { if (obj.IsSpecified) { - if (obj.Value == null) - throw new ArgumentNullException(name); - if (obj.Value.Trim().Length == 0) - throw new ArgumentException("Argument cannot be blank.", name); + if (obj.Value == null) throw CreateNotNullException(name, msg); + if (obj.Value.Trim().Length == 0) throw CreateNotEmptyException(name, msg); } } + private static ArgumentException CreateNotEmptyException(string name, string msg) + { + if (msg == null) return new ArgumentException(name, "Argument cannot be blank."); + else return new ArgumentException(name, msg); + } + //Numerics - public static void NotEqual(sbyte obj, sbyte value, string name) { if (obj == value) throw new ArgumentOutOfRangeException(name); } - public static void NotEqual(byte obj, byte value, string name) { if (obj == value) throw new ArgumentOutOfRangeException(name); } - public static void NotEqual(short obj, short value, string name) { if (obj == value) throw new ArgumentOutOfRangeException(name); } - public static void NotEqual(ushort obj, ushort value, string name) { if (obj == value) throw new ArgumentOutOfRangeException(name); } - public static void NotEqual(int obj, int value, string name) { if (obj == value) throw new ArgumentOutOfRangeException(name); } - public static void NotEqual(uint obj, uint value, string name) { if (obj == value) throw new ArgumentOutOfRangeException(name); } - public static void NotEqual(long obj, long value, string name) { if (obj == value) throw new ArgumentOutOfRangeException(name); } - public static void NotEqual(ulong obj, ulong value, string name) { if (obj == value) throw new ArgumentOutOfRangeException(name); } - public static void NotEqual(Optional obj, sbyte value, string name) { if (obj.IsSpecified && obj.Value == value) throw new ArgumentOutOfRangeException(name); } - public static void NotEqual(Optional obj, byte value, string name) { if (obj.IsSpecified && obj.Value == value) throw new ArgumentOutOfRangeException(name); } - public static void NotEqual(Optional obj, short value, string name) { if (obj.IsSpecified && obj.Value == value) throw new ArgumentOutOfRangeException(name); } - public static void NotEqual(Optional obj, ushort value, string name) { if (obj.IsSpecified && obj.Value == value) throw new ArgumentOutOfRangeException(name); } - public static void NotEqual(Optional obj, int value, string name) { if (obj.IsSpecified && obj.Value == value) throw new ArgumentOutOfRangeException(name); } - public static void NotEqual(Optional obj, uint value, string name) { if (obj.IsSpecified && obj.Value == value) throw new ArgumentOutOfRangeException(name); } - public static void NotEqual(Optional obj, long value, string name) { if (obj.IsSpecified && obj.Value == value) throw new ArgumentOutOfRangeException(name); } - public static void NotEqual(Optional obj, ulong value, string name) { if (obj.IsSpecified && obj.Value == value) throw new ArgumentOutOfRangeException(name); } - public static void NotEqual(sbyte? obj, sbyte value, string name) { if (obj == value) throw new ArgumentOutOfRangeException(name); } - public static void NotEqual(byte? obj, byte value, string name) { if (obj == value) throw new ArgumentOutOfRangeException(name); } - public static void NotEqual(short? obj, short value, string name) { if (obj == value) throw new ArgumentOutOfRangeException(name); } - public static void NotEqual(ushort? obj, ushort value, string name) { if (obj == value) throw new ArgumentOutOfRangeException(name); } - public static void NotEqual(int? obj, int value, string name) { if (obj == value) throw new ArgumentOutOfRangeException(name); } - public static void NotEqual(uint? obj, uint value, string name) { if (obj == value) throw new ArgumentOutOfRangeException(name); } - public static void NotEqual(long? obj, long value, string name) { if (obj == value) throw new ArgumentOutOfRangeException(name); } - public static void NotEqual(ulong? obj, ulong value, string name) { if (obj == value) throw new ArgumentOutOfRangeException(name); } - public static void NotEqual(Optional obj, sbyte value, string name) { if (obj.IsSpecified && obj.Value == value) throw new ArgumentOutOfRangeException(name); } - public static void NotEqual(Optional obj, byte value, string name) { if (obj.IsSpecified && obj.Value == value) throw new ArgumentOutOfRangeException(name); } - public static void NotEqual(Optional obj, short value, string name) { if (obj.IsSpecified && obj.Value == value) throw new ArgumentOutOfRangeException(name); } - public static void NotEqual(Optional obj, ushort value, string name) { if (obj.IsSpecified && obj.Value == value) throw new ArgumentOutOfRangeException(name); } - public static void NotEqual(Optional obj, int value, string name) { if (obj.IsSpecified && obj.Value == value) throw new ArgumentOutOfRangeException(name); } - public static void NotEqual(Optional obj, uint value, string name) { if (obj.IsSpecified && obj.Value == value) throw new ArgumentOutOfRangeException(name); } - public static void NotEqual(Optional obj, long value, string name) { if (obj.IsSpecified && obj.Value == value) throw new ArgumentOutOfRangeException(name); } - public static void NotEqual(Optional obj, ulong value, string name) { if (obj.IsSpecified && obj.Value == value) throw new ArgumentOutOfRangeException(name); } - - public static void AtLeast(sbyte obj, sbyte value, string name) { if (obj < value) throw new ArgumentOutOfRangeException(name); } - public static void AtLeast(byte obj, byte value, string name) { if (obj < value) throw new ArgumentOutOfRangeException(name); } - public static void AtLeast(short obj, short value, string name) { if (obj < value) throw new ArgumentOutOfRangeException(name); } - public static void AtLeast(ushort obj, ushort value, string name) { if (obj < value) throw new ArgumentOutOfRangeException(name); } - public static void AtLeast(int obj, int value, string name) { if (obj < value) throw new ArgumentOutOfRangeException(name); } - public static void AtLeast(uint obj, uint value, string name) { if (obj < value) throw new ArgumentOutOfRangeException(name); } - public static void AtLeast(long obj, long value, string name) { if (obj < value) throw new ArgumentOutOfRangeException(name); } - public static void AtLeast(ulong obj, ulong value, string name) { if (obj < value) throw new ArgumentOutOfRangeException(name); } - public static void AtLeast(Optional obj, sbyte value, string name) { if (obj.IsSpecified && obj.Value < value) throw new ArgumentOutOfRangeException(name); } - public static void AtLeast(Optional obj, byte value, string name) { if (obj.IsSpecified && obj.Value < value) throw new ArgumentOutOfRangeException(name); } - public static void AtLeast(Optional obj, short value, string name) { if (obj.IsSpecified && obj.Value < value) throw new ArgumentOutOfRangeException(name); } - public static void AtLeast(Optional obj, ushort value, string name) { if (obj.IsSpecified && obj.Value < value) throw new ArgumentOutOfRangeException(name); } - public static void AtLeast(Optional obj, int value, string name) { if (obj.IsSpecified && obj.Value < value) throw new ArgumentOutOfRangeException(name); } - public static void AtLeast(Optional obj, uint value, string name) { if (obj.IsSpecified && obj.Value < value) throw new ArgumentOutOfRangeException(name); } - public static void AtLeast(Optional obj, long value, string name) { if (obj.IsSpecified && obj.Value < value) throw new ArgumentOutOfRangeException(name); } - public static void AtLeast(Optional obj, ulong value, string name) { if (obj.IsSpecified && obj.Value < value) throw new ArgumentOutOfRangeException(name); } - - public static void GreaterThan(sbyte obj, sbyte value, string name) { if (obj <= value) throw new ArgumentOutOfRangeException(name); } - public static void GreaterThan(byte obj, byte value, string name) { if (obj <= value) throw new ArgumentOutOfRangeException(name); } - public static void GreaterThan(short obj, short value, string name) { if (obj <= value) throw new ArgumentOutOfRangeException(name); } - public static void GreaterThan(ushort obj, ushort value, string name) { if (obj <= value) throw new ArgumentOutOfRangeException(name); } - public static void GreaterThan(int obj, int value, string name) { if (obj <= value) throw new ArgumentOutOfRangeException(name); } - public static void GreaterThan(uint obj, uint value, string name) { if (obj <= value) throw new ArgumentOutOfRangeException(name); } - public static void GreaterThan(long obj, long value, string name) { if (obj <= value) throw new ArgumentOutOfRangeException(name); } - public static void GreaterThan(ulong obj, ulong value, string name) { if (obj <= value) throw new ArgumentOutOfRangeException(name); } - public static void GreaterThan(Optional obj, sbyte value, string name) { if (obj.IsSpecified && obj.Value <= value) throw new ArgumentOutOfRangeException(name); } - public static void GreaterThan(Optional obj, byte value, string name) { if (obj.IsSpecified && obj.Value <= value) throw new ArgumentOutOfRangeException(name); } - public static void GreaterThan(Optional obj, short value, string name) { if (obj.IsSpecified && obj.Value <= value) throw new ArgumentOutOfRangeException(name); } - public static void GreaterThan(Optional obj, ushort value, string name) { if (obj.IsSpecified && obj.Value <= value) throw new ArgumentOutOfRangeException(name); } - public static void GreaterThan(Optional obj, int value, string name) { if (obj.IsSpecified && obj.Value <= value) throw new ArgumentOutOfRangeException(name); } - public static void GreaterThan(Optional obj, uint value, string name) { if (obj.IsSpecified && obj.Value <= value) throw new ArgumentOutOfRangeException(name); } - public static void GreaterThan(Optional obj, long value, string name) { if (obj.IsSpecified && obj.Value <= value) throw new ArgumentOutOfRangeException(name); } - public static void GreaterThan(Optional obj, ulong value, string name) { if (obj.IsSpecified && obj.Value <= value) throw new ArgumentOutOfRangeException(name); } - - public static void AtMost(sbyte obj, sbyte value, string name) { if (obj > value) throw new ArgumentOutOfRangeException(name); } - public static void AtMost(byte obj, byte value, string name) { if (obj > value) throw new ArgumentOutOfRangeException(name); } - public static void AtMost(short obj, short value, string name) { if (obj > value) throw new ArgumentOutOfRangeException(name); } - public static void AtMost(ushort obj, ushort value, string name) { if (obj > value) throw new ArgumentOutOfRangeException(name); } - public static void AtMost(int obj, int value, string name) { if (obj > value) throw new ArgumentOutOfRangeException(name); } - public static void AtMost(uint obj, uint value, string name) { if (obj > value) throw new ArgumentOutOfRangeException(name); } - public static void AtMost(long obj, long value, string name) { if (obj > value) throw new ArgumentOutOfRangeException(name); } - public static void AtMost(ulong obj, ulong value, string name) { if (obj > value) throw new ArgumentOutOfRangeException(name); } - public static void AtMost(Optional obj, sbyte value, string name) { if (obj.IsSpecified && obj.Value > value) throw new ArgumentOutOfRangeException(name); } - public static void AtMost(Optional obj, byte value, string name) { if (obj.IsSpecified && obj.Value > value) throw new ArgumentOutOfRangeException(name); } - public static void AtMost(Optional obj, short value, string name) { if (obj.IsSpecified && obj.Value > value) throw new ArgumentOutOfRangeException(name); } - public static void AtMost(Optional obj, ushort value, string name) { if (obj.IsSpecified && obj.Value > value) throw new ArgumentOutOfRangeException(name); } - public static void AtMost(Optional obj, int value, string name) { if (obj.IsSpecified && obj.Value > value) throw new ArgumentOutOfRangeException(name); } - public static void AtMost(Optional obj, uint value, string name) { if (obj.IsSpecified && obj.Value > value) throw new ArgumentOutOfRangeException(name); } - public static void AtMost(Optional obj, long value, string name) { if (obj.IsSpecified && obj.Value > value) throw new ArgumentOutOfRangeException(name); } - public static void AtMost(Optional obj, ulong value, string name) { if (obj.IsSpecified && obj.Value > value) throw new ArgumentOutOfRangeException(name); } - - public static void LessThan(sbyte obj, sbyte value, string name) { if (obj >= value) throw new ArgumentOutOfRangeException(name); } - public static void LessThan(byte obj, byte value, string name) { if (obj >= value) throw new ArgumentOutOfRangeException(name); } - public static void LessThan(short obj, short value, string name) { if (obj >= value) throw new ArgumentOutOfRangeException(name); } - public static void LessThan(ushort obj, ushort value, string name) { if (obj >= value) throw new ArgumentOutOfRangeException(name); } - public static void LessThan(int obj, int value, string name) { if (obj >= value) throw new ArgumentOutOfRangeException(name); } - public static void LessThan(uint obj, uint value, string name) { if (obj >= value) throw new ArgumentOutOfRangeException(name); } - public static void LessThan(long obj, long value, string name) { if (obj >= value) throw new ArgumentOutOfRangeException(name); } - public static void LessThan(ulong obj, ulong value, string name) { if (obj >= value) throw new ArgumentOutOfRangeException(name); } - public static void LessThan(Optional obj, sbyte value, string name) { if (obj.IsSpecified && obj.Value >= value) throw new ArgumentOutOfRangeException(name); } - public static void LessThan(Optional obj, byte value, string name) { if (obj.IsSpecified && obj.Value >= value) throw new ArgumentOutOfRangeException(name); } - public static void LessThan(Optional obj, short value, string name) { if (obj.IsSpecified && obj.Value >= value) throw new ArgumentOutOfRangeException(name); } - public static void LessThan(Optional obj, ushort value, string name) { if (obj.IsSpecified && obj.Value >= value) throw new ArgumentOutOfRangeException(name); } - public static void LessThan(Optional obj, int value, string name) { if (obj.IsSpecified && obj.Value >= value) throw new ArgumentOutOfRangeException(name); } - public static void LessThan(Optional obj, uint value, string name) { if (obj.IsSpecified && obj.Value >= value) throw new ArgumentOutOfRangeException(name); } - public static void LessThan(Optional obj, long value, string name) { if (obj.IsSpecified && obj.Value >= value) throw new ArgumentOutOfRangeException(name); } - public static void LessThan(Optional obj, ulong value, string name) { if (obj.IsSpecified && obj.Value >= value) throw new ArgumentOutOfRangeException(name); } + public static void NotEqual(sbyte obj, sbyte value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(byte obj, byte value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(short obj, short value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(ushort obj, ushort value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(int obj, int value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(uint obj, uint value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(long obj, long value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(ulong obj, ulong value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(Optional obj, sbyte value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(Optional obj, byte value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(Optional obj, short value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(Optional obj, ushort value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(Optional obj, int value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(Optional obj, uint value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(Optional obj, long value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(Optional obj, ulong value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(sbyte? obj, sbyte value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(byte? obj, byte value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(short? obj, short value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(ushort? obj, ushort value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(int? obj, int value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(uint? obj, uint value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(long? obj, long value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(ulong? obj, ulong value, string name, string msg = null) { if (obj == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(Optional obj, sbyte value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(Optional obj, byte value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(Optional obj, short value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(Optional obj, ushort value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(Optional obj, int value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(Optional obj, uint value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(Optional obj, long value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + public static void NotEqual(Optional obj, ulong value, string name, string msg = null) { if (obj.IsSpecified && obj.Value == value) throw CreateNotEqualException(name, msg, value); } + + private static ArgumentException CreateNotEqualException(string name, string msg, T value) + { + if (msg == null) return new ArgumentException($"Value may not be equal to {value}", name); + else return new ArgumentException(msg, name); + } + + public static void AtLeast(sbyte obj, sbyte value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } + public static void AtLeast(byte obj, byte value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } + public static void AtLeast(short obj, short value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } + public static void AtLeast(ushort obj, ushort value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } + public static void AtLeast(int obj, int value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } + public static void AtLeast(uint obj, uint value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } + public static void AtLeast(long obj, long value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } + public static void AtLeast(ulong obj, ulong value, string name, string msg = null) { if (obj < value) throw CreateAtLeastException(name, msg, value); } + public static void AtLeast(Optional obj, sbyte value, string name, string msg = null) { if (obj.IsSpecified && obj.Value < value) throw CreateAtLeastException(name, msg, value); } + public static void AtLeast(Optional obj, byte value, string name, string msg = null) { if (obj.IsSpecified && obj.Value < value) throw CreateAtLeastException(name, msg, value); } + public static void AtLeast(Optional obj, short value, string name, string msg = null) { if (obj.IsSpecified && obj.Value < value) throw CreateAtLeastException(name, msg, value); } + public static void AtLeast(Optional obj, ushort value, string name, string msg = null) { if (obj.IsSpecified && obj.Value < value) throw CreateAtLeastException(name, msg, value); } + public static void AtLeast(Optional obj, int value, string name, string msg = null) { if (obj.IsSpecified && obj.Value < value) throw CreateAtLeastException(name, msg, value); } + public static void AtLeast(Optional obj, uint value, string name, string msg = null) { if (obj.IsSpecified && obj.Value < value) throw CreateAtLeastException(name, msg, value); } + public static void AtLeast(Optional obj, long value, string name, string msg = null) { if (obj.IsSpecified && obj.Value < value) throw CreateAtLeastException(name, msg, value); } + public static void AtLeast(Optional obj, ulong value, string name, string msg = null) { if (obj.IsSpecified && obj.Value < value) throw CreateAtLeastException(name, msg, value); } + + private static ArgumentException CreateAtLeastException(string name, string msg, T value) + { + if (msg == null) return new ArgumentException($"Value must be at least {value}", name); + else return new ArgumentException(msg, name); + } + + public static void GreaterThan(sbyte obj, sbyte value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } + public static void GreaterThan(byte obj, byte value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } + public static void GreaterThan(short obj, short value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } + public static void GreaterThan(ushort obj, ushort value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } + public static void GreaterThan(int obj, int value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } + public static void GreaterThan(uint obj, uint value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } + public static void GreaterThan(long obj, long value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } + public static void GreaterThan(ulong obj, ulong value, string name, string msg = null) { if (obj <= value) throw CreateGreaterThanException(name, msg, value); } + public static void GreaterThan(Optional obj, sbyte value, string name, string msg = null) { if (obj.IsSpecified && obj.Value <= value) throw CreateGreaterThanException(name, msg, value); } + public static void GreaterThan(Optional obj, byte value, string name, string msg = null) { if (obj.IsSpecified && obj.Value <= value) throw CreateGreaterThanException(name, msg, value); } + public static void GreaterThan(Optional obj, short value, string name, string msg = null) { if (obj.IsSpecified && obj.Value <= value) throw CreateGreaterThanException(name, msg, value); } + public static void GreaterThan(Optional obj, ushort value, string name, string msg = null) { if (obj.IsSpecified && obj.Value <= value) throw CreateGreaterThanException(name, msg, value); } + public static void GreaterThan(Optional obj, int value, string name, string msg = null) { if (obj.IsSpecified && obj.Value <= value) throw CreateGreaterThanException(name, msg, value); } + public static void GreaterThan(Optional obj, uint value, string name, string msg = null) { if (obj.IsSpecified && obj.Value <= value) throw CreateGreaterThanException(name, msg, value); } + public static void GreaterThan(Optional obj, long value, string name, string msg = null) { if (obj.IsSpecified && obj.Value <= value) throw CreateGreaterThanException(name, msg, value); } + public static void GreaterThan(Optional obj, ulong value, string name, string msg = null) { if (obj.IsSpecified && obj.Value <= value) throw CreateGreaterThanException(name, msg, value); } + + private static ArgumentException CreateGreaterThanException(string name, string msg, T value) + { + if (msg == null) return new ArgumentException($"Value must be greater than {value}", name); + else return new ArgumentException(msg, name); + } + + public static void AtMost(sbyte obj, sbyte value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } + public static void AtMost(byte obj, byte value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } + public static void AtMost(short obj, short value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } + public static void AtMost(ushort obj, ushort value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } + public static void AtMost(int obj, int value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } + public static void AtMost(uint obj, uint value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } + public static void AtMost(long obj, long value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } + public static void AtMost(ulong obj, ulong value, string name, string msg = null) { if (obj > value) throw CreateAtMostException(name, msg, value); } + public static void AtMost(Optional obj, sbyte value, string name, string msg = null) { if (obj.IsSpecified && obj.Value > value) throw CreateAtMostException(name, msg, value); } + public static void AtMost(Optional obj, byte value, string name, string msg = null) { if (obj.IsSpecified && obj.Value > value) throw CreateAtMostException(name, msg, value); } + public static void AtMost(Optional obj, short value, string name, string msg = null) { if (obj.IsSpecified && obj.Value > value) throw CreateAtMostException(name, msg, value); } + public static void AtMost(Optional obj, ushort value, string name, string msg = null) { if (obj.IsSpecified && obj.Value > value) throw CreateAtMostException(name, msg, value); } + public static void AtMost(Optional obj, int value, string name, string msg = null) { if (obj.IsSpecified && obj.Value > value) throw CreateAtMostException(name, msg, value); } + public static void AtMost(Optional obj, uint value, string name, string msg = null) { if (obj.IsSpecified && obj.Value > value) throw CreateAtMostException(name, msg, value); } + public static void AtMost(Optional obj, long value, string name, string msg = null) { if (obj.IsSpecified && obj.Value > value) throw CreateAtMostException(name, msg, value); } + public static void AtMost(Optional obj, ulong value, string name, string msg = null) { if (obj.IsSpecified && obj.Value > value) throw CreateAtMostException(name, msg, value); } + + private static ArgumentException CreateAtMostException(string name, string msg, T value) + { + if (msg == null) return new ArgumentException($"Value must be at most {value}", name); + else return new ArgumentException(msg, name); + } + + public static void LessThan(sbyte obj, sbyte value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } + public static void LessThan(byte obj, byte value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } + public static void LessThan(short obj, short value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } + public static void LessThan(ushort obj, ushort value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } + public static void LessThan(int obj, int value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } + public static void LessThan(uint obj, uint value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } + public static void LessThan(long obj, long value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } + public static void LessThan(ulong obj, ulong value, string name, string msg = null) { if (obj >= value) throw CreateLessThanException(name, msg, value); } + public static void LessThan(Optional obj, sbyte value, string name, string msg = null) { if (obj.IsSpecified && obj.Value >= value) throw CreateLessThanException(name, msg, value); } + public static void LessThan(Optional obj, byte value, string name, string msg = null) { if (obj.IsSpecified && obj.Value >= value) throw CreateLessThanException(name, msg, value); } + public static void LessThan(Optional obj, short value, string name, string msg = null) { if (obj.IsSpecified && obj.Value >= value) throw CreateLessThanException(name, msg, value); } + public static void LessThan(Optional obj, ushort value, string name, string msg = null) { if (obj.IsSpecified && obj.Value >= value) throw CreateLessThanException(name, msg, value); } + public static void LessThan(Optional obj, int value, string name, string msg = null) { if (obj.IsSpecified && obj.Value >= value) throw CreateLessThanException(name, msg, value); } + public static void LessThan(Optional obj, uint value, string name, string msg = null) { if (obj.IsSpecified && obj.Value >= value) throw CreateLessThanException(name, msg, value); } + public static void LessThan(Optional obj, long value, string name, string msg = null) { if (obj.IsSpecified && obj.Value >= value) throw CreateLessThanException(name, msg, value); } + public static void LessThan(Optional obj, ulong value, string name, string msg = null) { if (obj.IsSpecified && obj.Value >= value) throw CreateLessThanException(name, msg, value); } + + private static ArgumentException CreateLessThanException(string name, string msg, T value) + { + if (msg == null) return new ArgumentException($"Value must be less than {value}", name); + else return new ArgumentException(msg, name); + } } } diff --git a/src/Discord.Net.Core/project.json b/src/Discord.Net.Core/project.json index c44be49b6..6a62b9474 100644 --- a/src/Discord.Net.Core/project.json +++ b/src/Discord.Net.Core/project.json @@ -1,5 +1,5 @@ -{ - "version": "1.0.0-beta2-*", +{ + "version": "1.0.0-*", "description": "A .Net API wrapper and bot framework for Discord.", "authors": [ "RogueException" ], @@ -26,14 +26,14 @@ }, "dependencies": { - "Microsoft.Win32.Primitives": "4.0.1", + "Microsoft.Win32.Primitives": "4.3.0", "Newtonsoft.Json": "9.0.1", - "System.Collections.Concurrent": "4.0.12", - "System.Collections.Immutable": "1.2.0", - "System.Interactive.Async": "3.0.0", - "System.Net.Http": "4.1.0", + "System.Collections.Concurrent": "4.3.0", + "System.Collections.Immutable": "1.3.0", + "System.Interactive.Async": "3.1.0", + "System.Net.Http": "4.3.0", "System.Net.WebSockets.Client": { - "version": "4.0.0", + "version": "4.3.0", "type": "build" } }, diff --git a/src/Discord.Net.Rest/Discord.Net.Rest.csproj b/src/Discord.Net.Rest/Discord.Net.Rest.csproj new file mode 100644 index 000000000..6bc40571f --- /dev/null +++ b/src/Discord.Net.Rest/Discord.Net.Rest.csproj @@ -0,0 +1,44 @@ + + + + A core Discord.Net library containing the REST client and models. + 1.0.0-beta2 + netstandard1.3 + Discord.Net.Rest + discord;discordapp + https://github.com/RogueException/Discord.Net + http://opensource.org/licenses/MIT + git + git://github.com/RogueException/Discord.Net + $(PackageTargetFallback);dotnet5.4;dnxcore50;portable-net45+win8 + + + + + + + + + + + + + 1.0.0-alpha-20161104-2 + All + + + 4.3.0 + + + + + False + + + $(DefineConstants);RELEASE + $(NoWarn);CS1573;CS1591 + true + true + + + \ No newline at end of file diff --git a/src/Discord.Net.Rest/Discord.Net.Rest.xproj b/src/Discord.Net.Rest/Discord.Net.Rest.xproj deleted file mode 100644 index 6a5d3e2b8..000000000 --- a/src/Discord.Net.Rest/Discord.Net.Rest.xproj +++ /dev/null @@ -1,19 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - bfc6dc28-0351-4573-926a-d4124244c04f - Discord.Rest - .\obj - .\bin\ - v4.6.1 - - - 2.0 - - - \ No newline at end of file diff --git a/src/Discord.Net.Rest/DiscordRestClient.cs b/src/Discord.Net.Rest/DiscordRestClient.cs index f36c0fb06..8c1bd45da 100644 --- a/src/Discord.Net.Rest/DiscordRestClient.cs +++ b/src/Discord.Net.Rest/DiscordRestClient.cs @@ -8,6 +8,8 @@ namespace Discord.Rest { public class DiscordRestClient : BaseDiscordClient, IDiscordClient { + private RestApplication _applicationInfo; + public new RestSelfUser CurrentUser => base.CurrentUser as RestSelfUser; public DiscordRestClient() : this(new DiscordRestConfig()) { } @@ -21,10 +23,17 @@ namespace Discord.Rest base.CurrentUser = RestSelfUser.Create(this, ApiClient.CurrentUser); return Task.CompletedTask; } + protected override Task OnLogoutAsync() + { + _applicationInfo = null; + return Task.CompletedTask; + } /// - public Task GetApplicationInfoAsync() - => ClientHelper.GetApplicationInfoAsync(this); + public async Task GetApplicationInfoAsync() + { + return _applicationInfo ?? (_applicationInfo = await ClientHelper.GetApplicationInfoAsync(this)); + } /// public Task GetChannelAsync(ulong id) diff --git a/src/Discord.Net.Rest/DiscordRestConfig.cs b/src/Discord.Net.Rest/DiscordRestConfig.cs index 8dee72231..33a3cb4e8 100644 --- a/src/Discord.Net.Rest/DiscordRestConfig.cs +++ b/src/Discord.Net.Rest/DiscordRestConfig.cs @@ -6,7 +6,6 @@ namespace Discord.Rest { public static string UserAgent { get; } = $"DiscordBot (https://github.com/RogueException/Discord.Net, v{Version})"; - internal const int RestTimeout = 10000; internal const int MessageQueueInterval = 100; internal const int WebSocketQueueInterval = 100; diff --git a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs index 858269366..99edc6f48 100644 --- a/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs +++ b/src/Discord.Net.Rest/Entities/Channels/ChannelHelper.cs @@ -6,6 +6,7 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using Model = Discord.API.Channel; +using UserModel = Discord.API.User; namespace Discord.Rest { @@ -43,13 +44,13 @@ namespace Discord.Rest } //Invites - public static async Task> GetInvitesAsync(IChannel channel, BaseDiscordClient client, + public static async Task> GetInvitesAsync(IGuildChannel channel, BaseDiscordClient client, RequestOptions options) { var models = await client.ApiClient.GetChannelInvitesAsync(channel.Id, options).ConfigureAwait(false); return models.Select(x => RestInviteMetadata.Create(client, null, channel, x)).ToImmutableArray(); } - public static async Task CreateInviteAsync(IChannel channel, BaseDiscordClient client, + public static async Task CreateInviteAsync(IGuildChannel channel, BaseDiscordClient client, int? maxAge, int? maxUses, bool isTemporary, RequestOptions options) { var args = new CreateChannelInviteParams { IsTemporary = isTemporary }; @@ -62,18 +63,24 @@ namespace Discord.Rest } //Messages - public static async Task GetMessageAsync(IChannel channel, BaseDiscordClient client, - ulong id, IGuild guild, RequestOptions options) + public static async Task GetMessageAsync(IMessageChannel channel, BaseDiscordClient client, + ulong id, RequestOptions options) { + var guildId = (channel as IGuildChannel)?.GuildId; + var guild = guildId != null ? await (client as IDiscordClient).GetGuildAsync(guildId.Value, CacheMode.CacheOnly).ConfigureAwait(false) : null; var model = await client.ApiClient.GetChannelMessageAsync(channel.Id, id, options).ConfigureAwait(false); - return RestMessage.Create(client, guild, model); + var author = GetAuthor(client, guild, model.Author.Value); + return RestMessage.Create(client, channel, author, model); } - public static IAsyncEnumerable> GetMessagesAsync(IChannel channel, BaseDiscordClient client, - ulong? fromMessageId, Direction dir, int limit, IGuild guild, RequestOptions options) + public static IAsyncEnumerable> GetMessagesAsync(IMessageChannel channel, BaseDiscordClient client, + ulong? fromMessageId, Direction dir, int limit, RequestOptions options) { if (dir == Direction.Around) throw new NotImplementedException(); //TODO: Impl + var guildId = (channel as IGuildChannel)?.GuildId; + var guild = guildId != null ? (client as IDiscordClient).GetGuildAsync(guildId.Value, CacheMode.CacheOnly).Result : null; + return new PagedAsyncEnumerable( DiscordConfig.MaxMessagesPerBatch, async (info, ct) => @@ -85,8 +92,15 @@ namespace Discord.Rest }; if (info.Position != null) args.RelativeMessageId = info.Position.Value; + var models = await client.ApiClient.GetChannelMessagesAsync(channel.Id, args, options).ConfigureAwait(false); - return models.Select(x => RestMessage.Create(client, guild, x)).ToImmutableArray(); + var builder = ImmutableArray.CreateBuilder(); + foreach (var model in models) + { + var author = GetAuthor(client, guild, model.Author.Value); + builder.Add(RestMessage.Create(client, channel, author, model)); + } + return builder.ToImmutable(); }, nextPage: (info, lastPage) => { @@ -102,37 +116,45 @@ namespace Discord.Rest count: limit ); } - public static async Task> GetPinnedMessagesAsync(IChannel channel, BaseDiscordClient client, - IGuild guild, RequestOptions options) + public static async Task> GetPinnedMessagesAsync(IMessageChannel channel, BaseDiscordClient client, + RequestOptions options) { + var guildId = (channel as IGuildChannel)?.GuildId; + var guild = guildId != null ? await (client as IDiscordClient).GetGuildAsync(guildId.Value, CacheMode.CacheOnly).ConfigureAwait(false) : null; var models = await client.ApiClient.GetPinsAsync(channel.Id, options).ConfigureAwait(false); - return models.Select(x => RestMessage.Create(client, guild, x)).ToImmutableArray(); + var builder = ImmutableArray.CreateBuilder(); + foreach (var model in models) + { + var author = GetAuthor(client, guild, model.Author.Value); + builder.Add(RestMessage.Create(client, channel, author, model)); + } + return builder.ToImmutable(); } - public static async Task SendMessageAsync(IChannel channel, BaseDiscordClient client, - string text, bool isTTS, IGuild guild, RequestOptions options) + public static async Task SendMessageAsync(IMessageChannel channel, BaseDiscordClient client, + string text, bool isTTS, EmbedBuilder embed, RequestOptions options) { - var args = new CreateMessageParams(text) { IsTTS = isTTS }; + var args = new CreateMessageParams(text) { IsTTS = isTTS, Embed = embed?.Build() }; var model = await client.ApiClient.CreateMessageAsync(channel.Id, args, options).ConfigureAwait(false); - return RestUserMessage.Create(client, guild, model); + return RestUserMessage.Create(client, channel, client.CurrentUser, model); } - public static async Task SendFileAsync(IChannel channel, BaseDiscordClient client, - string filePath, string text, bool isTTS, IGuild guild, RequestOptions options) + public static async Task SendFileAsync(IMessageChannel channel, BaseDiscordClient client, + string filePath, string text, bool isTTS, RequestOptions options) { string filename = Path.GetFileName(filePath); using (var file = File.OpenRead(filePath)) - return await SendFileAsync(channel, client, file, filename, text, isTTS, guild, options).ConfigureAwait(false); + return await SendFileAsync(channel, client, file, filename, text, isTTS, options).ConfigureAwait(false); } - public static async Task SendFileAsync(IChannel channel, BaseDiscordClient client, - Stream stream, string filename, string text, bool isTTS, IGuild guild, RequestOptions options) + public static async Task SendFileAsync(IMessageChannel channel, BaseDiscordClient client, + Stream stream, string filename, string text, bool isTTS, RequestOptions options) { var args = new UploadFileParams(stream) { Filename = filename, Content = text, IsTTS = isTTS }; var model = await client.ApiClient.UploadFileAsync(channel.Id, args, options).ConfigureAwait(false); - return RestUserMessage.Create(client, guild, model); + return RestUserMessage.Create(client, channel, client.CurrentUser, model); } - public static async Task DeleteMessagesAsync(IChannel channel, BaseDiscordClient client, + public static async Task DeleteMessagesAsync(IMessageChannel channel, BaseDiscordClient client, IEnumerable messages, RequestOptions options) { var args = new DeleteMessagesParams(messages.Select(x => x.Id).ToArray()); @@ -216,5 +238,16 @@ namespace Discord.Rest public static IDisposable EnterTypingState(IMessageChannel channel, BaseDiscordClient client, RequestOptions options) => new TypingNotifier(client, channel, options); + + //Helpers + private static IUser GetAuthor(BaseDiscordClient client, IGuild guild, UserModel model) + { + IUser author = null; + if (guild != null) + author = guild.GetUserAsync(model.Id, CacheMode.CacheOnly).Result; + if (author == null) + author = RestUser.Create(client, model); + return author; + } } } diff --git a/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs b/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs index 6d2b729dd..554104d4d 100644 --- a/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/IRestMessageChannel.cs @@ -7,7 +7,7 @@ namespace Discord.Rest public interface IRestMessageChannel : IMessageChannel { /// Sends a message to this message channel. - new Task SendMessageAsync(string text, bool isTTS = false, RequestOptions options = null); + new Task SendMessageAsync(string text, bool isTTS = false, EmbedBuilder embed = null, RequestOptions options = null); /// Sends a file to this text channel, with an optional caption. new Task SendFileAsync(string filePath, string text = null, bool isTTS = false, RequestOptions options = null); /// Sends a file to this text channel, with an optional caption. diff --git a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs index 9d866220d..573cfef72 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestDMChannel.cs @@ -53,22 +53,22 @@ namespace Discord.Rest } public Task GetMessageAsync(ulong id, RequestOptions options = null) - => ChannelHelper.GetMessageAsync(this, Discord, id, null, options); + => ChannelHelper.GetMessageAsync(this, Discord, id, options); public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) - => ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit, null, options); + => ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit, options); public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) - => ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit, null, options); + => ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit, options); public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) - => ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit, null, options); + => ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit, options); public Task> GetPinnedMessagesAsync(RequestOptions options = null) - => ChannelHelper.GetPinnedMessagesAsync(this, Discord, null, options); + => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); - public Task SendMessageAsync(string text, bool isTTS = false, RequestOptions options = null) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, null, options); + public Task SendMessageAsync(string text, bool isTTS = false, EmbedBuilder embed = null, RequestOptions options = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); public Task SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, null, options); + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, null, options); + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); @@ -126,8 +126,8 @@ namespace Discord.Rest => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) => await SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false); - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, RequestOptions options) - => await SendMessageAsync(text, isTTS, options).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, EmbedBuilder embed, RequestOptions options) + => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); IDisposable IMessageChannel.EnterTypingState(RequestOptions options) => EnterTypingState(options); diff --git a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs index bb840ece4..e2a015c75 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestGroupChannel.cs @@ -66,22 +66,22 @@ namespace Discord.Rest } public Task GetMessageAsync(ulong id, RequestOptions options = null) - => ChannelHelper.GetMessageAsync(this, Discord, id, null, options); + => ChannelHelper.GetMessageAsync(this, Discord, id, options); public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) - => ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit, null, options); + => ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit, options); public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) - => ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit, null, options); + => ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit, options); public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) - => ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit, null, options); + => ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit, options); public Task> GetPinnedMessagesAsync(RequestOptions options = null) - => ChannelHelper.GetPinnedMessagesAsync(this, Discord, null, options); + => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); - public Task SendMessageAsync(string text, bool isTTS = false, RequestOptions options = null) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, null, options); + public Task SendMessageAsync(string text, bool isTTS = false, EmbedBuilder embed = null, RequestOptions options = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); public Task SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, null, options); + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, null, options); + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); @@ -136,8 +136,8 @@ namespace Discord.Rest => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) => await SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false); - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, RequestOptions options) - => await SendMessageAsync(text, isTTS, options).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, EmbedBuilder embed, RequestOptions options) + => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); IDisposable IMessageChannel.EnterTypingState(RequestOptions options) => EnterTypingState(options); diff --git a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs index 4a617517e..991b30283 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestTextChannel.cs @@ -45,22 +45,22 @@ namespace Discord.Rest => ChannelHelper.GetUsersAsync(this, Guild, Discord, null, null, options); public Task GetMessageAsync(ulong id, RequestOptions options = null) - => ChannelHelper.GetMessageAsync(this, Discord, id, null, options); + => ChannelHelper.GetMessageAsync(this, Discord, id, options); public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) - => ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit, null, options); + => ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit, options); public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) - => ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit, null, options); + => ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit, options); public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) - => ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit, null, options); + => ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit, options); public Task> GetPinnedMessagesAsync(RequestOptions options = null) - => ChannelHelper.GetPinnedMessagesAsync(this, Discord, null, options); + => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); - public Task SendMessageAsync(string text, bool isTTS = false, RequestOptions options = null) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, null, options); + public Task SendMessageAsync(string text, bool isTTS = false, EmbedBuilder embed = null, RequestOptions options = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); public Task SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, null, options); + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, null, options); + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); @@ -108,8 +108,8 @@ namespace Discord.Rest => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) => await SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false); - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, RequestOptions options) - => await SendMessageAsync(text, isTTS, options).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, EmbedBuilder embed, RequestOptions options) + => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); IDisposable IMessageChannel.EnterTypingState(RequestOptions options) => EnterTypingState(options); diff --git a/src/Discord.Net.Rest/Entities/Channels/RestVirtualMessageChannel.cs b/src/Discord.Net.Rest/Entities/Channels/RestVirtualMessageChannel.cs index 97a0a93e6..6127eaf65 100644 --- a/src/Discord.Net.Rest/Entities/Channels/RestVirtualMessageChannel.cs +++ b/src/Discord.Net.Rest/Entities/Channels/RestVirtualMessageChannel.cs @@ -23,22 +23,22 @@ namespace Discord.Rest } public Task GetMessageAsync(ulong id, RequestOptions options = null) - => ChannelHelper.GetMessageAsync(this, Discord, id, null, options); + => ChannelHelper.GetMessageAsync(this, Discord, id, options); public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) - => ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit, null, options); + => ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit, options); public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) - => ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit, null, options); + => ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit, options); public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) - => ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit, null, options); + => ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit, options); public Task> GetPinnedMessagesAsync(RequestOptions options = null) - => ChannelHelper.GetPinnedMessagesAsync(this, Discord, null, options); + => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); - public Task SendMessageAsync(string text, bool isTTS, RequestOptions options = null) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, null, options); + public Task SendMessageAsync(string text, bool isTTS, EmbedBuilder embed = null, RequestOptions options = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); public Task SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, null, options); + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, null, options); + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); @@ -86,8 +86,8 @@ namespace Discord.Rest => await SendFileAsync(filePath, text, isTTS, options); async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) => await SendFileAsync(stream, filename, text, isTTS, options); - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, RequestOptions options) - => await SendMessageAsync(text, isTTS, options); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, EmbedBuilder embed, RequestOptions options) + => await SendMessageAsync(text, isTTS, embed, options); IDisposable IMessageChannel.EnterTypingState(RequestOptions options) => EnterTypingState(options); diff --git a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs index f91f131d9..7aaa19304 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/GuildHelper.cs @@ -142,7 +142,7 @@ namespace Discord.Rest if (name == null) throw new ArgumentNullException(nameof(name)); var model = await client.ApiClient.CreateGuildRoleAsync(guild.Id, options).ConfigureAwait(false); - var role = RestRole.Create(client, model); + var role = RestRole.Create(client, guild, model); await role.ModifyAsync(x => { diff --git a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs index a0ba7a6de..a85107831 100644 --- a/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs +++ b/src/Discord.Net.Rest/Entities/Guilds/RestGuild.cs @@ -87,7 +87,7 @@ namespace Discord.Rest if (model.Roles != null) { for (int i = 0; i < model.Roles.Length; i++) - roles[model.Roles[i].Id] = RestRole.Create(Discord, model.Roles[i]); + roles[model.Roles[i].Id] = RestRole.Create(Discord, this, model.Roles[i]); } _roles = roles.ToImmutable(); diff --git a/src/Discord.Net.Rest/Entities/Messages/Embed.cs b/src/Discord.Net.Rest/Entities/Messages/Embed.cs index 20979534e..a291dc6c0 100644 --- a/src/Discord.Net.Rest/Entities/Messages/Embed.cs +++ b/src/Discord.Net.Rest/Entities/Messages/Embed.cs @@ -1,4 +1,7 @@ -using System.Diagnostics; +using System; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; using Model = Discord.API.Embed; namespace Discord @@ -10,23 +13,55 @@ namespace Discord public string Url { get; } public string Title { get; } public string Type { get; } + public DateTimeOffset? Timestamp { get; } + public Color? Color { get; } + public EmbedImage? Image { get; } + public EmbedVideo? Video { get; } + public EmbedAuthor? Author { get; } + public EmbedFooter? Footer { get; } public EmbedProvider? Provider { get; } public EmbedThumbnail? Thumbnail { get; } + public ImmutableArray Fields { get; } - internal Embed(string type, string title, string description, string url, EmbedProvider? provider, EmbedThumbnail? thumbnail) + internal Embed(string type, + string title, + string description, + string url, + DateTimeOffset? timestamp, + Color? color, + EmbedImage? image, + EmbedVideo? video, + EmbedAuthor? author, + EmbedFooter? footer, + EmbedProvider? provider, + EmbedThumbnail? thumbnail, + ImmutableArray fields) { Type = type; Title = title; Description = description; Url = url; + Color = color; + Timestamp = timestamp; + Image = image; + Video = video; + Author = author; + Footer = footer; Provider = provider; Thumbnail = thumbnail; + Fields = fields; } internal static Embed Create(Model model) { - return new Embed(model.Type, model.Title, model.Description, model.Url, + return new Embed(model.Type, model.Title, model.Description, model.Url,model.Timestamp, + model.Color.HasValue ? new Color(model.Color.Value) : (Color?)null, + model.Image.IsSpecified ? EmbedImage.Create(model.Image.Value) : (EmbedImage?)null, + model.Video.IsSpecified ? EmbedVideo.Create(model.Video.Value) : (EmbedVideo?)null, + model.Author.IsSpecified ? EmbedAuthor.Create(model.Author.Value) : (EmbedAuthor?)null, + model.Footer.IsSpecified ? EmbedFooter.Create(model.Footer.Value) : (EmbedFooter?)null, model.Provider.IsSpecified ? EmbedProvider.Create(model.Provider.Value) : (EmbedProvider?)null, - model.Thumbnail.IsSpecified ? EmbedThumbnail.Create(model.Thumbnail.Value) : (EmbedThumbnail?)null); + model.Thumbnail.IsSpecified ? EmbedThumbnail.Create(model.Thumbnail.Value) : (EmbedThumbnail?)null, + model.Fields.IsSpecified ? model.Fields.Value.Select(EmbedField.Create).ToImmutableArray() : ImmutableArray.Create()); } public override string ToString() => Title; diff --git a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs index 33df3631c..e7115babd 100644 --- a/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs +++ b/src/Discord.Net.Rest/Entities/Messages/MessageHelper.cs @@ -15,7 +15,12 @@ namespace Discord.Rest { var args = new ModifyMessageParams(); func(args); - return await client.ApiClient.ModifyMessageAsync(msg.Channel.Id, msg.Id, args, options).ConfigureAwait(false); + var apiArgs = new API.Rest.ModifyMessageParams + { + Content = args.Content, + Embed = args.Embed.IsSpecified ? args.Embed.Value.Build() : Optional.Create() + }; + return await client.ApiClient.ModifyMessageAsync(msg.Channel.Id, msg.Id, apiArgs, options).ConfigureAwait(false); } public static async Task DeleteAsync(IMessage msg, BaseDiscordClient client, RequestOptions options) diff --git a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs index ea064dd81..b50edf03b 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestMessage.cs @@ -9,11 +9,10 @@ namespace Discord.Rest { public abstract class RestMessage : RestEntity, IMessage, IUpdateable { - internal readonly IGuild _guild; private long _timestampTicks; public IMessageChannel Channel { get; } - public RestUser Author { get; } + public IUser Author { get; } public string Content { get; private set; } @@ -32,19 +31,18 @@ namespace Discord.Rest public DateTimeOffset Timestamp => DateTimeUtils.FromTicks(_timestampTicks); - internal RestMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, RestUser author, IGuild guild) + internal RestMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author) : base(discord, id) { Channel = channel; Author = author; - _guild = guild; } - internal static RestMessage Create(BaseDiscordClient discord, IGuild guild, Model model) + internal static RestMessage Create(BaseDiscordClient discord, IMessageChannel channel, IUser author, Model model) { if (model.Type == MessageType.Default) - return RestUserMessage.Create(discord, guild, model); + return RestUserMessage.Create(discord, channel, author, model); else - return RestSystemMessage.Create(discord, guild, model); + return RestSystemMessage.Create(discord, channel, author, model); } internal virtual void Update(Model model) { diff --git a/src/Discord.Net.Rest/Entities/Messages/RestSystemMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestSystemMessage.cs index 0725ab603..a5ced8c8f 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestSystemMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestSystemMessage.cs @@ -8,15 +8,13 @@ namespace Discord.Rest { public MessageType Type { get; private set; } - internal RestSystemMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, RestUser author, IGuild guild) - : base(discord, id, channel, author, guild) + internal RestSystemMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author) + : base(discord, id, channel, author) { } - internal new static RestSystemMessage Create(BaseDiscordClient discord, IGuild guild, Model model) + internal new static RestSystemMessage Create(BaseDiscordClient discord, IMessageChannel channel, IUser author, Model model) { - var entity = new RestSystemMessage(discord, model.Id, - RestVirtualMessageChannel.Create(discord, model.ChannelId), - RestUser.Create(discord, model.Author.Value), guild); + var entity = new RestSystemMessage(discord, model.Id, channel, author); entity.Update(model); return entity; } diff --git a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs index dde1c9d34..b95253b8e 100644 --- a/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs +++ b/src/Discord.Net.Rest/Entities/Messages/RestUserMessage.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; -using System.Linq; using System.Threading.Tasks; using Model = Discord.API.Message; @@ -32,15 +31,13 @@ namespace Discord.Rest public override IReadOnlyCollection Tags => _tags; public IReadOnlyDictionary Reactions => _reactions.ToDictionary(x => x.Emoji, x => x.Count); - internal RestUserMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, RestUser author, IGuild guild) - : base(discord, id, channel, author, guild) + internal RestUserMessage(BaseDiscordClient discord, ulong id, IMessageChannel channel, IUser author) + : base(discord, id, channel, author) { } - internal new static RestUserMessage Create(BaseDiscordClient discord, IGuild guild, Model model) + internal new static RestUserMessage Create(BaseDiscordClient discord, IMessageChannel channel, IUser author, Model model) { - var entity = new RestUserMessage(discord, model.Id, - RestVirtualMessageChannel.Create(discord, model.ChannelId), - RestUser.Create(discord, model.Author.Value), guild); + var entity = new RestUserMessage(discord, model.Id, channel, author); entity.Update(model); return entity; } @@ -122,7 +119,9 @@ namespace Discord.Rest if (model.Content.IsSpecified) { var text = model.Content.Value; - _tags = MessageHelper.ParseTags(text, null, _guild, mentions); + var guildId = (Channel as IGuildChannel)?.GuildId; + var guild = guildId != null ? (Discord as IDiscordClient).GetGuildAsync(guildId.Value, CacheMode.CacheOnly).Result : null; + _tags = MessageHelper.ParseTags(text, null, guild, mentions); model.Content = text; } } @@ -155,9 +154,12 @@ namespace Discord.Rest public Task UnpinAsync(RequestOptions options) => MessageHelper.UnpinAsync(this, Discord, options); + public string Resolve(int startIndex, TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name, + TagHandling roleHandling = TagHandling.Name, TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name) + => MentionUtils.Resolve(this, startIndex, userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling); public string Resolve(TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name, TagHandling roleHandling = TagHandling.Name, TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name) - => MentionUtils.Resolve(this, userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling); + => MentionUtils.Resolve(this, 0, userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling); private string DebuggerDisplay => $"{Author}: {Content} ({Id}{(Attachments.Count > 0 ? $", {Attachments.Count} Attachments" : "")})"; } diff --git a/src/Discord.Net.Rest/Entities/Roles/RestRole.cs b/src/Discord.Net.Rest/Entities/Roles/RestRole.cs index 6e81ce9df..eee1fdf0a 100644 --- a/src/Discord.Net.Rest/Entities/Roles/RestRole.cs +++ b/src/Discord.Net.Rest/Entities/Roles/RestRole.cs @@ -9,7 +9,7 @@ namespace Discord.Rest [DebuggerDisplay(@"{DebuggerDisplay,nq}")] public class RestRole : RestEntity, IRole { - public RestGuild Guild { get; } + internal IGuild Guild { get; } public Color Color { get; private set; } public bool IsHoisted { get; private set; } public bool IsManaged { get; private set; } @@ -22,13 +22,14 @@ namespace Discord.Rest public bool IsEveryone => Id == Guild.Id; public string Mention => MentionUtils.MentionRole(Id); - internal RestRole(BaseDiscordClient discord, ulong id) + internal RestRole(BaseDiscordClient discord, IGuild guild, ulong id) : base(discord, id) { + Guild = guild; } - internal static RestRole Create(BaseDiscordClient discord, Model model) + internal static RestRole Create(BaseDiscordClient discord, IGuild guild, Model model) { - var entity = new RestRole(discord, model.Id); + var entity = new RestRole(discord, guild, model.Id); entity.Update(model); return entity; } @@ -51,10 +52,20 @@ namespace Discord.Rest public Task DeleteAsync(RequestOptions options = null) => RoleHelper.DeleteAsync(this, Discord, options); + public int CompareTo(IRole role) => this.Compare(role); + public override string ToString() => Name; private string DebuggerDisplay => $"{Name} ({Id})"; //IRole - IGuild IRole.Guild => Guild; + IGuild IRole.Guild + { + get + { + if (Guild != null) + return Guild; + throw new InvalidOperationException("Unable to return this entity's parent unless it was fetched through that object."); + } + } } } diff --git a/src/Discord.Net.Rest/project.json b/src/Discord.Net.Rest/project.json index 19b244961..b1ad15cb8 100644 --- a/src/Discord.Net.Rest/project.json +++ b/src/Discord.Net.Rest/project.json @@ -1,5 +1,5 @@ -{ - "version": "1.0.0-beta2-*", +{ + "version": "1.0.0-*", "description": "A core Discord.Net library containing the REST client and models.", "authors": [ "RogueException" ], @@ -29,7 +29,7 @@ "Discord.Net.Core": { "target": "project" }, - "System.IO.FileSystem": "4.0.1" + "System.IO.FileSystem": "4.3.0" }, "frameworks": { @@ -41,4 +41,4 @@ ] } } -} +} \ No newline at end of file diff --git a/src/Discord.Net.Rpc/Discord.Net.Rpc.csproj b/src/Discord.Net.Rpc/Discord.Net.Rpc.csproj new file mode 100644 index 000000000..efd2ea893 --- /dev/null +++ b/src/Discord.Net.Rpc/Discord.Net.Rpc.csproj @@ -0,0 +1,48 @@ + + + + A core Discord.Net library containing the RPC client and models. + 1.0.0-beta2 + netstandard1.3 + Discord.Net.Rpc + discord;discordapp + https://github.com/RogueException/Discord.Net + http://opensource.org/licenses/MIT + git + git://github.com/RogueException/Discord.Net + $(PackageTargetFallback);dotnet5.4;dnxcore50;portable-net45+win8 + + + + + + + + + + + + + + 1.0.0-alpha-20161104-2 + All + + + 4.3.0 + + + 4.3.0 + + + + + False + + + $(DefineConstants);RELEASE + $(NoWarn);CS1573;CS1591 + true + true + + + \ No newline at end of file diff --git a/src/Discord.Net.Rpc/Discord.Net.Rpc.xproj b/src/Discord.Net.Rpc/Discord.Net.Rpc.xproj deleted file mode 100644 index c5d036842..000000000 --- a/src/Discord.Net.Rpc/Discord.Net.Rpc.xproj +++ /dev/null @@ -1,19 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - 5688a353-121e-40a1-8bfa-b17b91fb48fb - Discord.Rpc - .\obj - .\bin\ - v4.6.1 - - - 2.0 - - - \ No newline at end of file diff --git a/src/Discord.Net.Rpc/DiscordRpcClient.cs b/src/Discord.Net.Rpc/DiscordRpcClient.cs index 52fe6172f..3ca288b12 100644 --- a/src/Discord.Net.Rpc/DiscordRpcClient.cs +++ b/src/Discord.Net.Rpc/DiscordRpcClient.cs @@ -14,7 +14,7 @@ using System.Threading.Tasks; namespace Discord.Rpc { - public partial class DiscordRpcClient : BaseDiscordClient + public partial class DiscordRpcClient : BaseDiscordClient, IDiscordClient { private readonly Logger _rpcLogger; private readonly JsonSerializer _serializer; @@ -33,7 +33,7 @@ namespace Discord.Rpc public new API.DiscordRpcApiClient ApiClient => base.ApiClient as API.DiscordRpcApiClient; public new RestSelfUser CurrentUser { get { return base.CurrentUser as RestSelfUser; } private set { base.CurrentUser = value; } } - public RestApplication CurrentApplication { get; private set; } + public RestApplication ApplicationInfo { get; private set; } /// Creates a new RPC discord client. public DiscordRpcClient(string clientId, string origin) @@ -393,7 +393,7 @@ namespace Discord.Rpc { var response = await ApiClient.SendAuthenticateAsync(options).ConfigureAwait(false); CurrentUser = RestSelfUser.Create(this, response.User); - CurrentApplication = RestApplication.Create(this, response.Application); + ApplicationInfo = RestApplication.Create(this, response.Application); Scopes = response.Scopes; TokenExpiresAt = response.Expires; @@ -547,5 +547,8 @@ namespace Discord.Rpc return; } } + + //IDiscordClient + Task IDiscordClient.GetApplicationInfoAsync() => Task.FromResult(ApplicationInfo); } } diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs index 3df04b320..af0102574 100644 --- a/src/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcDMChannel.cs @@ -34,22 +34,22 @@ namespace Discord.Rpc //TODO: Use RPC cache public Task GetMessageAsync(ulong id, RequestOptions options = null) - => ChannelHelper.GetMessageAsync(this, Discord, id, null, options); + => ChannelHelper.GetMessageAsync(this, Discord, id, options); public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) - => ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit, null, options); + => ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit, options); public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) - => ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit, null, options); + => ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit, options); public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) - => ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit, null, options); + => ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit, options); public Task> GetPinnedMessagesAsync(RequestOptions options = null) - => ChannelHelper.GetPinnedMessagesAsync(this, Discord, null, options); + => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); - public Task SendMessageAsync(string text, bool isTTS = false, RequestOptions options = null) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, null, options); + public Task SendMessageAsync(string text, bool isTTS = false, EmbedBuilder embed = null, RequestOptions options = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); public Task SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, null, options); + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, null, options); + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); @@ -104,8 +104,8 @@ namespace Discord.Rpc => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) => await SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false); - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, RequestOptions options) - => await SendMessageAsync(text, isTTS, options).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, EmbedBuilder embed, RequestOptions options) + => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); IDisposable IMessageChannel.EnterTypingState(RequestOptions options) => EnterTypingState(options); diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs index d88847067..c88621d8f 100644 --- a/src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcGroupChannel.cs @@ -36,22 +36,22 @@ namespace Discord.Rpc //TODO: Use RPC cache public Task GetMessageAsync(ulong id, RequestOptions options = null) - => ChannelHelper.GetMessageAsync(this, Discord, id, null, options); + => ChannelHelper.GetMessageAsync(this, Discord, id, options); public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) - => ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit, null, options); + => ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit, options); public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) - => ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit, null, options); + => ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit, options); public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) - => ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit, null, options); + => ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit, options); public Task> GetPinnedMessagesAsync(RequestOptions options = null) - => ChannelHelper.GetPinnedMessagesAsync(this, Discord, null, options); + => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); - public Task SendMessageAsync(string text, bool isTTS = false, RequestOptions options = null) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, null, options); + public Task SendMessageAsync(string text, bool isTTS = false, EmbedBuilder embed = null, RequestOptions options = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); public Task SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, null, options); + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, null, options); + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); @@ -103,8 +103,8 @@ namespace Discord.Rpc => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) => await SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false); - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, RequestOptions options) - => await SendMessageAsync(text, isTTS, options).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, EmbedBuilder embed, RequestOptions options) + => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); IDisposable IMessageChannel.EnterTypingState(RequestOptions options) => EnterTypingState(options); diff --git a/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs b/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs index e6e3d3e48..a5779bdbb 100644 --- a/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs +++ b/src/Discord.Net.Rpc/Entities/Channels/RpcTextChannel.cs @@ -39,22 +39,22 @@ namespace Discord.Rpc //TODO: Use RPC cache public Task GetMessageAsync(ulong id, RequestOptions options = null) - => ChannelHelper.GetMessageAsync(this, Discord, id, null, options); + => ChannelHelper.GetMessageAsync(this, Discord, id, options); public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) - => ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit, null, options); + => ChannelHelper.GetMessagesAsync(this, Discord, null, Direction.Before, limit, options); public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) - => ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit, null, options); + => ChannelHelper.GetMessagesAsync(this, Discord, fromMessageId, dir, limit, options); public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) - => ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit, null, options); + => ChannelHelper.GetMessagesAsync(this, Discord, fromMessage.Id, dir, limit, options); public Task> GetPinnedMessagesAsync(RequestOptions options = null) - => ChannelHelper.GetPinnedMessagesAsync(this, Discord, null, options); + => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); - public Task SendMessageAsync(string text, bool isTTS = false, RequestOptions options = null) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, null, options); + public Task SendMessageAsync(string text, bool isTTS = false, EmbedBuilder embed = null, RequestOptions options = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); public Task SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, null, options); + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, null, options); + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); @@ -105,8 +105,8 @@ namespace Discord.Rpc => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) => await SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false); - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, RequestOptions options) - => await SendMessageAsync(text, isTTS, options).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, EmbedBuilder embed, RequestOptions options) + => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); IDisposable IMessageChannel.EnterTypingState(RequestOptions options) => EnterTypingState(options); } diff --git a/src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs b/src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs index 476a1ed25..c03dd7ab7 100644 --- a/src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs +++ b/src/Discord.Net.Rpc/Entities/Messages/RpcUserMessage.cs @@ -123,9 +123,12 @@ namespace Discord.Rpc public Task UnpinAsync(RequestOptions options) => MessageHelper.UnpinAsync(this, Discord, options); + public string Resolve(int startIndex, TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name, + TagHandling roleHandling = TagHandling.Name, TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name) + => MentionUtils.Resolve(this, startIndex, userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling); public string Resolve(TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name, TagHandling roleHandling = TagHandling.Name, TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name) - => MentionUtils.Resolve(this, userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling); + => MentionUtils.Resolve(this, 0, userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling); private string DebuggerDisplay => $"{Author}: {Content} ({Id}{(Attachments.Count > 0 ? $", {Attachments.Count} Attachments" : "")})"; } diff --git a/src/Discord.Net.Rpc/project.json b/src/Discord.Net.Rpc/project.json index dc1944f5b..8b2db7421 100644 --- a/src/Discord.Net.Rpc/project.json +++ b/src/Discord.Net.Rpc/project.json @@ -1,5 +1,5 @@ { - "version": "1.0.0-beta2-*", + "version": "1.0.0-*", "description": "A core Discord.Net library containing the RPC client and models.", "authors": [ "RogueException" ], @@ -32,8 +32,8 @@ "Discord.Net.Rest": { "target": "project" }, - "System.IO.Compression": "4.1.0", - "System.Net.WebSockets.Client": "4.0.0" + "System.IO.Compression": "4.3.0", + "System.Net.WebSockets.Client": "4.3.0" }, "frameworks": { @@ -45,4 +45,4 @@ ] } } -} +} \ No newline at end of file diff --git a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj new file mode 100644 index 000000000..dd6541412 --- /dev/null +++ b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.csproj @@ -0,0 +1,58 @@ + + + + A core Discord.Net library containing the WebSocket client and models. + 1.0.0-beta2 + netstandard1.3 + true + Discord.Net.WebSocket + discord;discordapp + https://github.com/RogueException/Discord.Net + http://opensource.org/licenses/MIT + git + git://github.com/RogueException/Discord.Net + $(PackageTargetFallback);dotnet5.4;dnxcore50;portable-net45+win8 + + + + + + + + + + + + + + 1.0.0-alpha-20161104-2 + All + + + 4.3.0 + + + 4.3.0 + + + 4.3.0 + + + 4.3.0 + + + 4.3.0 + + + + + False + + + $(DefineConstants);RELEASE + $(NoWarn);CS1573;CS1591 + true + true + + + \ No newline at end of file diff --git a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.xproj b/src/Discord.Net.WebSocket/Discord.Net.WebSocket.xproj deleted file mode 100644 index 45e13b5ce..000000000 --- a/src/Discord.Net.WebSocket/Discord.Net.WebSocket.xproj +++ /dev/null @@ -1,19 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - 22ab6c66-536c-4ac2-bbdb-a8bc4eb6b14d - Discord.WebSocket - .\obj - .\bin\ - v4.6.1 - - - 2.0 - - - \ No newline at end of file diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index ebe7c6835..fc3094871 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -38,6 +38,7 @@ namespace Discord.WebSocket private int _nextAudioId; private bool _canReconnect; private DateTimeOffset? _statusSince; + private RestApplication _applicationInfo; /// Gets the shard of of this client. public int ShardId { get; } @@ -123,6 +124,7 @@ namespace Discord.WebSocket if (ConnectionState != ConnectionState.Disconnected) await DisconnectInternalAsync(null, false).ConfigureAwait(false); + _applicationInfo = null; _voiceRegions = ImmutableDictionary.Create(); } @@ -333,8 +335,10 @@ namespace Discord.WebSocket } /// - public Task GetApplicationInfoAsync() - => ClientHelper.GetApplicationInfoAsync(this); + public async Task GetApplicationInfoAsync() + { + return _applicationInfo ?? (_applicationInfo = await ClientHelper.GetApplicationInfoAsync(this)); + } /// public SocketGuild GetGuild(ulong id) diff --git a/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs index 7ba08544b..f1d4221a2 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/ISocketMessageChannel.cs @@ -11,7 +11,7 @@ namespace Discord.WebSocket IReadOnlyCollection CachedMessages { get; } /// Sends a message to this message channel. - new Task SendMessageAsync(string text, bool isTTS = false, RequestOptions options = null); + new Task SendMessageAsync(string text, bool isTTS = false, EmbedBuilder embed = null, RequestOptions options = null); /// Sends a file to this text channel, with an optional caption. new Task SendFileAsync(string filePath, string text = null, bool isTTS = false, RequestOptions options = null); /// Sends a file to this text channel, with an optional caption. diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs index 8b61e89e3..1bc0fc9b5 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketChannelHelper.cs @@ -8,8 +8,8 @@ namespace Discord.WebSocket { internal static class SocketChannelHelper { - public static IAsyncEnumerable> GetMessagesAsync(SocketChannel channel, DiscordSocketClient discord, MessageCache messages, - ulong? fromMessageId, Direction dir, int limit, CacheMode mode, IGuild guild, RequestOptions options) + public static IAsyncEnumerable> GetMessagesAsync(ISocketMessageChannel channel, DiscordSocketClient discord, MessageCache messages, + ulong? fromMessageId, Direction dir, int limit, CacheMode mode, RequestOptions options) { if (dir == Direction.Around) throw new NotImplementedException(); //TODO: Impl @@ -37,7 +37,7 @@ namespace Discord.WebSocket //Download remaining messages ulong? minId = cachedMessages.Count > 0 ? cachedMessages.Min(x => x.Id) : fromMessageId; - var downloadedMessages = ChannelHelper.GetMessagesAsync(channel, discord, minId, dir, limit, guild, options); + var downloadedMessages = ChannelHelper.GetMessagesAsync(channel, discord, minId, dir, limit, options); return result.Concat(downloadedMessages); } else @@ -46,7 +46,7 @@ namespace Discord.WebSocket return result; //Dont use cache in this case - return ChannelHelper.GetMessagesAsync(channel, discord, fromMessageId, dir, limit, guild, options); + return ChannelHelper.GetMessagesAsync(channel, discord, fromMessageId, dir, limit, options); } } public static IReadOnlyCollection GetCachedMessages(SocketChannel channel, DiscordSocketClient discord, MessageCache messages, diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs index 4da4139d0..48f87764b 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketDMChannel.cs @@ -48,15 +48,15 @@ namespace Discord.WebSocket { IMessage msg = _messages?.Get(id); if (msg == null) - msg = await ChannelHelper.GetMessageAsync(this, Discord, id, null, options).ConfigureAwait(false); + msg = await ChannelHelper.GetMessageAsync(this, Discord, id, options).ConfigureAwait(false); return msg; } public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) - => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit, CacheMode.AllowDownload, null, options); + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit, CacheMode.AllowDownload, options); public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) - => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, CacheMode.AllowDownload, null, options); + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, CacheMode.AllowDownload, options); public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) - => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, CacheMode.AllowDownload, null, options); + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, CacheMode.AllowDownload, options); public IReadOnlyCollection GetCachedMessages(int limit = DiscordConfig.MaxMessagesPerBatch) => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, null, Direction.Before, limit); public IReadOnlyCollection GetCachedMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) @@ -64,14 +64,14 @@ namespace Discord.WebSocket public IReadOnlyCollection GetCachedMessages(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, fromMessage.Id, dir, limit); public Task> GetPinnedMessagesAsync(RequestOptions options = null) - => ChannelHelper.GetPinnedMessagesAsync(this, Discord, null, options); + => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); - public Task SendMessageAsync(string text, bool isTTS = false, RequestOptions options = null) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, null, options); + public Task SendMessageAsync(string text, bool isTTS = false, EmbedBuilder embed = null, RequestOptions options = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); public Task SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, null, options); + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, null, options); + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); @@ -123,19 +123,19 @@ namespace Discord.WebSocket return GetCachedMessage(id); } IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode, RequestOptions options) - => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit, mode, null, options); + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit, mode, options); IAsyncEnumerable> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit, CacheMode mode, RequestOptions options) - => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, mode, null, options); + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, mode, options); IAsyncEnumerable> IMessageChannel.GetMessagesAsync(IMessage fromMessage, Direction dir, int limit, CacheMode mode, RequestOptions options) - => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, mode, null, options); + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, mode, options); async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) => await SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false); - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, RequestOptions options) - => await SendMessageAsync(text, isTTS, options).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, EmbedBuilder embed, RequestOptions options) + => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); IDisposable IMessageChannel.EnterTypingState(RequestOptions options) => EnterTypingState(options); diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs index 3b47a8452..93407e22e 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketGroupChannel.cs @@ -71,15 +71,15 @@ namespace Discord.WebSocket { IMessage msg = _messages?.Get(id); if (msg == null) - msg = await ChannelHelper.GetMessageAsync(this, Discord, id, null, options).ConfigureAwait(false); + msg = await ChannelHelper.GetMessageAsync(this, Discord, id, options).ConfigureAwait(false); return msg; } public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) - => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit, CacheMode.AllowDownload, null, options); + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit, CacheMode.AllowDownload, options); public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) - => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, CacheMode.AllowDownload, null, options); + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, CacheMode.AllowDownload, options); public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) - => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, CacheMode.AllowDownload, null, options); + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, CacheMode.AllowDownload, options); public IReadOnlyCollection GetCachedMessages(int limit = DiscordConfig.MaxMessagesPerBatch) => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, null, Direction.Before, limit); public IReadOnlyCollection GetCachedMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) @@ -87,14 +87,14 @@ namespace Discord.WebSocket public IReadOnlyCollection GetCachedMessages(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, fromMessage.Id, dir, limit); public Task> GetPinnedMessagesAsync(RequestOptions options = null) - => ChannelHelper.GetPinnedMessagesAsync(this, Discord, null, options); + => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); - public Task SendMessageAsync(string text, bool isTTS = false, RequestOptions options = null) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, null, options); + public Task SendMessageAsync(string text, bool isTTS = false, EmbedBuilder embed = null, RequestOptions options = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); public Task SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, null, options); + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, null, options); + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); @@ -186,19 +186,19 @@ namespace Discord.WebSocket return GetCachedMessage(id); } IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode, RequestOptions options) - => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit, mode, null, options); + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit, mode, options); IAsyncEnumerable> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit, CacheMode mode, RequestOptions options) - => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, mode, null, options); + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, mode, options); IAsyncEnumerable> IMessageChannel.GetMessagesAsync(IMessage fromMessage, Direction dir, int limit, CacheMode mode, RequestOptions options) - => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, mode, null, options); + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, mode, options); async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) => await SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false); - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, RequestOptions options) - => await SendMessageAsync(text, isTTS, options).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, EmbedBuilder embed, RequestOptions options) + => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); IDisposable IMessageChannel.EnterTypingState(RequestOptions options) => EnterTypingState(options); diff --git a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs index 8dff22232..9b687c9fb 100644 --- a/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs +++ b/src/Discord.Net.WebSocket/Entities/Channels/SocketTextChannel.cs @@ -54,15 +54,15 @@ namespace Discord.WebSocket { IMessage msg = _messages?.Get(id); if (msg == null) - msg = await ChannelHelper.GetMessageAsync(this, Discord, id, Guild, options).ConfigureAwait(false); + msg = await ChannelHelper.GetMessageAsync(this, Discord, id, options).ConfigureAwait(false); return msg; } public IAsyncEnumerable> GetMessagesAsync(int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) - => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit, CacheMode.AllowDownload, Guild, options); + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit, CacheMode.AllowDownload, options); public IAsyncEnumerable> GetMessagesAsync(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) - => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, CacheMode.AllowDownload, Guild, options); + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, CacheMode.AllowDownload, options); public IAsyncEnumerable> GetMessagesAsync(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch, RequestOptions options = null) - => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, CacheMode.AllowDownload, Guild, options); + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, CacheMode.AllowDownload, options); public IReadOnlyCollection GetCachedMessages(int limit = DiscordConfig.MaxMessagesPerBatch) => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, null, Direction.Before, limit); public IReadOnlyCollection GetCachedMessages(ulong fromMessageId, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) @@ -70,14 +70,14 @@ namespace Discord.WebSocket public IReadOnlyCollection GetCachedMessages(IMessage fromMessage, Direction dir, int limit = DiscordConfig.MaxMessagesPerBatch) => SocketChannelHelper.GetCachedMessages(this, Discord, _messages, fromMessage.Id, dir, limit); public Task> GetPinnedMessagesAsync(RequestOptions options = null) - => ChannelHelper.GetPinnedMessagesAsync(this, Discord, Guild, options); + => ChannelHelper.GetPinnedMessagesAsync(this, Discord, options); - public Task SendMessageAsync(string text, bool isTTS = false, RequestOptions options = null) - => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, Guild, options); + public Task SendMessageAsync(string text, bool isTTS = false, EmbedBuilder embed = null, RequestOptions options = null) + => ChannelHelper.SendMessageAsync(this, Discord, text, isTTS, embed, options); public Task SendFileAsync(string filePath, string text, bool isTTS = false, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, Guild, options); + => ChannelHelper.SendFileAsync(this, Discord, filePath, text, isTTS, options); public Task SendFileAsync(Stream stream, string filename, string text, bool isTTS = false, RequestOptions options = null) - => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, Guild, options); + => ChannelHelper.SendFileAsync(this, Discord, stream, filename, text, isTTS, options); public Task DeleteMessagesAsync(IEnumerable messages, RequestOptions options = null) => ChannelHelper.DeleteMessagesAsync(this, Discord, messages, options); @@ -124,19 +124,19 @@ namespace Discord.WebSocket return GetCachedMessage(id); } IAsyncEnumerable> IMessageChannel.GetMessagesAsync(int limit, CacheMode mode, RequestOptions options) - => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit, mode, Guild, options); + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, null, Direction.Before, limit, mode, options); IAsyncEnumerable> IMessageChannel.GetMessagesAsync(ulong fromMessageId, Direction dir, int limit, CacheMode mode, RequestOptions options) - => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, mode, Guild, options); + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessageId, dir, limit, mode, options); IAsyncEnumerable> IMessageChannel.GetMessagesAsync(IMessage fromMessage, Direction dir, int limit, CacheMode mode, RequestOptions options) - => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, mode, Guild, options); + => SocketChannelHelper.GetMessagesAsync(this, Discord, _messages, fromMessage.Id, dir, limit, mode, options); async Task> IMessageChannel.GetPinnedMessagesAsync(RequestOptions options) => await GetPinnedMessagesAsync(options).ConfigureAwait(false); async Task IMessageChannel.SendFileAsync(string filePath, string text, bool isTTS, RequestOptions options) => await SendFileAsync(filePath, text, isTTS, options).ConfigureAwait(false); async Task IMessageChannel.SendFileAsync(Stream stream, string filename, string text, bool isTTS, RequestOptions options) => await SendFileAsync(stream, filename, text, isTTS, options).ConfigureAwait(false); - async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, RequestOptions options) - => await SendMessageAsync(text, isTTS, options).ConfigureAwait(false); + async Task IMessageChannel.SendMessageAsync(string text, bool isTTS, EmbedBuilder embed, RequestOptions options) + => await SendMessageAsync(text, isTTS, embed, options).ConfigureAwait(false); IDisposable IMessageChannel.EnterTypingState(RequestOptions options) => EnterTypingState(options); } diff --git a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs index cd65f4513..e8a1fa079 100644 --- a/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs +++ b/src/Discord.Net.WebSocket/Entities/Messages/SocketUserMessage.cs @@ -151,9 +151,12 @@ namespace Discord.WebSocket public Task UnpinAsync(RequestOptions options = null) => MessageHelper.UnpinAsync(this, Discord, options); + public string Resolve(int startIndex, TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name, + TagHandling roleHandling = TagHandling.Name, TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name) + => MentionUtils.Resolve(this, startIndex, userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling); public string Resolve(TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name, TagHandling roleHandling = TagHandling.Name, TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name) - => MentionUtils.Resolve(this, userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling); + => MentionUtils.Resolve(this, 0, userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling); private string DebuggerDisplay => $"{Author}: {Content} ({Id}{(Attachments.Count > 0 ? $", {Attachments.Count} Attachments" : "")})"; internal new SocketUserMessage Clone() => MemberwiseClone() as SocketUserMessage; diff --git a/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs b/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs index 515389da1..5fa585138 100644 --- a/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs +++ b/src/Discord.Net.WebSocket/Entities/Roles/SocketRole.cs @@ -57,5 +57,6 @@ namespace Discord.WebSocket //IRole IGuild IRole.Guild => Guild; + public int CompareTo(IRole role) => this.CompareTo(role); } } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs index 5a670c14d..3ef45d230 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketGuildUser.cs @@ -25,19 +25,39 @@ namespace Discord.WebSocket public override ushort DiscriminatorValue { get { return GlobalUser.DiscriminatorValue; } internal set { GlobalUser.DiscriminatorValue = value; } } public override string AvatarId { get { return GlobalUser.AvatarId; } internal set { GlobalUser.AvatarId = value; } } public GuildPermissions GuildPermissions => new GuildPermissions(Permissions.ResolveGuild(Guild, this)); - public IReadOnlyCollection RoleIds => _roleIds; internal override SocketPresence Presence { get { return GlobalUser.Presence; } set { GlobalUser.Presence = value; } } - public SocketVoiceState? VoiceState => Guild.GetVoiceState(Id); public bool IsSelfDeafened => VoiceState?.IsSelfDeafened ?? false; public bool IsSelfMuted => VoiceState?.IsSelfMuted ?? false; public bool IsSuppressed => VoiceState?.IsSuppressed ?? false; - public SocketVoiceChannel VoiceChannel => VoiceState?.VoiceChannel; public bool IsDeafened => VoiceState?.IsDeafened ?? false; public bool IsMuted => VoiceState?.IsMuted ?? false; + public DateTimeOffset? JoinedAt => DateTimeUtils.FromTicks(_joinedAtTicks); + public IReadOnlyCollection RoleIds => _roleIds; + public SocketVoiceChannel VoiceChannel => VoiceState?.VoiceChannel; public string VoiceSessionId => VoiceState?.VoiceSessionId ?? ""; + public SocketVoiceState? VoiceState => Guild.GetVoiceState(Id); - public DateTimeOffset? JoinedAt => DateTimeUtils.FromTicks(_joinedAtTicks); + /// The position of the user within the role hirearchy. + /// The returned value equal to the position of the highest role the user has, + /// or int.MaxValue if user is the server owner. + public int Hierarchy + { + get + { + if (Guild.OwnerId == Id) + return int.MaxValue; + + int maxPos = 0; + for (int i = 0; i < _roleIds.Length; i++) + { + var role = Guild.GetRole(_roleIds[i]); + if (role != null && role.Position > maxPos) + maxPos = role.Position; + } + return maxPos; + } + } internal SocketGuildUser(SocketGuild guild, SocketGlobalUser globalUser) : base(guild.Discord, globalUser.Id) diff --git a/src/Discord.Net.WebSocket/project.json b/src/Discord.Net.WebSocket/project.json index 7bc15a34d..46eb0eccd 100644 --- a/src/Discord.Net.WebSocket/project.json +++ b/src/Discord.Net.WebSocket/project.json @@ -1,5 +1,5 @@ { - "version": "1.0.0-beta2-*", + "version": "1.0.0-*", "description": "A core Discord.Net library containing the WebSocket client and models.", "authors": [ "RogueException" ], @@ -36,11 +36,11 @@ "Discord.Net.Rest": { "target": "project" }, - "System.IO.Compression": "4.1.0", - "System.Net.NameResolution": "4.0.0", - "System.Net.Sockets": "4.1.0", - "System.Net.WebSockets.Client": "4.0.0", - "System.Runtime.InteropServices": "4.1.0" + "System.IO.Compression": "4.3.0", + "System.Net.NameResolution": "4.3.0", + "System.Net.Sockets": "4.3.0", + "System.Net.WebSockets.Client": "4.3.0", + "System.Runtime.InteropServices": "4.3.0" }, "frameworks": { @@ -52,4 +52,4 @@ ] } } -} +} \ No newline at end of file diff --git a/src/Discord.Net/Discord.Net.csproj b/src/Discord.Net/Discord.Net.csproj new file mode 100644 index 000000000..ae9eb3e2a --- /dev/null +++ b/src/Discord.Net/Discord.Net.csproj @@ -0,0 +1,42 @@ + + + + An aynchronous API wrapper for Discord. This metapackage includes all of the optional Discord.Net components. + 1.0.0-beta2 + netstandard1.3 + Discord.Net + discord;discordapp + https://github.com/RogueException/Discord.Net + http://opensource.org/licenses/MIT + git + git://github.com/RogueException/Discord.Net + $(PackageTargetFallback);dotnet5.4;dnxcore50;portable-net45+win8 + + + + + + + + + + + + + + + + + 1.0.0-alpha-20161104-2 + All + + + + + False + + + $(DefineConstants);RELEASE + + + \ No newline at end of file diff --git a/src/Discord.Net/Discord.Net.xproj b/src/Discord.Net/Discord.Net.xproj deleted file mode 100644 index 079338b62..000000000 --- a/src/Discord.Net/Discord.Net.xproj +++ /dev/null @@ -1,19 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - 496db20a-a455-4d01-b6bc-90fe6d7c6b81 - Discord.Net - .\obj - .\bin\ - v4.6.1 - - - 2.0 - - - \ No newline at end of file diff --git a/src/Discord.Net/project.json b/src/Discord.Net/project.json index 4286df5ea..98b3b6211 100644 --- a/src/Discord.Net/project.json +++ b/src/Discord.Net/project.json @@ -1,5 +1,5 @@ -{ - "version": "1.0.0-beta2-*", +{ + "version": "1.0.0-*", "description": "An aynchronous API wrapper for Discord. This metapackage includes all of the optional Discord.Net components.", "authors": [ "RogueException" ],