Browse Source

Merge branch 'dev' of https://github.com/RogueException/Discord.Net into feature/better-relations

pull/369/head
ObsidianMinor 8 years ago
parent
commit
17302083ab
100 changed files with 1945 additions and 717 deletions
  1. +2
    -0
      .gitignore
  2. +87
    -29
      Discord.Net.sln
  3. +19
    -8
      README.md
  4. +0
    -15
      build.bat
  5. +4
    -0
      build.ps1
  6. +1
    -1
      docs/CONTRIBUTING.md
  7. +1
    -1
      docs/api/.manifest
  8. +195
    -108
      docs/guides/commands.md
  9. +1
    -1
      docs/guides/events.md
  10. +13
    -3
      docs/guides/intro.md
  11. +11
    -0
      docs/guides/samples/audio_create_ffmpeg.cs
  12. +9
    -0
      docs/guides/samples/audio_ffmpeg.cs
  13. +13
    -13
      docs/guides/samples/command_handler.cs
  14. +9
    -14
      docs/guides/samples/dependency_map_setup.cs
  15. +26
    -15
      docs/guides/samples/dependency_module.cs
  16. +6
    -0
      docs/guides/samples/empty-module.cs
  17. +1
    -1
      docs/guides/samples/faq/send_message.cs
  18. +2
    -2
      docs/guides/samples/faq/status.cs
  19. +116
    -23
      docs/guides/samples/first-steps.cs
  20. +12
    -9
      docs/guides/samples/groups.cs
  21. +3
    -8
      docs/guides/samples/joining_audio.cs
  22. +14
    -6
      docs/guides/samples/logging.cs
  23. +2
    -2
      docs/guides/samples/module.cs
  24. +0
    -10
      docs/guides/samples/require_context.cs
  25. +3
    -3
      docs/guides/samples/require_owner.cs
  26. +0
    -6
      docs/guides/samples/require_permission.cs
  27. +1
    -0
      docs/guides/samples/typereader.cs
  28. +90
    -7
      docs/guides/voice.md
  29. +9
    -2
      docs/index.md
  30. +17
    -0
      pack.ps1
  31. +3
    -0
      src/Discord.Net.Analyzers/AssemblyInfo.cs
  32. +55
    -0
      src/Discord.Net.Analyzers/ConfigureAwaitAnalyzer.cs
  33. +32
    -0
      src/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj
  34. +1
    -1
      src/Discord.Net.Commands/Attributes/AliasAttribute.cs
  35. +1
    -1
      src/Discord.Net.Commands/Attributes/CommandAttribute.cs
  36. +9
    -0
      src/Discord.Net.Commands/Attributes/DontInjectAttribute.cs
  37. +22
    -0
      src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs
  38. +11
    -0
      src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs
  39. +1
    -1
      src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs
  40. +73
    -0
      src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs
  41. +17
    -1
      src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs
  42. +20
    -0
      src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs
  43. +26
    -4
      src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs
  44. +15
    -3
      src/Discord.Net.Commands/Builders/CommandBuilder.cs
  45. +14
    -5
      src/Discord.Net.Commands/Builders/ModuleBuilder.cs
  46. +58
    -27
      src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs
  47. +25
    -3
      src/Discord.Net.Commands/Builders/ParameterBuilder.cs
  48. +2
    -10
      src/Discord.Net.Commands/CommandContext.cs
  49. +26
    -0
      src/Discord.Net.Commands/CommandMatch.cs
  50. +1
    -1
      src/Discord.Net.Commands/CommandParser.cs
  51. +81
    -62
      src/Discord.Net.Commands/CommandService.cs
  52. +12
    -0
      src/Discord.Net.Commands/CommandServiceConfig.cs
  53. +46
    -5
      src/Discord.Net.Commands/Dependencies/DependencyMap.cs
  54. +75
    -1
      src/Discord.Net.Commands/Dependencies/IDependencyMap.cs
  55. +9
    -24
      src/Discord.Net.Commands/Discord.Net.Commands.csproj
  56. +11
    -0
      src/Discord.Net.Commands/IModuleBase.cs
  57. +31
    -24
      src/Discord.Net.Commands/Info/CommandInfo.cs
  58. +28
    -28
      src/Discord.Net.Commands/Info/ModuleInfo.cs
  59. +35
    -10
      src/Discord.Net.Commands/Info/ParameterInfo.cs
  60. +9
    -15
      src/Discord.Net.Commands/Map/CommandMap.cs
  61. +48
    -28
      src/Discord.Net.Commands/Map/CommandMapNode.cs
  62. +29
    -4
      src/Discord.Net.Commands/ModuleBase.cs
  63. +8
    -5
      src/Discord.Net.Commands/PrimitiveParsers.cs
  64. +1
    -1
      src/Discord.Net.Commands/Readers/ChannelTypeReader.cs
  65. +1
    -1
      src/Discord.Net.Commands/Readers/EnumTypeReader.cs
  66. +1
    -1
      src/Discord.Net.Commands/Readers/MessageTypeReader.cs
  67. +32
    -0
      src/Discord.Net.Commands/Readers/PrimitiveTypeReader.cs
  68. +1
    -1
      src/Discord.Net.Commands/Readers/RoleTypeReader.cs
  69. +0
    -22
      src/Discord.Net.Commands/Readers/SimpleTypeReader.cs
  70. +1
    -1
      src/Discord.Net.Commands/Readers/TypeReader.cs
  71. +7
    -7
      src/Discord.Net.Commands/Readers/UserTypeReader.cs
  72. +2
    -2
      src/Discord.Net.Commands/Results/ParseResult.cs
  73. +3
    -3
      src/Discord.Net.Commands/Results/SearchResult.cs
  74. +1
    -0
      src/Discord.Net.Commands/RunMode.cs
  75. +27
    -12
      src/Discord.Net.Commands/Utilities/ReflectionUtils.cs
  76. +0
    -43
      src/Discord.Net.Commands/project.json
  77. +1
    -0
      src/Discord.Net.Core/AssemblyInfo.cs
  78. +9
    -0
      src/Discord.Net.Core/Audio/AudioApplication.cs
  79. +41
    -0
      src/Discord.Net.Core/Audio/AudioInStream.cs
  80. +41
    -0
      src/Discord.Net.Core/Audio/AudioOutStream.cs
  81. +28
    -5
      src/Discord.Net.Core/Audio/IAudioClient.cs
  82. +16
    -0
      src/Discord.Net.Core/Audio/RTPFrame.cs
  83. +3
    -3
      src/Discord.Net.Core/CDN.cs
  84. +11
    -0
      src/Discord.Net.Core/Commands/ICommandContext.cs
  85. +20
    -44
      src/Discord.Net.Core/Discord.Net.Core.csproj
  86. +6
    -0
      src/Discord.Net.Core/DiscordConfig.cs
  87. +20
    -0
      src/Discord.Net.Core/Entities/Channels/BulkGuildChannelProperties.cs
  88. +0
    -0
      src/Discord.Net.Core/Entities/Channels/Direction.cs
  89. +30
    -0
      src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs
  90. +1
    -1
      src/Discord.Net.Core/Entities/Channels/IAudioChannel.cs
  91. +3
    -4
      src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs
  92. +3
    -1
      src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs
  93. +2
    -3
      src/Discord.Net.Core/Entities/Channels/ITextChannel.cs
  94. +4
    -5
      src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs
  95. +11
    -0
      src/Discord.Net.Core/Entities/Channels/TextChannelProperties.cs
  96. +15
    -0
      src/Discord.Net.Core/Entities/Channels/VoiceChannelProperties.cs
  97. +21
    -0
      src/Discord.Net.Core/Entities/Guilds/GuildEmbedProperties.cs
  98. +1
    -7
      src/Discord.Net.Core/Entities/Guilds/GuildEmoji.cs
  99. +9
    -0
      src/Discord.Net.Core/Entities/Guilds/GuildIntegrationProperties.cs
  100. +71
    -0
      src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs

+ 2
- 0
.gitignore View File

@@ -198,6 +198,8 @@ FakesAssemblies/
#Custom #Custom
project.lock.json project.lock.json
/test/Discord.Net.Tests/config.json /test/Discord.Net.Tests/config.json
/test/Discord.Net.Tests/cache.db*
/docs/_build /docs/_build
*.pyc *.pyc
/.editorconfig /.editorconfig
.vscode/

+ 87
- 29
Discord.Net.sln View File

@@ -1,16 +1,8 @@


Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15 # Visual Studio 15
VisualStudioVersion = 15.0.25914.0
VisualStudioVersion = 15.0.26014.0
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F7F3E124-93C7-4846-AE87-9CE12BD82859}"
ProjectSection(SolutionItems) = preProject
global.json = global.json
README.md = README.md
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net", "src\Discord.Net\Discord.Net.csproj", "{496DB20A-A455-4D01-B6BC-90FE6D7C6B81}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Core", "src\Discord.Net.Core\Discord.Net.Core.csproj", "{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 EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Impls", "Impls", "{288C363D-A636-4EAE-9AC1-4698B641B26E}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Impls", "Impls", "{288C363D-A636-4EAE-9AC1-4698B641B26E}"
@@ -23,6 +15,20 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Commands", "src
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.WebSocket", "src\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj", "{688FD1D8-7F01-4539-B2E9-F473C5D699C7}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.WebSocket", "src\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj", "{688FD1D8-7F01-4539-B2E9-F473C5D699C7}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Providers", "Providers", "{B0657AAE-DCC5-4FBF-8E5D-1FB578CF3012}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Providers.WS4Net", "src\Discord.Net.Providers.WS4Net\Discord.Net.Providers.WS4Net.csproj", "{6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Providers.UdpClient", "src\Discord.Net.Providers.UdpClient\Discord.Net.Providers.UdpClient.csproj", "{ABC9F4B9-2452-4725-B522-754E0A02E282}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions", "{CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Tests", "test\Discord.Net.Tests\Discord.Net.Tests.csproj", "{C38E5BC1-11CB-4101-8A38-5B40A1BC6433}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{F66D75C0-E304-46E0-9C3A-294F340DB37D}"
EndProject
Project("{13B669BE-BB05-4DDF-9536-439F39A36129}") = "Discord.Net.Relay", "src\Discord.Net.Relay\Discord.Net.Relay.csproj", "{2705FCB3-68C9-4CEB-89CC-01F8EC80512B}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -33,26 +39,14 @@ Global
Release|x86 = Release|x86 Release|x86 = Release|x86
EndGlobalSection EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution 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}.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.ActiveCfg = Debug|Any CPU
{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|Any CPU.Build.0 = Debug|Any CPU {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|x64.ActiveCfg = Debug|x64 {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|x64.Build.0 = Debug|x64
{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|x86.ActiveCfg = Debug|x86 {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}.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|Any CPU.ActiveCfg = Release|Any CPU
{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|Any CPU.Build.0 = Release|Any CPU
{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|x64.ActiveCfg = Release|x64 {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|x64.Build.0 = Release|x64
{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|x86.ActiveCfg = Release|x86 {91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|x86.ActiveCfg = Release|x86
@@ -63,8 +57,8 @@ Global
{BFC6DC28-0351-4573-926A-D4124244C04F}.Debug|x64.Build.0 = 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.ActiveCfg = Debug|Any CPU
{BFC6DC28-0351-4573-926A-D4124244C04F}.Debug|x86.Build.0 = 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|Any CPU.ActiveCfg = Release|Any CPU
{BFC6DC28-0351-4573-926A-D4124244C04F}.Release|Any CPU.Build.0 = Release|Any CPU
{BFC6DC28-0351-4573-926A-D4124244C04F}.Release|x64.ActiveCfg = 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|x64.Build.0 = Debug|Any CPU
{BFC6DC28-0351-4573-926A-D4124244C04F}.Release|x86.ActiveCfg = Debug|Any CPU {BFC6DC28-0351-4573-926A-D4124244C04F}.Release|x86.ActiveCfg = Debug|Any CPU
@@ -75,8 +69,8 @@ Global
{5688A353-121E-40A1-8BFA-B17B91FB48FB}.Debug|x64.Build.0 = 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.ActiveCfg = Debug|Any CPU
{5688A353-121E-40A1-8BFA-B17B91FB48FB}.Debug|x86.Build.0 = 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|Any CPU.ActiveCfg = Release|Any CPU
{5688A353-121E-40A1-8BFA-B17B91FB48FB}.Release|Any CPU.Build.0 = Release|Any CPU
{5688A353-121E-40A1-8BFA-B17B91FB48FB}.Release|x64.ActiveCfg = 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|x64.Build.0 = Debug|Any CPU
{5688A353-121E-40A1-8BFA-B17B91FB48FB}.Release|x86.ActiveCfg = Debug|Any CPU {5688A353-121E-40A1-8BFA-B17B91FB48FB}.Release|x86.ActiveCfg = Debug|Any CPU
@@ -87,8 +81,8 @@ Global
{078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Debug|x64.Build.0 = 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.ActiveCfg = Debug|Any CPU
{078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Debug|x86.Build.0 = 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|Any CPU.ActiveCfg = Release|Any CPU
{078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Release|Any CPU.Build.0 = Release|Any CPU
{078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Release|x64.ActiveCfg = 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|x64.Build.0 = Debug|Any CPU
{078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Release|x86.ActiveCfg = Debug|Any CPU {078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Release|x86.ActiveCfg = Debug|Any CPU
@@ -105,6 +99,66 @@ Global
{688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Release|x64.Build.0 = 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.ActiveCfg = Release|x86
{688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Release|x86.Build.0 = Release|x86 {688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Release|x86.Build.0 = Release|x86
{6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Debug|x64.ActiveCfg = Debug|x64
{6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Debug|x64.Build.0 = Debug|x64
{6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Debug|x86.ActiveCfg = Debug|x86
{6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Debug|x86.Build.0 = Debug|x86
{6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Release|Any CPU.Build.0 = Release|Any CPU
{6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Release|x64.ActiveCfg = Release|x64
{6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Release|x64.Build.0 = Release|x64
{6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Release|x86.ActiveCfg = Release|x86
{6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Release|x86.Build.0 = Release|x86
{547261FC-8BA3-40EA-A040-A38ABDAA8D72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{547261FC-8BA3-40EA-A040-A38ABDAA8D72}.Debug|Any CPU.Build.0 = Debug|Any CPU
{547261FC-8BA3-40EA-A040-A38ABDAA8D72}.Debug|x64.ActiveCfg = Debug|x64
{547261FC-8BA3-40EA-A040-A38ABDAA8D72}.Debug|x64.Build.0 = Debug|x64
{547261FC-8BA3-40EA-A040-A38ABDAA8D72}.Debug|x86.ActiveCfg = Debug|x86
{547261FC-8BA3-40EA-A040-A38ABDAA8D72}.Debug|x86.Build.0 = Debug|x86
{547261FC-8BA3-40EA-A040-A38ABDAA8D72}.Release|Any CPU.ActiveCfg = Release|Any CPU
{547261FC-8BA3-40EA-A040-A38ABDAA8D72}.Release|Any CPU.Build.0 = Release|Any CPU
{547261FC-8BA3-40EA-A040-A38ABDAA8D72}.Release|x64.ActiveCfg = Release|x64
{547261FC-8BA3-40EA-A040-A38ABDAA8D72}.Release|x64.Build.0 = Release|x64
{547261FC-8BA3-40EA-A040-A38ABDAA8D72}.Release|x86.ActiveCfg = Release|x86
{547261FC-8BA3-40EA-A040-A38ABDAA8D72}.Release|x86.Build.0 = Release|x86
{ABC9F4B9-2452-4725-B522-754E0A02E282}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ABC9F4B9-2452-4725-B522-754E0A02E282}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ABC9F4B9-2452-4725-B522-754E0A02E282}.Debug|x64.ActiveCfg = Debug|x64
{ABC9F4B9-2452-4725-B522-754E0A02E282}.Debug|x64.Build.0 = Debug|x64
{ABC9F4B9-2452-4725-B522-754E0A02E282}.Debug|x86.ActiveCfg = Debug|x86
{ABC9F4B9-2452-4725-B522-754E0A02E282}.Debug|x86.Build.0 = Debug|x86
{ABC9F4B9-2452-4725-B522-754E0A02E282}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ABC9F4B9-2452-4725-B522-754E0A02E282}.Release|Any CPU.Build.0 = Release|Any CPU
{ABC9F4B9-2452-4725-B522-754E0A02E282}.Release|x64.ActiveCfg = Release|x64
{ABC9F4B9-2452-4725-B522-754E0A02E282}.Release|x64.Build.0 = Release|x64
{ABC9F4B9-2452-4725-B522-754E0A02E282}.Release|x86.ActiveCfg = Release|x86
{ABC9F4B9-2452-4725-B522-754E0A02E282}.Release|x86.Build.0 = Release|x86
{C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Debug|x64.ActiveCfg = Debug|x64
{C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Debug|x64.Build.0 = Debug|x64
{C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Debug|x86.ActiveCfg = Debug|x86
{C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Debug|x86.Build.0 = Debug|x86
{C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|Any CPU.Build.0 = Release|Any CPU
{C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|x64.ActiveCfg = Release|x64
{C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|x64.Build.0 = Release|x64
{C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|x86.ActiveCfg = Release|x86
{C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|x86.Build.0 = Release|x86
{2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Debug|x64.ActiveCfg = Debug|x64
{2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Debug|x64.Build.0 = Debug|x64
{2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Debug|x86.ActiveCfg = Debug|x86
{2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Debug|x86.Build.0 = Debug|x86
{2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|Any CPU.Build.0 = Release|Any CPU
{2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|x64.ActiveCfg = Release|x64
{2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|x64.Build.0 = Release|x64
{2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|x86.ActiveCfg = Release|x86
{2705FCB3-68C9-4CEB-89CC-01F8EC80512B}.Release|x86.Build.0 = Release|x86
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -112,6 +166,10 @@ Global
GlobalSection(NestedProjects) = preSolution GlobalSection(NestedProjects) = preSolution
{BFC6DC28-0351-4573-926A-D4124244C04F} = {288C363D-A636-4EAE-9AC1-4698B641B26E} {BFC6DC28-0351-4573-926A-D4124244C04F} = {288C363D-A636-4EAE-9AC1-4698B641B26E}
{5688A353-121E-40A1-8BFA-B17B91FB48FB} = {288C363D-A636-4EAE-9AC1-4698B641B26E} {5688A353-121E-40A1-8BFA-B17B91FB48FB} = {288C363D-A636-4EAE-9AC1-4698B641B26E}
{078DD7E6-943D-4D09-AFC2-D2BA58B76C9C} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A}
{688FD1D8-7F01-4539-B2E9-F473C5D699C7} = {288C363D-A636-4EAE-9AC1-4698B641B26E} {688FD1D8-7F01-4539-B2E9-F473C5D699C7} = {288C363D-A636-4EAE-9AC1-4698B641B26E}
{6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7} = {B0657AAE-DCC5-4FBF-8E5D-1FB578CF3012}
{ABC9F4B9-2452-4725-B522-754E0A02E282} = {B0657AAE-DCC5-4FBF-8E5D-1FB578CF3012}
{2705FCB3-68C9-4CEB-89CC-01F8EC80512B} = {F66D75C0-E304-46E0-9C3A-294F340DB37D}
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

+ 19
- 8
README.md View File

@@ -1,33 +1,44 @@
# Discord.Net v1.0.0-beta2
# Discord.Net v1.0.0-rc
[![MyGet](https://img.shields.io/myget/discord-net/vpre/Discord.Net.svg)](https://www.myget.org/feed/Packages/discord-net) [![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)
[![Build status](https://ci.appveyor.com/api/projects/status/5sb7n8a09w9clute/branch/dev?svg=true)](https://ci.appveyor.com/project/RogueException/discord-net/branch/dev)
[![Discord](https://discordapp.com/api/guilds/81384788765712384/widget.png)](https://discord.gg/0SBTUU1wZTVjAMPx)


An unofficial .Net API Wrapper for the Discord client (http://discordapp.com).
An unofficial .NET API Wrapper for the Discord client (http://discordapp.com).


Check out the [documentation](https://discord.foxbot.me/docs/) or join the [Discord API Chat](https://discord.gg/0SBTUU1wZTVjAMPx). Check out the [documentation](https://discord.foxbot.me/docs/) or join the [Discord API Chat](https://discord.gg/0SBTUU1wZTVjAMPx).


## Installation ## Installation
### Stable (NuGet) ### Stable (NuGet)
Our stable builds are available from NuGet:
Our stable builds available from NuGet through the Discord.Net metapackage:
- [Discord.Net](https://www.nuget.org/packages/Discord.Net/) - [Discord.Net](https://www.nuget.org/packages/Discord.Net/)

The individual components may also be installed from NuGet:
- [Discord.Net.Rest](https://www.nuget.org/packages/Discord.Net.Rest/)
- [Discord.Net.Rpc](https://www.nuget.org/packages/Discord.Net.Rpc/)
- [Discord.Net.WebSocket](https://www.nuget.org/packages/Discord.Net.WebSocket/)
- [Discord.Net.Commands](https://www.nuget.org/packages/Discord.Net.Commands/) - [Discord.Net.Commands](https://www.nuget.org/packages/Discord.Net.Commands/)


The following providers are available for platforms not supporting .NET Standard 1.3:
- [Discord.Net.Providers.UdpClient](https://www.nuget.org/packages/Discord.Net.Providers.UdpClient/)
- [Discord.Net.Providers.WS4Net](https://www.nuget.org/packages/Discord.Net.Providers.WS4Net/)

### Unstable (MyGet) ### Unstable (MyGet)
Bleeding edge builds are available using our MyGet feed (`https://www.myget.org/F/discord-net/api/v3/index.json`). These builds may break at any time - use with caution.
Nightly builds are available through our MyGet feed (`https://www.myget.org/F/discord-net/api/v3/index.json`).


## Compiling ## Compiling
In order to compile Discord.Net, you require the following: In order to compile Discord.Net, you require the following:


### Using Visual Studio ### Using Visual Studio
- [Visual Studio 2017 RC](https://www.microsoft.com/net/core#windowsvs2017) - [Visual Studio 2017 RC](https://www.microsoft.com/net/core#windowsvs2017)
- [.NET Core SDK 1.0 RC3](https://github.com/dotnet/core/blob/master/release-notes/rc3-download.md)


The .NET Core and Docker (Preview) workload is required during Visual Studio installation. The .NET Core and Docker (Preview) workload is required during Visual Studio installation.


### Using Command Line ### Using Command Line
- [.Net Core 1.1 SDK](https://www.microsoft.com/net/download/core)
- [.NET Core SDK 1.0 RC3](https://github.com/dotnet/core/blob/master/release-notes/rc3-download.md)


## Known Issues ## Known Issues


### WebSockets (Win7 and earlier) ### 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).
.NET Core 1.1 does not support WebSockets on Win7 and earlier. It's recommended to use the Discord.Net.Providers.WS4Net package until this is resolved.
Track the issue [here](https://github.com/dotnet/corefx/issues/9503).

+ 0
- 15
build.bat View File

@@ -1,15 +0,0 @@
@echo Off
dotnet restore
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%"

+ 4
- 0
build.ps1 View File

@@ -0,0 +1,4 @@
appveyor-retry dotnet restore Discord.Net.sln -v Minimal /p:BuildNumber="$Env:BUILD"
if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) }
dotnet build Discord.Net.sln -c "Release" /p:BuildNumber="$Env:BUILD"
if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) }

+ 1
- 1
docs/CONTRIBUTING.md View File

@@ -10,6 +10,6 @@ I don't really have any strict conditions for writing documentation, but just ke


### Compiling ### Compiling


Documentation is compiled into a static site using [DocFx](dotnet.github.io/docfx/). You **must** install a version of DocFx that supports .NET Core. The latest build of that is [2.1.0-cli-alpha](https://github.com/dotnet/docfx/releases/tag/v2.1.0-cli-alpha).
Documentation is compiled into a static site using [DocFx](https://dotnet.github.io/docfx/). We currently use version 2.8


After making changes, compile your changes into the static site with `docfx`. You can also view your changes live with `docfx --serve`. After making changes, compile your changes into the static site with `docfx`. You can also view your changes live with `docfx --serve`.

+ 1
- 1
docs/api/.manifest
File diff suppressed because it is too large
View File


+ 195
- 108
docs/guides/commands.md View File

@@ -3,139 +3,238 @@
[Discord.Commands](xref:Discord.Commands) provides an Attribute-based [Discord.Commands](xref:Discord.Commands) provides an Attribute-based
Command Parser. Command Parser.


### Setup
## Setup


To use Commands, you must create a
[Commands Service](xref:Discord.Commands.CommandService)
and a Command Handler.
To use Commands, you must create a [Commands Service] and a
Command Handler.


Included below is a very bare-bones Command Handler. You can extend
your Command Handler as much as you like, however the below is the
Included below is a very bare-bones Command Handler. You can extend
your Command Handler as much as you like, however the below is the
bare minimum. bare minimum.


[!code-csharp[Barebones Command Handler](samples/command_handler.cs)]
The CommandService optionally will accept a [CommandServiceConfig],
which _does_ set a few default values for you. It is recommended to
look over the properties in [CommandServiceConfig], and their default
values.


## Commands
[!code-csharp[Command Handler](samples/command_handler.cs)]


In 1.0, Commands are no longer implemented at runtime with a builder
pattern. While a builder pattern may be provided later, commands are
created primarily with attributes.
[Command Service]: xref:Discord.Commands.CommandService
[CommandServiceConfig]: xref:Discord.Commands.CommandServiceConfig


### Basic Structure
## With Attributes


All commands belong to a Module. (See the below section for creating
modules).
In 1.0, Commands can be defined ahead of time, with attributes, or
at runtime, with builders.


All commands in a module must be defined as a `Task`.
For most bots, ahead-of-time commands should be all you need, and this
is the recommended method of defining commands.


To add parameters to your command, you simply need to add parameters
to the Task that represents the command. You are _not_ required to
accept all arguments as `String`, they will be automatically parsed
into the type you specify for the arument. See the Example Module
for an example of command parameters.
### Modules


## Modules
The first step to creating commands is to create a _module_.


Modules are an organizational pattern that allow you to write your
commands in different classes, and have them automatically loaded.
Modules are an organizational pattern that allow you to write your
commands in different classes, and have them automatically loaded.


Discord.Net's implementation of Modules is influenced heavily from Discord.Net's implementation of Modules is influenced heavily from
ASP.Net Core's Controller pattern. This means that the lifetime of a ASP.Net Core's Controller pattern. This means that the lifetime of a
module instance is only as long as the command being ran in it.
module instance is only as long as the command being invoked.


**Avoid using long-running code** in your modules whereever possible.
**Avoid using long-running code** in your modules whereever possible.
You should **not** be implementing very much logic into your modules; You should **not** be implementing very much logic into your modules;
outsource to a service for that. outsource to a service for that.


If you are unfamiliar with Inversion of Control, it is recommended to
read the MSDN article on [IoC] and [Dependency Injection].
If you are unfamiliar with Inversion of Control, it is recommended to
read the MSDN article on [IoC] and [Dependency Injection].

To begin, create a new class somewhere in your project, and
inherit the class from [ModuleBase]. This class **must** be `public`.

>[!NOTE]
>[ModuleBase] is an _abstract_ class, meaning that you may extend it
>or override it as you see fit. Your module may inherit from any
>extension of ModuleBase.


To create a module, create a class that inherits from
@Discord.Commands.ModuleBase.
By now, your module should look like this:
[!code-csharp[Empty Module](samples/empty-module.cs)]


[IoC]: https://msdn.microsoft.com/en-us/library/ff921087.aspx [IoC]: https://msdn.microsoft.com/en-us/library/ff921087.aspx
[Dependency Injection]: https://msdn.microsoft.com/en-us/library/ff921152.aspx [Dependency Injection]: https://msdn.microsoft.com/en-us/library/ff921152.aspx
[ModuleBase]: xref:Discord.Commands.ModuleBase`1

### Adding Commands

The next step to creating commands, is actually creating commands.

To create a command, add a method to your module of type `Task`.
Typically, you will want to mark this method as `async`, although it is
not required.

Adding parameters to a command is done by adding parameters to the
parent Task.

For example, to take an integer as an argument, add `int arg`. To take
a user as an argument, add `IUser user`. In 1.0, a command can accept
nearly any type of argument; a full list of types that are parsed by
default can be found in the below section on _Type Readers_.

Parameters, by default, are always required. To make a parameter
optional, give it a default value. To accept a comma-separated list,
set the parameter to `params Type[]`.

Should a parameter include spaces, it **must** be wrapped in quotes.
For example, for a command with a parameter `string food`, you would
execute it with `!favoritefood "Key Lime Pie"`.

If you would like a parameter to parse until the end of a command,
flag the parameter with the [RemainderAttribute]. This will allow a
user to invoke a command without wrapping a parameter in quotes.

Finally, flag your command with the [CommandAttribute]. (You must
specify a name for this command, except for when it is part of a
module group - see below).

[RemainderAttribute]: xref:Discord.Commands.RemainderAttribute
[CommandAttribute]: xref:Discord.Commands.CommandAttribute

### Command Overloads

You may add overloads of your commands, and the command parser will
automatically pick up on it.

If, for whatever reason, you have too commands which are ambiguous to
each other, you may use the @Discord.Commands.PriorityAttribute to
specify which should be tested before the other.

Priority's are sorted in ascending order; the higher priority will be
called first.

### CommandContext

Every command can access the execution context through the [Context]
property on [ModuleBase]. CommandContext allows you to access the
message, channel, guild, and user that the command was invoked from,
as well as the underlying discord client the command was invoked from.

Different types of Contexts may be specified using the generic variant
of [ModuleBase]. When using a [SocketCommandContext], for example,
the properties on this context will already be Socket entities. You
will not need to cast them.

To reply to messages, you may also invoke [ReplyAsync], instead of
accessing the channel through the [Context] and sending a message.

[Context]: xref:Discord.Commands.ModuleBase`1#Discord_Commands_ModuleBase_1_Context
[SocketCommandContext]: xref:Discord.Commands.SocketCommandContext

>![WARNING]
>Contexts should **NOT** be mixed! You cannot have one module that
>uses CommandContext, and another that uses SocketCommandContext.


### Example Module ### Example Module


[!code-csharp[Modules](samples/module.cs)]
At this point, your module should look comparable to this example:
[!code-csharp[Example Module](samples/module.cs)]


#### Loading Modules Automatically #### Loading Modules Automatically


The Command Service can automatically discover all classes in an
Assembly that inherit @Discord.Commands.ModuleBase, and load them.
The Command Service can automatically discover all classes in an
Assembly that inherit [ModuleBase], and load them.


To have a module opt-out of auto-loading, pass `autoload: false` in
the Module attribute.
To opt a module out of auto-loading, flag it with
[DontAutoLoadAttribute]


Invoke [CommandService.AddModules] to discover modules and install them.
Invoke [CommandService.AddModulesAsync] to discover modules and
install them.


[CommandService.AddModules]: xref:Discord.Commands.CommandService#Discord_Commands_CommandService_AddModules
[DontAutoLoadAttribute]: xref:Discord.Commands.DontAutoLoadAttribute
[CommandService.AddModulesAsync]: xref:Discord_Commands_CommandService#Discord_Commands_CommandService_AddModulesAsync_Assembly_


#### Loading Modules Manually #### Loading Modules Manually


To manually load a module, invoke [CommandService.AddModule],
by passing in the generic type of your module, and optionally
To manually load a module, invoke [CommandService.AddModuleAsync],
by passing in the generic type of your module, and optionally
a dependency map. a dependency map.


[CommandService.AddModule]: xref:Discord.Commands.CommandService#Discord_Commands_CommandService_AddModule__1_Discord_Commands_IDependencyMap_
[CommandService.AddModuleAsync]: xref:Discord.Commands.CommandService#Discord_Commands_CommandService_AddModuleAsync__1


### Module Constructors ### Module Constructors


Modules are constructed using Dependency Injection. Any parameters
that are placed in the constructor must be injected into an
@Discord.Commands.IDependencyMap. Alternatively, you may accept an
Modules are constructed using Dependency Injection. Any parameters
that are placed in the constructor must be injected into an
@Discord.Commands.IDependencyMap. Alternatively, you may accept an
IDependencyMap as an argument and extract services yourself. IDependencyMap as an argument and extract services yourself.


### Command Groups
### Module Properties


Command Groups allow you to create a module where commands are prefixed.
To create a group, create a new module and flag it with the
@Discord.Commands.GroupAttribute.
Modules with public settable properties will have them injected after module
construction.


>[!NOTE]
>Groups do not _need_ to be modules. Only classes with commands should
>inherit from ModuleBase. If you plan on using a group for strictly
>organizational purposes, there is no reason to make it a module.
### Module Groups


[!code-csharp[Groups Sample](samples/groups.cs)]
Module Groups allow you to create a module where commands are prefixed.
To create a group, flag a module with the
@Discord.Commands.GroupAttribute

Module groups also allow you to create **nameless commands**, where the
[CommandAttribute] is configured with no name. In this case, the
command will inherit the name of the group it belongs to.

### Submodules

Submodules are modules that reside within another module. Typically,
submodules are used to create nested groups (although not required to
create nested groups).

[!code-csharp[Groups and Submodules](samples/groups.cs)]

## With Builders

**TODO**


## Dependency Injection ## Dependency Injection


The commands service is bundled with a very barebones Dependency
Injection service for your convienence. It is recommended that
The commands service is bundled with a very barebones Dependency
Injection service for your convienence. It is recommended that
you use DI when writing your modules. you use DI when writing your modules.


### Setup ### Setup


First, you need to create an @Discord.Commands.IDependencyMap.
The library includes @Discord.Commands.DependencyMap to help with
this, however you may create your own IDependencyMap if you wish.
First, you need to create an @Discord.Commands.IDependencyMap.
The library includes @Discord.Commands.DependencyMap to help with
this, however you may create your own IDependencyMap if you wish.


Next, add the dependencies your modules will use to the map.
Next, add the dependencies your modules will use to the map.


Finally, pass the map into the `LoadAssembly` method.
Finally, pass the map into the `LoadAssembly` method.
Your modules will automatically be loaded with this dependency map. Your modules will automatically be loaded with this dependency map.


[!code-csharp[DependencyMap Setup](samples/dependency_map_setup.cs)] [!code-csharp[DependencyMap Setup](samples/dependency_map_setup.cs)]


### Usage in Modules ### Usage in Modules


In the constructor of your module, any parameters will be filled in by
In the constructor of your module, any parameters will be filled in by
the @Discord.Commands.IDependencyMap you pass into `LoadAssembly`. the @Discord.Commands.IDependencyMap you pass into `LoadAssembly`.


Any publicly settable properties will also be filled in the same manner.

>[!NOTE]
> Annotating a property with the [DontInject] attribute will prevent it from
being injected.

>[!NOTE] >[!NOTE]
>If you accept `CommandService` or `IDependencyMap` as a parameter in
your constructor, these parameters will be filled by the
CommandService the module was loaded from, and the DependencyMap passed
into it, respectively.
>If you accept `CommandService` or `IDependencyMap` as a parameter in
your constructor or as an injectable property, these entries will be filled
by the CommandService the module was loaded from, and the DependencyMap passed
into it, respectively.


[!code-csharp[DependencyMap in Modules](samples/dependency_module.cs)] [!code-csharp[DependencyMap in Modules](samples/dependency_module.cs)]


# Preconditions # Preconditions


Preconditions serve as a permissions system for your commands. Keep in
mind, however, that they are not limited to _just_ permissions, and
Preconditions serve as a permissions system for your commands. Keep in
mind, however, that they are not limited to _just_ permissions, and
can be as complex as you want them to be. can be as complex as you want them to be.


>[!NOTE] >[!NOTE]
@@ -143,91 +242,79 @@ can be as complex as you want them to be.


## Bundled Preconditions ## Bundled Preconditions


@Discord.Commands ships with two built-in preconditions,
@Discord.Commands.RequireContextAttribute and
@Discord.Commands.RequirePermissionAttribute.

### RequireContext

@Discord.Commands.RequireContextAttribute is a precondition that
requires your command to be executed in the specified context.

You may require three different types of context:
* Guild
* DM
* Group
Commands ships with four bundled preconditions; you may view their
usages on their API page.


Since these are `Flags`, you may OR them together.

[!code-csharp[RequireContext](samples/require_context.cs)]

### RequirePermission

@Discord.Commands.RequirePermissionAttribute is a precondition that
allows you to quickly specfiy that a user must poesess a permission
to execute a command.

You may require either a @Discord.GuildPermission or
@Discord.ChannelPermission

[!code-csharp[RequireContext](samples/require_permission.cs)]
- @Discord.Commands.RequireContextAttribute
- @Discord.Commands.RequireOwnerAttribute
- @Discord.Commands.RequireBotPermissionAttribute
- @Discord.Commands.RequireUserPermissionAttribute


## Custom Preconditions ## Custom Preconditions


To write your own preconditions, create a new class that inherits from To write your own preconditions, create a new class that inherits from
@Discord.Commands.PreconditionAttribute @Discord.Commands.PreconditionAttribute


In order for your precondition to function, you will need to override
`CheckPermissions`, which is a `Task<PreconditionResult>`.
In order for your precondition to function, you will need to override
[CheckPermissions].

Your IDE should provide an option to fill this in for you. Your IDE should provide an option to fill this in for you.


Return `PreconditionResult.FromSuccess()` if the context met the
required parameters, otherwise return `PreconditionResult.FromError()`,
Return [PreconditionResult.FromSuccess] if the context met the
required parameters, otherwise return [PreconditionResult.FromError],
optionally including an error message. optionally including an error message.


[!code-csharp[Custom Precondition](samples/require_owner.cs)] [!code-csharp[Custom Precondition](samples/require_owner.cs)]


[CheckPermissions]: xref:Discord.Commands.PreconditionAttribute#Discord_Commands_PreconditionAttribute_CheckPermissions_Discord_Commands_CommandContext_Discord_Commands_CommandInfo_Discord_Commands_IDependencyMap_
[PreconditionResult.FromSuccess]: xref:Discord.Commands.PreconditionResult#Discord_Commands_PreconditionResult_FromSuccess
[PreconditionResult.FromError]: xref:Discord.Commands.PreconditionResult#Discord_Commands_PreconditionResult_FromError_System_String_

# Type Readers # Type Readers


Type Readers allow you to parse different types of arguments in
Type Readers allow you to parse different types of arguments in
your commands. your commands.


By default, the following Types are supported arguments: By default, the following Types are supported arguments:


- string
- bool
- char
- sbyte/byte - sbyte/byte
- ushort/short - ushort/short
- uint/int - uint/int
- ulong/long - ulong/long
- float, double, decimal - float, double, decimal
- DateTime/DateTimeOffset
- IUser/IGuildUser
- string
- DateTime/DateTimeOffset/TimeSpan
- IMessage/IUserMessage
- IChannel/IGuildChannel/ITextChannel/IVoiceChannel/IGroupChannel - IChannel/IGuildChannel/ITextChannel/IVoiceChannel/IGroupChannel
- IUser/IGuildUser/IGroupUser
- IRole - IRole
- IMessage/IUserMessage


### Creating a Type Readers ### Creating a Type Readers


To create a TypeReader, create a new class that imports @Discord and
To create a TypeReader, create a new class that imports @Discord and
@Discord.Commands. Ensure your class inherits from @Discord.Commands.TypeReader @Discord.Commands. Ensure your class inherits from @Discord.Commands.TypeReader


Next, satisfy the `TypeReader` class by overriding `Task<TypeReaderResult> Read(CommandContext context, string input)`.
Next, satisfy the `TypeReader` class by overriding [Read].


>[!NOTE] >[!NOTE]
>In many cases, Visual Studio can fill this in for you, using the
>In many cases, Visual Studio can fill this in for you, using the
>"Implement Abstract Class" IntelliSense hint. >"Implement Abstract Class" IntelliSense hint.


Inside this task, add whatever logic you need to parse the input string. Inside this task, add whatever logic you need to parse the input string.


Finally, return a `TypeReaderResult`. If you were able to successfully
parse the input, return `TypeReaderResult.FromSuccess(parsedInput)`.
Finally, return a `TypeReaderResult`. If you were able to successfully
parse the input, return `TypeReaderResult.FromSuccess(parsedInput)`.
Otherwise, return `TypeReaderResult.FromError`. Otherwise, return `TypeReaderResult.FromError`.


[Read]: xref:Discord.Commands.TypeReader#Discord_Commands_TypeReader_Read_Discord_Commands_CommandContext_System_String_

#### Sample #### Sample


[!code-csharp[TypeReaders](samples/typereader.cs)] [!code-csharp[TypeReaders](samples/typereader.cs)]


### Installing TypeReaders ### Installing TypeReaders


TypeReaders are not automatically discovered by the Command Service,
and must be explicitly added. To install a TypeReader, invoke [CommandService.AddTypeReader](xref:Discord.Commands.CommandService#Discord_Commands_CommandService_AddTypeReader__1_Discord_Commands_TypeReader_).
TypeReaders are not automatically discovered by the Command Service,
and must be explicitly added. To install a TypeReader, invoke [CommandService.AddTypeReader](xref:Discord.Commands.CommandService#Discord_Commands_CommandService_AddTypeReader__1_Discord_Commands_TypeReader_).

+ 1
- 1
docs/guides/events.md View File

@@ -17,7 +17,7 @@ To hook into events, you must be using the @Discord.WebSocket.DiscordSocketClien


Connection Events will be raised when the Connection State of your client changes. Connection Events will be raised when the Connection State of your client changes.


[DiscordSocketClient.Connected](xref:Discord.WebSocket.DiscordSocketClient#Discord_WebSocket_DiscordSocketClient_Connected) and [Disconnected](Discord_WebSocket_DiscordSocketClient_Disconnected) are raised when the Gateway Socket connects or disconnects, respectively.
[DiscordSocketClient.Connected](xref:Discord.WebSocket.DiscordSocketClient#Discord_WebSocket_DiscordSocketClient_Connected) and [Disconnected](xref:Discord.WebSocket.DiscordSocketClient#Discord_WebSocket_DiscordSocketClient_Disconnected) are raised when the Gateway Socket connects or disconnects, respectively.


>[!WARNING] >[!WARNING]
>You should not use DiscordClient.Connected to run code when your client first connects to Discord. The client has not received and parsed the READY event and guild stream yet, and will have an incomplete or empty cache. >You should not use DiscordClient.Connected to run code when your client first connects to Discord. The client has not received and parsed the READY event and guild stream yet, and will have an incomplete or empty cache.


+ 13
- 3
docs/guides/intro.md View File

@@ -23,11 +23,19 @@ You may add the MyGet feed to Visual Studio directly from `https://www.myget.org
You can also pull the latest source from [GitHub](https://github.com/RogueException/Discord.Net). You can also pull the latest source from [GitHub](https://github.com/RogueException/Discord.Net).


>[!WARNING] >[!WARNING]
>The versions of Discord.Net on NuGet are behind the versions this documentation is written for.
>The versions of Discord.Net on NuGet are behind the versions this
>documentation is written for.
>You MUST install from MyGet or Source!


## Async ## Async


Discord.Net uses C# tasks extensiely - nearly all operations return one. It is highly reccomended these tasks be awaited whenever possible. To do so requires the calling method to be marked as async, which can be problematic in a console application. An example of how to get around this is provided below.
Discord.Net uses C# tasks extensiely - nearly all operations return
one.

It is highly reccomended these tasks be awaited whenever possible.
To do so requires the calling method to be marked as async, which
can be problematic in a console application. An example of how to
get around this is provided below.


For more information, go to [MSDN's Async-Await section.](https://msdn.microsoft.com/en-us/library/hh191443.aspx) For more information, go to [MSDN's Async-Await section.](https://msdn.microsoft.com/en-us/library/hh191443.aspx)


@@ -37,4 +45,6 @@ For more information, go to [MSDN's Async-Await section.](https://msdn.microsoft


>[!NOTE] >[!NOTE]
>In previous versions of Discord.Net, you had to hook into the `Ready` and `GuildAvailable` events to determine when your client was ready for use. >In previous versions of Discord.Net, you had to hook into the `Ready` and `GuildAvailable` events to determine when your client was ready for use.
>In 1.0, the [ConnectAsync](xref:Discord.DiscordSocketClient#ConnectAsync) method will automatically wait for the Ready event, and for all guilds to stream. To avoid this, pass `false` into `ConnectAsync`.
>In 1.0, the [ConnectAsync] method will automatically wait for the Ready event, and for all guilds to stream. To avoid this, pass `false` into `ConnectAsync`.

[ConnectAsync]: xref:Discord.WebSocket.DiscordSocketClient#Discord_WebSocket_DiscordSocketClient_ConnectAsync_System_Boolean_

+ 11
- 0
docs/guides/samples/audio_create_ffmpeg.cs View File

@@ -0,0 +1,11 @@
private Process CreateStream(string path)
{
var ffmpeg = new ProcessStartInfo
{
FileName = "ffmpeg",
Arguments = $"-i {path} -ac 2 -f s16le -ar 48000 pipe:1",
UseShellExecute = false,
RedirectStandardOutput = true,
};
return Process.Start(ffmpeg);
}

+ 9
- 0
docs/guides/samples/audio_ffmpeg.cs View File

@@ -0,0 +1,9 @@
private async Task SendAsync(IAudioClient client, string path)
{
// Create FFmpeg using the previous example
var ffmpeg = CreateStream(path);
var output = ffmpeg.StandardOutput.BaseStream;
var discord = client.CreatePCMStream(1920);
await output.CopyToAsync(discord);
await discord.FlushAsync();
}

+ 13
- 13
docs/guides/samples/command_handler.cs View File

@@ -8,6 +8,7 @@ public class Program
{ {
private CommandService commands; private CommandService commands;
private DiscordSocketClient client; private DiscordSocketClient client;
private DependencyMap map;


static void Main(string[] args) => new Program().Start().GetAwaiter().GetResult(); static void Main(string[] args) => new Program().Start().GetAwaiter().GetResult();


@@ -18,6 +19,8 @@ public class Program


string token = "bot token here"; string token = "bot token here";


map = new DependencyMap();

await InstallCommands(); await InstallCommands();


await client.LoginAsync(TokenType.Bot, token); await client.LoginAsync(TokenType.Bot, token);
@@ -25,13 +28,12 @@ public class Program


await Task.Delay(-1); await Task.Delay(-1);
} }

public async Task InstallCommands() public async Task InstallCommands()
{ {
// Hook the MessageReceived Event into our Command Handler // Hook the MessageReceived Event into our Command Handler
client.MessageReceived += HandleCommand; client.MessageReceived += HandleCommand;
// Discover all of the commands in this assembly and load them. // Discover all of the commands in this assembly and load them.
await commands.LoadAssembly(Assembly.GetEntryAssembly());
await commands.AddModulesAsync(Assembly.GetEntryAssembly());
} }
public async Task HandleCommand(SocketMessage messageParam) public async Task HandleCommand(SocketMessage messageParam)
{ {
@@ -41,16 +43,14 @@ public class Program
// Create a number to track where the prefix ends and the command begins // Create a number to track where the prefix ends and the command begins
int argPos = 0; int argPos = 0;
// Determine if the message is a command, based on if it starts with '!' or a mention prefix // Determine if the message is a command, based on if it starts with '!' or a mention prefix
if (message.HasCharPrefix('!', ref argPos) || message.HasMentionPrefix(client.CurrentUser, ref argPos))
{
// Create a Command Context
var context = new CommandContext(client, message);
// Execute the command. (result does not indicate a return value,
// rather an object stating if the command executed succesfully)
var result = await _commands.Execute(context, argPos);
if (!result.IsSuccess)
await msg.Channel.SendMessageAsync(result.ErrorReason);
}
if (!(message.HasCharPrefix('!', ref argPos) || message.HasMentionPrefix(client.CurrentUser, ref argPos))) return;
// Create a Command Context
var context = new CommandContext(client, message);
// Execute the command. (result does not indicate a return value,
// rather an object stating if the command executed succesfully)
var result = await commands.ExecuteAsync(context, argPos, map);
if (!result.IsSuccess)
await context.Channel.SendMessageAsync(result.ErrorReason);
} }


}
}

+ 9
- 14
docs/guides/samples/dependency_map_setup.cs View File

@@ -7,18 +7,13 @@ public class Commands
{ {
public async Task Install(DiscordSocketClient client) public async Task Install(DiscordSocketClient client)
{ {
var commands = new CommandService();
var map = new DependencyMap();
map.Add(client);
map.Add(commands);
await commands.LoadAssembly(Assembly.GetCurrentAssembly(), map);
// Here, we will inject the Dependency Map with
// all of the services our client will use.
_map.Add(client);
_map.Add(commands);
_map.Add(new NotificationService(_map));
_map.Add(new DatabaseService(_map));
// ...
await _commands.AddModulesAsync(Assembly.GetEntryAssembly());
} }
// In ConfigureServices, we will inject the Dependency Map with
// all of the services our client will use.
public Task ConfigureServices(IDependencyMap map)
{
map.Add(new NotificationService(map));
map.Add(new DatabaseService(map));
}
// ...
}
}

+ 26
- 15
docs/guides/samples/dependency_module.cs View File

@@ -2,28 +2,39 @@ using Discord;
using Discord.Commands; using Discord.Commands;
using Discord.WebSocket; using Discord.WebSocket;


[Module]
public class ModuleA
public class ModuleA : ModuleBase
{ {
private DiscordSocketClient client;
private ISelfUser self;
private readonly DatabaseService _database;


public ModuleA(IDiscordClient c, ISelfUser s)
// Dependencies can be injected via the constructor
public ModuleA(DatabaseService database)
{ {
if (!(c is DiscordSocketClient)) throw new InvalidOperationException("This module requires a DiscordSocketClient");
client = c as DiscordSocketClient;
self = s;
_database = database;
}

public async Task ReadFromDb()
{
var x = _database.getX();
await ReplyAsync(x);
} }
} }


public class ModuleB public class ModuleB
{ {
private IDiscordClient client;
private CommandService commands;
public ModuleB(CommandService c, IDependencyMap m)

// Public settable properties will be injected
public AnnounceService { get; set; }

// Public properties without setters will not
public CommandService Commands { get; }

// Public properties annotated with [DontInject] will not
[DontInject]
public NotificationService { get; set; }

public ModuleB(CommandService commands)
{ {
commands = c;
client = m.Get<IDiscordClient>();
Commands = commands;
} }
}

}

+ 6
- 0
docs/guides/samples/empty-module.cs View File

@@ -0,0 +1,6 @@
using Discord.Commands;

public class InfoModule : ModuleBase
{
}

+ 1
- 1
docs/guides/samples/faq/send_message.cs View File

@@ -1,6 +1,6 @@
public async Task SendMessageToChannel(ulong ChannelId) public async Task SendMessageToChannel(ulong ChannelId)
{ {
var channel = _client.GetChannel(ChannelId) as ISocketMessageChannel;
var channel = _client.GetChannel(ChannelId) as SocketMessageChannel;
await channel?.SendMessageAsync("aaaaaaaaahhh!!!") await channel?.SendMessageAsync("aaaaaaaaahhh!!!")
/* ^ This question mark is used to indicate that 'channel' may sometimes be null, and in cases that it is null, we will do nothing here. */ /* ^ This question mark is used to indicate that 'channel' may sometimes be null, and in cases that it is null, we will do nothing here. */
} }

+ 2
- 2
docs/guides/samples/faq/status.cs View File

@@ -1,5 +1,5 @@
public async Task ModifyStatus() public async Task ModifyStatus()
{ {
await _client.SetStatus(UserStatus.Idle);
await _client.SetGame("Type !help for help");
await _client.SetStatusAsync(UserStatus.Idle);
await _client.SetGameAsync("Type !help for help");
} }

+ 116
- 23
docs/guides/samples/first-steps.cs View File

@@ -1,35 +1,128 @@
using System;
using System.Reflection;
using System.Threading.Tasks;
using Discord; using Discord;
using Discord.Commands;
using Discord.WebSocket; using Discord.WebSocket;


class Program class Program
{ {
// Convert our sync-main to an async main method
static void Main(string[] args) => new Program().Run().GetAwaiter().GetResult();
private readonly DiscordSocketClient _client;
// Keep the CommandService and IDependencyMap around for use with commands.
private readonly IDependencyMap _map = new DependencyMap();
private readonly CommandService _commands = new CommandService();


// Create a DiscordClient with WebSocket support
private DiscordSocketClient client;
// Program entry point
static void Main(string[] args)
{
// Call the Program constructor, followed by the
// MainAsync method and wait until it finishes (which should be never).
new Program().MainAsync().GetAwaiter().GetResult();
}

private Program()
{
_client = new DiscordSocketClient(new DiscordSocketConfig
{
// How much logging do you want to see?
LogLevel = LogSeverity.Info,
// If you or another service needs to do anything with messages
// (eg. checking Reactions), you should probably
// set the MessageCacheSize here.
//MessageCacheSize = 50,

// If your platform doesn't have native websockets,
// add Discord.Net.Providers.WS4Net from NuGet,
// add the `using` at the top, and uncomment this line:
//WebSocketProvider = WS4NetProvider.Instance
});
}


public async Task Run()
// Create a named logging handler, so it can be re-used by addons
// that ask for a Func<LogMessage, Task>.
private static Task Logger(LogMessage message)
{ {
client = new DiscordSocketClient();
var cc = Console.ForegroundColor;
switch (message.Severity)
{
case LogSeverity.Critical:
case LogSeverity.Error:
Console.ForegroundColor = ConsoleColor.Red;
break;
case LogSeverity.Warning:
Console.ForegroundColor = ConsoleColor.Yellow;
break;
case LogSeverity.Info:
Console.ForegroundColor = ConsoleColor.White;
break;
case LogSeverity.Verbose:
case LogSeverity.Debug:
Console.ForegroundColor = ConsoleColor.DarkGray;
break;
}
Console.WriteLine($"{DateTime.Now,-19} [{message.Severity,8}] {message.Source}: {message.Message}");
Console.ForegroundColor = cc;
return Task.CompletedTask;
}

private async Task MainAsync()
{
// Subscribe the logging handler.
_client.Log += Logger;

// Centralize the logic for commands into a seperate method.
await InitCommands();

// Login and connect.
await _client.LoginAsync(TokenType.Bot, /* <DON'T HARDCODE YOUR TOKEN> */);
await _client.ConnectAsync();
// Place the token of your bot account here
string token = "aaabbbccc";

// Hook into the MessageReceived event on DiscordSocketClient
client.MessageReceived += async (message) =>
{ // Check to see if the Message Content is "!ping"
if (message.Content == "!ping")
// Send 'pong' back to the channel the message was sent in
await message.Channel.SendMessageAsync("pong");
};

// Configure the client to use a Bot token, and use our token
await client.LoginAsync(TokenType.Bot, token);
// Connect the client to Discord's gateway
await client.ConnectAsync();

// Block this task until the program is exited.
// Wait infinitely so your bot actually stays connected.
await Task.Delay(-1); await Task.Delay(-1);
} }

private async Task InitCommands()
{
// Repeat this for all the service classes
// and other dependencies that your commands might need.
_map.Add(new SomeServiceClass());

// Either search the program and add all Module classes that can be found:
await _commands.AddModulesAsync(Assembly.GetEntryAssembly());
// Or add Modules manually if you prefer to be a little more explicit:
await _commands.AddModuleAsync<SomeModule>();

// Subscribe a handler to see if a message invokes a command.
_client.MessageReceived += CmdHandler;
}

private async Task CmdHandler(SocketMessage arg)
{
// Bail out if it's a System Message.
var msg = arg as SocketUserMessage;
if (msg == null) return;

// Create a number to track where the prefix ends and the command begins
int pos = 0;
// Replace the '!' with whatever character
// you want to prefix your commands with.
// Uncomment the second half if you also want
// commands to be invoked by mentioning the bot instead.
if (msg.HasCharPrefix('!', ref pos) /* || msg.HasMentionPrefix(msg.Discord.CurrentUser, ref pos) */)
{
// Create a Command Context
var context = new SocketCommandContext(msg.Discord, msg);
// Execute the command. (result does not indicate a return value,
// rather an object stating if the command executed succesfully).
var result = await _commands.ExecuteAsync(context, pos, _map);

// Uncomment the following lines if you want the bot
// to send a message if it failed (not advised for most situations).
//if (!result.IsSuccess && result.Error != CommandError.UnknownCommand)
// await msg.Channel.SendMessageAsync(result.ErrorReason);
}
}
} }

+ 12
- 9
docs/guides/samples/groups.cs View File

@@ -1,15 +1,18 @@
[Group("admin")] [Group("admin")]
public class AdminModule : ModuleBase public class AdminModule : ModuleBase
{ {
[Group("mod")]
public class ModerationGroup : ModuleBase
[Group("clean")]
public class CleanModule : ModuleBase
{ {
// ~admin mod ban foxbot#0282
[Command("ban")]
public async Task Ban(IGuildUser user) { }
}
// ~admin clean 15
[Command]
public async Task Default(int count = 10) => Messages(count);


// ~admin clean 100
[Command("clean")]
public async Task Clean(int count = 100) { }
// ~admin clean messages 15
[Command("messages")]
public async Task Messages(int count = 10) { }
}
// ~admin ban foxbot#0282
[Command("ban")]
public async Task Ban(IGuildUser user) { }
} }

+ 3
- 8
docs/guides/samples/joining_audio.cs View File

@@ -1,15 +1,10 @@
// Create an IAudioClient, and store it for later use
private IAudioClient _audio;

// Create a Join command, that will join the parameter or the user's current voice channel
[Command("join")] [Command("join")]
public async Task JoinChannel(IUserMessage msg,
IVoiceChannel channel = null)
public async Task JoinChannel(IVoiceChannel channel = null)
{ {
// Get the audio channel // Get the audio channel
channel = channel ?? (msg.Author as IGuildUser)?.VoiceChannel; channel = channel ?? (msg.Author as IGuildUser)?.VoiceChannel;
if (channel == null) { await msg.Channel.SendMessageAsync("User must be in a voice channel, or a voice channel must be passed as an argument."); return; } if (channel == null) { await msg.Channel.SendMessageAsync("User must be in a voice channel, or a voice channel must be passed as an argument."); return; }


// Get the IAudioClient by calling the JoinAsync method
_audio = await channel.JoinAsync();
// For the next step with transmitting audio, you would want to pass this Audio Client in to a service.
var audioClient = await channel.ConnectAsync();
} }

+ 14
- 6
docs/guides/samples/logging.cs View File

@@ -3,18 +3,26 @@ using Discord.Rest;


public class Program public class Program
{ {
// Note: This is the light client, it only supports REST calls.
private DiscordClient _client;
private DiscordSocketClient _client;
static void Main(string[] args) => new Program().Start().GetAwaiter().GetResult(); static void Main(string[] args) => new Program().Start().GetAwaiter().GetResult();
public async Task Start() public async Task Start()
{ {
_client = new DiscordClient(new DiscordConfig() {
_client = new DiscordSocketClient(new DiscordSocketConfig() {
LogLevel = LogSeverity.Info LogLevel = LogSeverity.Info
}); });


_client.Log += (message) => Console.WriteLine($"{message.ToString()}");
_client.Log += Log;


await _client.LoginAsync(TokenType.Bot, "bot token");
await _client.LoginAsync(TokenType.Bot, "bot token");
await _client.ConnectAsync();
await Task.Delay(-1);
} }
}

private Task Log(LogMessage message)
{
Console.WriteLine(message.ToString());
return Task.CompletedTask;
}
}

+ 2
- 2
docs/guides/samples/module.cs View File

@@ -7,7 +7,7 @@ public class Info : ModuleBase
{ {
// ~say hello -> hello // ~say hello -> hello
[Command("say"), Summary("Echos a message.")] [Command("say"), Summary("Echos a message.")]
public async Task Say([Unparsed, Summary("The text to echo")] string echo)
public async Task Say([Remainder, Summary("The text to echo")] string echo)
{ {
// ReplyAsync is a method on ModuleBase // ReplyAsync is a method on ModuleBase
await ReplyAsync(echo); await ReplyAsync(echo);
@@ -39,4 +39,4 @@ public class Sample : ModuleBase
var userInfo = user ?? Context.Client.CurrentUser; var userInfo = user ?? Context.Client.CurrentUser;
await ReplyAsync($"{userInfo.Username}#{userInfo.Discriminator}"); await ReplyAsync($"{userInfo.Username}#{userInfo.Discriminator}");
} }
}
}

+ 0
- 10
docs/guides/samples/require_context.cs View File

@@ -1,10 +0,0 @@
public class InfoModule : ModuleBase
{
// Constrain this command to Guilds
[RequireContext(ContextType.Guild)]
public async Task Whois(IGuildUser user) { }

// Constrain this command to either Guilds or DMs
[RequireContext(ContextType.Guild | ContextType.DM)]
public async Task Info() { }
}

+ 3
- 3
docs/guides/samples/require_owner.cs View File

@@ -1,10 +1,10 @@
// Defining the Precondition
// (Note: This precondition is obsolete, it is recommended to use the RequireOwnerAttribute that is bundled with Discord.Commands)


// Inherit from PreconditionAttribute // Inherit from PreconditionAttribute
public class RequireOwnerAttribute : PreconditionAttribute public class RequireOwnerAttribute : PreconditionAttribute
{ {
// Override the CheckPermissions method // Override the CheckPermissions method
public override Task<PreconditionResult> CheckPermissions(CommandContext context, CommandInfo command, IDependencyMap map)
public async override Task<PreconditionResult> CheckPermissions(CommandContext context, CommandInfo command, IDependencyMap map)
{ {
// Get the ID of the bot's owner // Get the ID of the bot's owner
var ownerId = (await map.Get<DiscordSocketClient>().GetApplicationInfoAsync()).Owner.Id; var ownerId = (await map.Get<DiscordSocketClient>().GetApplicationInfoAsync()).Owner.Id;
@@ -15,4 +15,4 @@ public class RequireOwnerAttribute : PreconditionAttribute
else else
return PreconditionResult.FromError("You must be the owner of the bot to run this command."); return PreconditionResult.FromError("You must be the owner of the bot to run this command.");
} }
}
}

+ 0
- 6
docs/guides/samples/require_permission.cs View File

@@ -1,6 +0,0 @@
public class AdminModule : ModuleBase
{
[Command("ban")]
[RequirePermission(GuildPermission.BanMembers)]
public async Task Ban(IGuildUser target) { }
}

+ 1
- 0
docs/guides/samples/typereader.cs View File

@@ -1,3 +1,4 @@
// Note: This example is obsolete, a boolean type reader is bundled with Discord.Commands
using Discord; using Discord;
using Discord.Commands; using Discord.Commands;




+ 90
- 7
docs/guides/voice.md View File

@@ -7,22 +7,105 @@


## Installation ## Installation


To use Audio, you must first configure your `DiscordSocketClient` with Audio support.
To use Audio, you must first configure your [DiscordSocketClient]
with Audio support.


In your @Discord.DiscordSocketConfig, set `AudioMode` to the appropriate @Discord.Audio.AudioMode for your bot. For most bots, you will only need to use `AudioMode.Outgoing`.
In your [DiscordSocketConfig], set `AudioMode` to the appropriate
[AudioMode] for your bot. For most bots, you will only need to use
`AudioMode.Outgoing`.

[DiscordSocketClient]: xref:Discord.WebSocket.DiscordSocketClient
[DiscordSocketConfig]: xref:Discord.WebSocket.DiscordSocketConfig
[AudioMode]: xref:Discord.Audio.AudioMode


### Dependencies ### Dependencies


Audio requires two native libraries, `libsodium` and `opus`. Both of these libraries must be placed in the runtime directory of your bot (for .NET 4.6, the directory where your exe is located; for .NET core, directory where your project.json is located)
Audio requires two native libraries, `libsodium` and `opus`.
Both of these libraries must be placed in the runtime directory of your
bot. (When developing on .NET Framework, this would be `bin/debug`,
when developing on .NET Core, this is where you execute `dotnet run`
from; typically the same directory as your csproj).

For Windows Users, precompiled binaries are available for your
convienence [here](https://discord.foxbot.me/binaries/)


For Windows Users, precompiled binaries are available for your convienence [here](https://discord.foxbot.me/binaries/)
For Linux Users, you will need to compile [Sodium] and [Opus] from
source, or install them from your package manager.


For Linux Users, you will need to compile from source. [Sodium Source Code](https://download.libsodium.org/libsodium/releases/), [Opus Source Code](http://downloads.xiph.org/releases/opus/).
[Sodium]: https://download.libsodium.org/libsodium/releases/
[Opus]: http://downloads.xiph.org/releases/opus/


## Joining a Channel ## Joining a Channel


Joining Voice Channels is relatively straight-forward, and is a requirement for sending or receiving audio. This will also allow us to create an @Discord.Audio.IAudioClient, which will be used later to send or receive audio.
Joining a channel is the first step to sending audio, and will return
an [IAudioClient] to send data with.

To join a channel, simply await [ConnectAsync] on any instance of an
@Discord.IVoiceChannel.


[!code-csharp[Joining a Channel](samples/joining_audio.cs)] [!code-csharp[Joining a Channel](samples/joining_audio.cs)]


The client will sustain a connection to this channel until it is kicked, disconnected from Discord, or told to disconnect.
The client will sustain a connection to this channel until it is
kicked, disconnected from Discord, or told to disconnect.

It should be noted that voice connections are created on a per-guild
basis; only one audio connection may be open by the bot in a single
guild. To switch channels within a guild, invoke [ConnectAsync] on
another voice channel in the guild.

[IAudioClient]: xref:Discord.Audio.IAudioClient
[ConnectAsync]: xref:Discord.IVoiceChannel#Discord_IVoiceChannel_ConnectAsync

## Transmitting Audio

### With FFmpeg

[FFmpeg] is an open source, highly versatile AV-muxing tool. This is
the recommended method of transmitting audio.

Before you begin, you will need to have a version of FFmpeg downloaded
and placed somewhere in your PATH (or alongside the bot, in the same
location as libsodium and opus). Windows binaries are available on
[FFmpeg's download page].

[FFmpeg]: https://ffmpeg.org/
[FFmpeg's download page]: https://ffmpeg.org/download.html

First, you will need to create a Process that starts FFmpeg. An
example of how to do this is included below, though it is important
that you return PCM at 48000hz.

>[!NOTE]
>As of the time of this writing, Discord.Audio struggles significantly
>with processing audio that is already opus-encoded; you will need to
>use the PCM write streams.

[!code-csharp[Creating FFmpeg](samples/audio_create_ffmpeg.cs)]

Next, to transmit audio from FFmpeg to Discord, you will need to
pull an [AudioOutStream] from your [IAudioClient]. Since we're using
PCM audio, use [IAudioClient.CreatePCMStream].

The sample rate argument doesn't particularly matter, so long as it is
a valid rate (120, 240, 480, 960, 1920, or 2880). For the sake of
simplicity, I recommend using 1920.

Channels should be left at `2`, unless you specified a different value
for `-ac 2` when creating FFmpeg.

[AudioOutStream]: xref:Discord.Audio.AudioOutStream
[IAudioClient.CreatePCMStream]: xref:Discord.Audio.IAudioClient#Discord_Audio_IAudioClient_CreatePCMStream_System_Int32_System_Int32_System_Nullable_System_Int32__System_Int32_

Finally, audio will need to be piped from FFmpeg's stdout into your
AudioOutStream. This step can be as complex as you'd like it to be, but
for the majority of cases, you can just use [Stream.CopyToAsync], as
shown below.

[Stream.CopyToAsync]: https://msdn.microsoft.com/en-us/library/hh159084(v=vs.110).aspx

If you are implementing a queue for sending songs, it's likely that
you will want to wait for audio to stop playing before continuing on
to the next song. You can await `AudioOutStream.FlushAsync` to wait for
the audio client's internal buffer to clear out.

[!code-csharp[Sending Audio](samples/audio_ffmpeg.cs)]

+ 9
- 2
docs/index.md View File

@@ -1,6 +1,13 @@


# Discord.Net Documentation # Discord.Net Documentation


Refer to [Guides](guides/intro.md) for tutorials on using Discord.Net, or the [API documentation](api/index.md) to review individual objects in the library.
Discord.Net is an asynchronous, multiplatform .NET Library used to interface with the [Discord API](https://discordapp.com/).


**Todo:** Put something meaningful here.
If this is your first time using Discord.Net, you should refer to the [Intro](guides/intro.md) for tutorials.
More experienced users might refer to the [API Documentation](api/index.md) for a breakdown of the individuals objects in the library.

For additional resources:
- [Discord API Guild](https://discord.gg/discord-api) - Look for `#dotnet_discord-net`
- [GitHub](https://github.com/RogueException/Discord.Net/tree/dev)
- [NuGet](https://www.nuget.org/packages/Discord.Net/)
- [MyGet Feed](https://www.myget.org/feed/Packages/discord-net) - Addons and nightly builds

+ 17
- 0
pack.ps1 View File

@@ -0,0 +1,17 @@
dotnet pack "src\Discord.Net.Core\Discord.Net.Core.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD"
if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) }
dotnet pack "src\Discord.Net.Rest\Discord.Net.Rest.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD"
if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) }
dotnet pack "src\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD"
if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) }
dotnet pack "src\Discord.Net.Rpc\Discord.Net.Rpc.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD"
if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) }
dotnet pack "src\Discord.Net.Commands\Discord.Net.Commands.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD"
if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) }
dotnet pack "src\Discord.Net.Providers.WS4Net\Discord.Net.Providers.WS4Net.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD"
if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) }
dotnet pack "src\Discord.Net.Providers.UdpClient\Discord.Net.Providers.UdpClient.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD"
if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) }

nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties build="$Env:BUILD"
if ($LastExitCode -ne 0) { $host.SetShouldExit($LastExitCode) }

+ 3
- 0
src/Discord.Net.Analyzers/AssemblyInfo.cs View File

@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("Discord.Net.Tests")]

+ 55
- 0
src/Discord.Net.Analyzers/ConfigureAwaitAnalyzer.cs View File

@@ -0,0 +1,55 @@
using System;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

namespace RegexAnalyzer
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class ConfigureAwaitAnalyzer : DiagnosticAnalyzer
{
public const string DiagnosticId = "ConfigureAwait";
internal const string Title = "ConfigureAwait was not specified";
internal const string MessageFormat = "ConfigureAwait error {0}";
internal const string Description = "ConfigureAwait(false) should be used.";
internal const string Category = "Usage";
internal static DiagnosticDescriptor Rule =
new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat,
Category, DiagnosticSeverity.Error, isEnabledByDefault: true, description: Description);

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);

public override void Initialize(AnalysisContext context)
{
context.RegisterSyntaxNodeAction(AnalyzeNode, SyntaxKind.InvocationExpression);
}
private void AnalyzeNode(SyntaxNodeAnalysisContext context)
{
/*var invocationExpr = (InvocationExpressionSyntax)context.Node;
var memberAccessExpr = invocationExpr.Expression as MemberAccessExpressionSyntax;
if (memberAccessExpr?.Name.ToString() != "Match") return;
var memberSymbol = context.SemanticModel.GetSymbolInfo(memberAccessExpr).Symbol as IMethodSymbol;
if (!memberSymbol?.ToString().StartsWith("System.Text.RegularExpressions.Regex.Match") ?? true) return;
var argumentList = invocationExpr.ArgumentList as ArgumentListSyntax;
if ((argumentList?.Arguments.Count ?? 0) < 2) return;
var regexLiteral = argumentList.Arguments[1].Expression as LiteralExpressionSyntax;
if (regexLiteral == null) return;
var regexOpt = context.SemanticModel.GetConstantValue(regexLiteral);
if (!regexOpt.HasValue) return;
var regex = regexOpt.Value as string;
if (regex == null) return;
try
{
System.Text.RegularExpressions.Regex.Match("", regex);
}
catch (ArgumentException e)
{
var diagnostic = Diagnostic.Create(Rule, regexLiteral.GetLocation(), e.Message);
context.ReportDiagnostic(diagnostic);
}*/
}
}
}

+ 32
- 0
src/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj View File

@@ -0,0 +1,32 @@
<Project ToolsVersion="15.0" Sdk="Microsoft.NET.Sdk" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VersionPrefix>1.0.0</VersionPrefix>
<VersionSuffix Condition="'$(BuildNumber)' == ''">rc-dev</VersionSuffix>
<VersionSuffix Condition="'$(BuildNumber)' != ''">rc-$(BuildNumber)</VersionSuffix>
<TargetFrameworks>netstandard1.3</TargetFrameworks>
<AssemblyName>Discord.Net.Analyzers</AssemblyName>
<Authors>RogueException</Authors>
<Description>A Discord.Net extension adding compile-time analysis.</Description>
<PackageTags>discord;discordapp</PackageTags>
<PackageProjectUrl>https://github.com/RogueException/Discord.Net</PackageProjectUrl>
<PackageLicenseUrl>http://opensource.org/licenses/MIT</PackageLicenseUrl>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>git://github.com/RogueException/Discord.Net</RepositoryUrl>
<RootNamespace>Discord.Analyzers</RootNamespace>
<PackageTargetFallback>portable-net45+win81</PackageTargetFallback>
<DisableImplicitFrameworkReferences>true</DisableImplicitFrameworkReferences>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Discord.Net.Core\Discord.Net.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis" Version="2.0.0-rc2">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<NoWarn>$(NoWarn);CS1573;CS1591</NoWarn>
<WarningsAsErrors>true</WarningsAsErrors>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
</Project>

+ 1
- 1
src/Discord.Net.Commands/Attributes/AliasAttribute.cs View File

@@ -3,7 +3,7 @@ using System;
namespace Discord.Commands namespace Discord.Commands
{ {
/// <summary> Provides aliases for a command. </summary> /// <summary> Provides aliases for a command. </summary>
[AttributeUsage(AttributeTargets.Method)]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AliasAttribute : Attribute public class AliasAttribute : Attribute
{ {
/// <summary> The aliases which have been defined for the command. </summary> /// <summary> The aliases which have been defined for the command. </summary>


+ 1
- 1
src/Discord.Net.Commands/Attributes/CommandAttribute.cs View File

@@ -6,7 +6,7 @@ namespace Discord.Commands
public class CommandAttribute : Attribute public class CommandAttribute : Attribute
{ {
public string Text { get; } public string Text { get; }
public RunMode RunMode { get; set; } = RunMode.Sync;
public RunMode RunMode { get; set; } = RunMode.Default;


public CommandAttribute() public CommandAttribute()
{ {


+ 9
- 0
src/Discord.Net.Commands/Attributes/DontInjectAttribute.cs View File

@@ -0,0 +1,9 @@
using System;

namespace Discord.Commands {

[AttributeUsage(AttributeTargets.Property)]
public class DontInjectAttribute : Attribute {
}

}

+ 22
- 0
src/Discord.Net.Commands/Attributes/OverrideTypeReaderAttribute.cs View File

@@ -0,0 +1,22 @@
using System;

using System.Reflection;

namespace Discord.Commands
{
[AttributeUsage(AttributeTargets.Parameter)]
public class OverrideTypeReaderAttribute : Attribute
{
private static readonly TypeInfo _typeReaderTypeInfo = typeof(TypeReader).GetTypeInfo();

public Type TypeReader { get; }

public OverrideTypeReaderAttribute(Type overridenTypeReader)
{
if (!_typeReaderTypeInfo.IsAssignableFrom(overridenTypeReader.GetTypeInfo()))
throw new ArgumentException($"{nameof(overridenTypeReader)} must inherit from {nameof(TypeReader)}");
TypeReader = overridenTypeReader;
}
}
}

+ 11
- 0
src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs View File

@@ -0,0 +1,11 @@
using System;
using System.Threading.Tasks;

namespace Discord.Commands
{
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true, Inherited = true)]
public abstract class ParameterPreconditionAttribute : Attribute
{
public abstract Task<PreconditionResult> CheckPermissions(ICommandContext context, ParameterInfo parameter, object value, IDependencyMap map);
}
}

+ 1
- 1
src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs View File

@@ -6,6 +6,6 @@ namespace Discord.Commands
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = true)] [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public abstract class PreconditionAttribute : Attribute public abstract class PreconditionAttribute : Attribute
{ {
public abstract Task<PreconditionResult> CheckPermissions(CommandContext context, CommandInfo command, IDependencyMap map);
public abstract Task<PreconditionResult> CheckPermissions(ICommandContext context, CommandInfo command, IDependencyMap map);
} }
} }

+ 73
- 0
src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs View File

@@ -0,0 +1,73 @@
using System;
using System.Threading.Tasks;

namespace Discord.Commands
{
/// <summary>
/// This attribute requires that the bot has a specified permission in the channel a command is invoked in.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class RequireBotPermissionAttribute : PreconditionAttribute
{
public GuildPermission? GuildPermission { get; }
public ChannelPermission? ChannelPermission { get; }

/// <summary>
/// Require that the bot account has a specified GuildPermission
/// </summary>
/// <remarks>This precondition will always fail if the command is being invoked in a private channel.</remarks>
/// <param name="permission">The GuildPermission that the bot must have. Multiple permissions can be specified by ORing the permissions together.</param>
public RequireBotPermissionAttribute(GuildPermission permission)
{
GuildPermission = permission;
ChannelPermission = null;
}
/// <summary>
/// Require that the bot account has a specified ChannelPermission.
/// </summary>
/// <param name="permission">The ChannelPermission that the bot must have. Multiple permissions can be specified by ORing the permissions together.</param>
/// <example>
/// <code language="c#">
/// [Command("permission")]
/// [RequireBotPermission(ChannelPermission.ManageMessages)]
/// public async Task Purge()
/// {
/// }
/// </code>
/// </example>
public RequireBotPermissionAttribute(ChannelPermission permission)
{
ChannelPermission = permission;
GuildPermission = null;
}

public override async Task<PreconditionResult> CheckPermissions(ICommandContext 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();
}
}
}

+ 17
- 1
src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs View File

@@ -11,17 +11,33 @@ namespace Discord.Commands
Group = 0x04 Group = 0x04
} }


/// <summary>
/// Require that the command be invoked in a specified context.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class RequireContextAttribute : PreconditionAttribute public class RequireContextAttribute : PreconditionAttribute
{ {
public ContextType Contexts { get; } public ContextType Contexts { get; }


/// <summary>
/// Require that the command be invoked in a specified context.
/// </summary>
/// <param name="contexts">The type of context the command can be invoked in. Multiple contexts can be specified by ORing the contexts together.</param>
/// <example>
/// <code language="c#">
/// [Command("private_only")]
/// [RequireContext(ContextType.DM | ContextType.Group)]
/// public async Task PrivateOnly()
/// {
/// }
/// </code>
/// </example>
public RequireContextAttribute(ContextType contexts) public RequireContextAttribute(ContextType contexts)
{ {
Contexts = contexts; Contexts = contexts;
} }


public override Task<PreconditionResult> CheckPermissions(CommandContext context, CommandInfo command, IDependencyMap map)
public override Task<PreconditionResult> CheckPermissions(ICommandContext context, CommandInfo command, IDependencyMap map)
{ {
bool isValid = false; bool isValid = false;




+ 20
- 0
src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs View File

@@ -0,0 +1,20 @@
using System;
using System.Threading.Tasks;

namespace Discord.Commands
{
/// <summary>
/// Require that the command is invoked by the owner of the bot.
/// </summary>
/// <remarks>This precondition will only work if the bot is a bot account.</remarks>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class RequireOwnerAttribute : PreconditionAttribute
{
public override async Task<PreconditionResult> CheckPermissions(ICommandContext 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");
}
}
}

src/Discord.Net.Commands/Attributes/Preconditions/RequirePermissionAttribute.cs → src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs View File

@@ -3,24 +3,46 @@ using System.Threading.Tasks;


namespace Discord.Commands namespace Discord.Commands
{ {
/// <summary>
/// This attribute requires that the user invoking the command has a specified permission.
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
public class RequirePermissionAttribute : PreconditionAttribute
public class RequireUserPermissionAttribute : PreconditionAttribute
{ {
public GuildPermission? GuildPermission { get; } public GuildPermission? GuildPermission { get; }
public ChannelPermission? ChannelPermission { get; } public ChannelPermission? ChannelPermission { get; }


public RequirePermissionAttribute(GuildPermission permission)
/// <summary>
/// Require that the user invoking the command has a specified GuildPermission
/// </summary>
/// <remarks>This precondition will always fail if the command is being invoked in a private channel.</remarks>
/// <param name="permission">The GuildPermission that the user must have. Multiple permissions can be specified by ORing the permissions together.</param>
public RequireUserPermissionAttribute(GuildPermission permission)
{ {
GuildPermission = permission; GuildPermission = permission;
ChannelPermission = null; ChannelPermission = null;
} }
public RequirePermissionAttribute(ChannelPermission permission)
/// <summary>
/// Require that the user invoking the command has a specified ChannelPermission.
/// </summary>
/// <param name="permission">The ChannelPermission that the user must have. Multiple permissions can be specified by ORing the permissions together.</param>
/// <example>
/// <code language="c#">
/// [Command("permission")]
/// [RequireUserPermission(ChannelPermission.ReadMessageHistory | ChannelPermission.ReadMessages)]
/// public async Task HasPermission()
/// {
/// await ReplyAsync("You can read messages and the message history!");
/// }
/// </code>
/// </example>
public RequireUserPermissionAttribute(ChannelPermission permission)
{ {
ChannelPermission = permission; ChannelPermission = permission;
GuildPermission = null; GuildPermission = null;
} }
public override Task<PreconditionResult> CheckPermissions(CommandContext context, CommandInfo command, IDependencyMap map)
public override Task<PreconditionResult> CheckPermissions(ICommandContext context, CommandInfo command, IDependencyMap map)
{ {
var guildUser = context.User as IGuildUser; var guildUser = context.User as IGuildUser;



+ 15
- 3
src/Discord.Net.Commands/Builders/CommandBuilder.cs View File

@@ -12,7 +12,7 @@ namespace Discord.Commands.Builders
private readonly List<string> _aliases; private readonly List<string> _aliases;


public ModuleBuilder Module { get; } public ModuleBuilder Module { get; }
internal Func<CommandContext, object[], IDependencyMap, Task> Callback { get; set; }
internal Func<ICommandContext, object[], IDependencyMap, Task> Callback { get; set; }


public string Name { get; set; } public string Name { get; set; }
public string Summary { get; set; } public string Summary { get; set; }
@@ -34,7 +34,7 @@ namespace Discord.Commands.Builders
_aliases = new List<string>(); _aliases = new List<string>();
} }
//User-defined //User-defined
internal CommandBuilder(ModuleBuilder module, string primaryAlias, Func<CommandContext, object[], IDependencyMap, Task> callback)
internal CommandBuilder(ModuleBuilder module, string primaryAlias, Func<ICommandContext, object[], IDependencyMap, Task> callback)
: this(module) : this(module)
{ {
Discord.Preconditions.NotNull(primaryAlias, nameof(primaryAlias)); Discord.Preconditions.NotNull(primaryAlias, nameof(primaryAlias));
@@ -72,7 +72,12 @@ namespace Discord.Commands.Builders


public CommandBuilder AddAliases(params string[] aliases) public CommandBuilder AddAliases(params string[] aliases)
{ {
_aliases.AddRange(aliases);
for (int i = 0; i < aliases.Length; i++)
{
var alias = aliases[i] ?? "";
if (!_aliases.Contains(alias))
_aliases.Add(alias);
}
return this; return this;
} }
public CommandBuilder AddPrecondition(PreconditionAttribute precondition) public CommandBuilder AddPrecondition(PreconditionAttribute precondition)
@@ -80,6 +85,13 @@ namespace Discord.Commands.Builders
_preconditions.Add(precondition); _preconditions.Add(precondition);
return this; return this;
} }
public CommandBuilder AddParameter<T>(string name, Action<ParameterBuilder> createFunc)
{
var param = new ParameterBuilder(this, name, typeof(T));
createFunc(param);
_parameters.Add(param);
return this;
}
public CommandBuilder AddParameter(string name, Type type, Action<ParameterBuilder> createFunc) public CommandBuilder AddParameter(string name, Type type, Action<ParameterBuilder> createFunc)
{ {
var param = new ParameterBuilder(this, name, type); var param = new ParameterBuilder(this, name, type);


+ 14
- 5
src/Discord.Net.Commands/Builders/ModuleBuilder.cs View File

@@ -58,9 +58,14 @@ namespace Discord.Commands.Builders
return this; return this;
} }


public ModuleBuilder AddAlias(params string[] newAliases)
public ModuleBuilder AddAliases(params string[] aliases)
{ {
_aliases.AddRange(newAliases);
for (int i = 0; i < aliases.Length; i++)
{
var alias = aliases[i] ?? "";
if (!_aliases.Contains(alias))
_aliases.Add(alias);
}
return this; return this;
} }
public ModuleBuilder AddPrecondition(PreconditionAttribute precondition) public ModuleBuilder AddPrecondition(PreconditionAttribute precondition)
@@ -68,7 +73,7 @@ namespace Discord.Commands.Builders
_preconditions.Add(precondition); _preconditions.Add(precondition);
return this; return this;
} }
public ModuleBuilder AddCommand(string primaryAlias, Func<CommandContext, object[], IDependencyMap, Task> callback, Action<CommandBuilder> createFunc)
public ModuleBuilder AddCommand(string primaryAlias, Func<ICommandContext, object[], IDependencyMap, Task> callback, Action<CommandBuilder> createFunc)
{ {
var builder = new CommandBuilder(this, primaryAlias, callback); var builder = new CommandBuilder(this, primaryAlias, callback);
createFunc(builder); createFunc(builder);
@@ -97,13 +102,17 @@ namespace Discord.Commands.Builders
return this; return this;
} }


public ModuleInfo Build(CommandService service)
private ModuleInfo BuildImpl(CommandService service, ModuleInfo parent = null)
{ {
//Default name to first alias //Default name to first alias
if (Name == null) if (Name == null)
Name = _aliases[0]; Name = _aliases[0];


return new ModuleInfo(this, service);
return new ModuleInfo(this, service, parent);
} }

public ModuleInfo Build(CommandService service) => BuildImpl(service);

internal ModuleInfo Build(CommandService service, ModuleInfo parent) => BuildImpl(service, parent);
} }
} }

+ 58
- 27
src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs View File

@@ -10,7 +10,7 @@ namespace Discord.Commands
{ {
internal static class ModuleClassBuilder internal static class ModuleClassBuilder
{ {
private static readonly TypeInfo _moduleTypeInfo = typeof(ModuleBase).GetTypeInfo();
private static readonly TypeInfo _moduleTypeInfo = typeof(IModuleBase).GetTypeInfo();


public static IEnumerable<TypeInfo> Search(Assembly assembly) public static IEnumerable<TypeInfo> Search(Assembly assembly)
{ {
@@ -28,8 +28,8 @@ namespace Discord.Commands
public static Dictionary<Type, ModuleInfo> Build(CommandService service, params TypeInfo[] validTypes) => Build(validTypes, service); public static Dictionary<Type, ModuleInfo> Build(CommandService service, params TypeInfo[] validTypes) => Build(validTypes, service);
public static Dictionary<Type, ModuleInfo> Build(IEnumerable<TypeInfo> validTypes, CommandService service) public static Dictionary<Type, ModuleInfo> Build(IEnumerable<TypeInfo> validTypes, CommandService service)
{ {
if (!validTypes.Any())
throw new InvalidOperationException("Could not find any valid modules from the given selection");
/*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 topLevelGroups = validTypes.Where(x => x.DeclaringType == null);
var subGroups = validTypes.Intersect(topLevelGroups); var subGroups = validTypes.Intersect(topLevelGroups);
@@ -65,7 +65,8 @@ namespace Discord.Commands
if (builtTypes.Contains(typeInfo)) if (builtTypes.Contains(typeInfo))
continue; continue;
builder.AddModule((module) => {
builder.AddModule((module) =>
{
BuildModule(module, typeInfo, service); BuildModule(module, typeInfo, service);
BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service); BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service);
}); });
@@ -88,17 +89,20 @@ namespace Discord.Commands
else if (attribute is RemarksAttribute) else if (attribute is RemarksAttribute)
builder.Remarks = (attribute as RemarksAttribute).Text; builder.Remarks = (attribute as RemarksAttribute).Text;
else if (attribute is AliasAttribute) else if (attribute is AliasAttribute)
builder.AddAlias((attribute as AliasAttribute).Aliases);
builder.AddAliases((attribute as AliasAttribute).Aliases);
else if (attribute is GroupAttribute) else if (attribute is GroupAttribute)
{ {
var groupAttr = attribute as GroupAttribute; var groupAttr = attribute as GroupAttribute;
builder.Name = builder.Name ?? groupAttr.Prefix; builder.Name = builder.Name ?? groupAttr.Prefix;
builder.AddAlias(groupAttr.Prefix);
builder.AddAliases(groupAttr.Prefix);
} }
else if (attribute is PreconditionAttribute) else if (attribute is PreconditionAttribute)
builder.AddPrecondition(attribute as PreconditionAttribute); builder.AddPrecondition(attribute as PreconditionAttribute);
} }


//Check for unspecified info
if (builder.Aliases.Count == 0)
builder.AddAliases("");
if (builder.Name == null) if (builder.Name == null)
builder.Name = typeInfo.Name; builder.Name = typeInfo.Name;


@@ -106,7 +110,8 @@ namespace Discord.Commands


foreach (var method in validCommands) foreach (var method in validCommands)
{ {
builder.AddCommand((command) => {
builder.AddCommand((command) =>
{
BuildCommand(command, typeInfo, method, service); BuildCommand(command, typeInfo, method, service);
}); });
} }
@@ -115,7 +120,7 @@ namespace Discord.Commands
private static void BuildCommand(CommandBuilder builder, TypeInfo typeInfo, MethodInfo method, CommandService service) private static void BuildCommand(CommandBuilder builder, TypeInfo typeInfo, MethodInfo method, CommandService service)
{ {
var attributes = method.GetCustomAttributes(); var attributes = method.GetCustomAttributes();
foreach (var attribute in attributes) foreach (var attribute in attributes)
{ {
// TODO: C#7 type switch // TODO: C#7 type switch
@@ -140,25 +145,33 @@ namespace Discord.Commands
builder.AddPrecondition(attribute as PreconditionAttribute); builder.AddPrecondition(attribute as PreconditionAttribute);
} }


if (builder.Name == null)
builder.Name = method.Name;

var parameters = method.GetParameters(); var parameters = method.GetParameters();
int pos = 0, count = parameters.Length; int pos = 0, count = parameters.Length;
foreach (var paramInfo in parameters) foreach (var paramInfo in parameters)
{ {
builder.AddParameter((parameter) => {
builder.AddParameter((parameter) =>
{
BuildParameter(parameter, paramInfo, pos++, count, service); BuildParameter(parameter, paramInfo, pos++, count, service);
}); });
} }


var createInstance = ReflectionUtils.CreateBuilder<ModuleBase>(typeInfo, service);
var createInstance = ReflectionUtils.CreateBuilder<IModuleBase>(typeInfo, service);


builder.Callback = (ctx, args, map) => {
builder.Callback = (ctx, args, map) =>
{
var instance = createInstance(map); var instance = createInstance(map);
instance.Context = ctx;
instance.SetContext(ctx);
try try
{ {
return method.Invoke(instance, args) as Task ?? Task.CompletedTask;
instance.BeforeExecute();
return method.Invoke(instance, args) as Task ?? Task.Delay(0);
} }
finally{
finally
{
instance.AfterExecute();
(instance as IDisposable)?.Dispose(); (instance as IDisposable)?.Dispose();
} }
}; };
@@ -179,6 +192,10 @@ namespace Discord.Commands
// TODO: C#7 type switch // TODO: C#7 type switch
if (attribute is SummaryAttribute) if (attribute is SummaryAttribute)
builder.Summary = (attribute as SummaryAttribute).Text; builder.Summary = (attribute as SummaryAttribute).Text;
else if (attribute is OverrideTypeReaderAttribute)
builder.TypeReader = GetTypeReader(service, paramType, (attribute as OverrideTypeReaderAttribute).TypeReader);
else if (attribute is ParameterPreconditionAttribute)
builder.AddPrecondition(attribute as ParameterPreconditionAttribute);
else if (attribute is ParamArrayAttribute) else if (attribute is ParamArrayAttribute)
{ {
builder.IsMultiple = true; builder.IsMultiple = true;
@@ -193,23 +210,37 @@ namespace Discord.Commands
} }
} }


var reader = service.GetTypeReader(paramType);
if (reader == null)
builder.ParameterType = paramType;

if (builder.TypeReader == null)
{ {
var paramTypeInfo = paramType.GetTypeInfo();
if (paramTypeInfo.IsEnum)
{
reader = EnumTypeReader.GetReader(paramType);
service.AddTypeReader(paramType, reader);
}
var readers = service.GetTypeReaders(paramType);
TypeReader reader = null;

if (readers != null)
reader = readers.FirstOrDefault().Value;
else else
{
throw new InvalidOperationException($"{paramType.FullName} is not supported as a command parameter, are you missing a TypeReader?");
}
reader = service.GetDefaultTypeReader(paramType);

builder.TypeReader = reader;
}
}

private static TypeReader GetTypeReader(CommandService service, Type paramType, Type typeReaderType)
{
var readers = service.GetTypeReaders(paramType);
TypeReader reader = null;
if (readers != null)
{
if (readers.TryGetValue(typeReaderType, out reader))
return reader;
} }


builder.ParameterType = paramType;
builder.TypeReader = reader;
//We dont have a cached type reader, create one
reader = ReflectionUtils.CreateObject<TypeReader>(typeReaderType.GetTypeInfo(), service, DependencyMap.Empty);
service.AddTypeReader(paramType, reader);

return reader;
} }


private static bool IsValidModuleDefinition(TypeInfo typeInfo) private static bool IsValidModuleDefinition(TypeInfo typeInfo)


+ 25
- 3
src/Discord.Net.Commands/Builders/ParameterBuilder.cs View File

@@ -1,10 +1,15 @@
using System; using System;
using System.Linq;
using System.Reflection; using System.Reflection;


using System.Collections.Generic;

namespace Discord.Commands.Builders namespace Discord.Commands.Builders
{ {
public class ParameterBuilder public class ParameterBuilder
{ {
private readonly List<ParameterPreconditionAttribute> _preconditions;

public CommandBuilder Command { get; } public CommandBuilder Command { get; }
public string Name { get; internal set; } public string Name { get; internal set; }
public Type ParameterType { get; internal set; } public Type ParameterType { get; internal set; }
@@ -16,16 +21,20 @@ namespace Discord.Commands.Builders
public object DefaultValue { get; set; } public object DefaultValue { get; set; }
public string Summary { get; set; } public string Summary { get; set; }


public IReadOnlyList<ParameterPreconditionAttribute> Preconditions => _preconditions;

//Automatic //Automatic
internal ParameterBuilder(CommandBuilder command) internal ParameterBuilder(CommandBuilder command)
{ {
_preconditions = new List<ParameterPreconditionAttribute>();

Command = command; Command = command;
} }
//User-defined //User-defined
internal ParameterBuilder(CommandBuilder command, string name, Type type) internal ParameterBuilder(CommandBuilder command, string name, Type type)
: this(command) : this(command)
{ {
Preconditions.NotNull(name, nameof(name));
Discord.Preconditions.NotNull(name, nameof(name));


Name = name; Name = name;
SetType(type); SetType(type);
@@ -33,7 +42,14 @@ namespace Discord.Commands.Builders


internal void SetType(Type type) internal void SetType(Type type)
{ {
TypeReader = Command.Module.Service.GetTypeReader(type);
var readers = Command.Module.Service.GetTypeReaders(type);
if (readers != null)
TypeReader = readers.FirstOrDefault().Value;
else
TypeReader = Command.Module.Service.GetDefaultTypeReader(type);

if (TypeReader == null)
throw new InvalidOperationException($"{type} does not have a TypeReader registered for it");


if (type.GetTypeInfo().IsValueType) if (type.GetTypeInfo().IsValueType)
DefaultValue = Activator.CreateInstance(type); DefaultValue = Activator.CreateInstance(type);
@@ -49,7 +65,7 @@ namespace Discord.Commands.Builders
} }
public ParameterBuilder WithDefault(object defaultValue) public ParameterBuilder WithDefault(object defaultValue)
{ {
DefaultValue = defaultValue;
DefaultValue = defaultValue;
return this; return this;
} }
public ParameterBuilder WithIsOptional(bool isOptional) public ParameterBuilder WithIsOptional(bool isOptional)
@@ -68,6 +84,12 @@ namespace Discord.Commands.Builders
return this; return this;
} }


public ParameterBuilder AddPrecondition(ParameterPreconditionAttribute precondition)
{
_preconditions.Add(precondition);
return this;
}

internal ParameterInfo Build(CommandInfo info) internal ParameterInfo Build(CommandInfo info)
{ {
if (TypeReader == null) if (TypeReader == null)


+ 2
- 10
src/Discord.Net.Commands/CommandContext.cs View File

@@ -1,6 +1,6 @@
namespace Discord.Commands namespace Discord.Commands
{ {
public struct CommandContext
public class CommandContext : ICommandContext
{ {
public IDiscordClient Client { get; } public IDiscordClient Client { get; }
public IGuild Guild { get; } public IGuild Guild { get; }
@@ -9,15 +9,7 @@
public IUserMessage Message { get; } public IUserMessage Message { get; }


public bool IsPrivate => Channel is IPrivateChannel; public bool IsPrivate => Channel is IPrivateChannel;

public CommandContext(IDiscordClient client, IGuild guild, IMessageChannel channel, IUser user, IUserMessage msg)
{
Client = client;
Guild = guild;
Channel = channel;
User = user;
Message = msg;
}
public CommandContext(IDiscordClient client, IUserMessage msg) public CommandContext(IDiscordClient client, IUserMessage msg)
{ {
Client = client; Client = client;


+ 26
- 0
src/Discord.Net.Commands/CommandMatch.cs View File

@@ -0,0 +1,26 @@
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Discord.Commands
{
public struct CommandMatch
{
public CommandInfo Command { get; }
public string Alias { get; }

public CommandMatch(CommandInfo command, string alias)
{
Command = command;
Alias = alias;
}

public Task<PreconditionResult> CheckPreconditionsAsync(ICommandContext context, IDependencyMap map = null)
=> Command.CheckPreconditionsAsync(context, map);
public Task<ParseResult> ParseAsync(ICommandContext context, SearchResult searchResult, PreconditionResult? preconditionResult = null)
=> Command.ParseAsync(context, Alias.Length, searchResult, preconditionResult);
public Task<ExecuteResult> ExecuteAsync(ICommandContext context, IEnumerable<object> argList, IEnumerable<object> paramList, IDependencyMap map)
=> Command.ExecuteAsync(context, argList, paramList, map);
public Task<ExecuteResult> ExecuteAsync(ICommandContext context, ParseResult parseResult, IDependencyMap map)
=> Command.ExecuteAsync(context, parseResult, map);
}
}

+ 1
- 1
src/Discord.Net.Commands/CommandParser.cs View File

@@ -13,7 +13,7 @@ namespace Discord.Commands
QuotedParameter QuotedParameter
} }
public static async Task<ParseResult> ParseArgs(CommandInfo command, CommandContext context, string input, int startPos)
public static async Task<ParseResult> ParseArgs(CommandInfo command, ICommandContext context, string input, int startPos)
{ {
ParameterInfo curParam = null; ParameterInfo curParam = null;
StringBuilder argBuilder = new StringBuilder(input.Length); StringBuilder argBuilder = new StringBuilder(input.Length);


+ 81
- 62
src/Discord.Net.Commands/CommandService.cs View File

@@ -14,56 +14,46 @@ namespace Discord.Commands
public class CommandService public class CommandService
{ {
private readonly SemaphoreSlim _moduleLock; private readonly SemaphoreSlim _moduleLock;
private readonly ConcurrentDictionary<Type, ModuleInfo> _typedModuleDefs;
private readonly ConcurrentDictionary<Type, TypeReader> _typeReaders;
private readonly ConcurrentBag<ModuleInfo> _moduleDefs;
private readonly ConcurrentDictionary<Type, ModuleInfo> _typedModuleDefs;
private readonly ConcurrentDictionary<Type, ConcurrentDictionary<Type, TypeReader>> _typeReaders;
private readonly ConcurrentDictionary<Type, TypeReader> _defaultTypeReaders;
private readonly ImmutableList<Tuple<Type, Type>> _entityTypeReaders; //TODO: Candidate for C#7 Tuple
private readonly HashSet<ModuleInfo> _moduleDefs;
private readonly CommandMap _map; private readonly CommandMap _map;


public IEnumerable<ModuleInfo> Modules => _typedModuleDefs.Select(x => x.Value);
public IEnumerable<CommandInfo> Commands => _typedModuleDefs.SelectMany(x => x.Value.Commands);
internal readonly bool _caseSensitive;
internal readonly char _separatorChar;
internal readonly RunMode _defaultRunMode;


public CommandService()
public IEnumerable<ModuleInfo> Modules => _moduleDefs.Select(x => x);
public IEnumerable<CommandInfo> Commands => _moduleDefs.SelectMany(x => x.Commands);
public ILookup<Type, TypeReader> TypeReaders => _typeReaders.SelectMany(x => x.Value.Select(y => new {y.Key, y.Value})).ToLookup(x => x.Key, x => x.Value);

public CommandService() : this(new CommandServiceConfig()) { }
public CommandService(CommandServiceConfig config)
{ {
_caseSensitive = config.CaseSensitiveCommands;
_separatorChar = config.SeparatorChar;
_defaultRunMode = config.DefaultRunMode;
if (_defaultRunMode == RunMode.Default)
throw new InvalidOperationException("The default run mode cannot be set to Default, it must be one of Sync, Mixed, or Async");

_moduleLock = new SemaphoreSlim(1, 1); _moduleLock = new SemaphoreSlim(1, 1);
_typedModuleDefs = new ConcurrentDictionary<Type, ModuleInfo>(); _typedModuleDefs = new ConcurrentDictionary<Type, ModuleInfo>();
_moduleDefs = new ConcurrentBag<ModuleInfo>();
_map = new CommandMap();
_typeReaders = new ConcurrentDictionary<Type, TypeReader>
{
[typeof(bool)] = new SimpleTypeReader<bool>(),
[typeof(char)] = new SimpleTypeReader<char>(),
[typeof(string)] = new SimpleTypeReader<string>(),
[typeof(byte)] = new SimpleTypeReader<byte>(),
[typeof(sbyte)] = new SimpleTypeReader<sbyte>(),
[typeof(ushort)] = new SimpleTypeReader<ushort>(),
[typeof(short)] = new SimpleTypeReader<short>(),
[typeof(uint)] = new SimpleTypeReader<uint>(),
[typeof(int)] = new SimpleTypeReader<int>(),
[typeof(ulong)] = new SimpleTypeReader<ulong>(),
[typeof(long)] = new SimpleTypeReader<long>(),
[typeof(float)] = new SimpleTypeReader<float>(),
[typeof(double)] = new SimpleTypeReader<double>(),
[typeof(decimal)] = new SimpleTypeReader<decimal>(),
[typeof(DateTime)] = new SimpleTypeReader<DateTime>(),
[typeof(DateTimeOffset)] = new SimpleTypeReader<DateTimeOffset>(),
[typeof(IMessage)] = new MessageTypeReader<IMessage>(),
[typeof(IUserMessage)] = new MessageTypeReader<IUserMessage>(),
[typeof(IChannel)] = new ChannelTypeReader<IChannel>(),
[typeof(IDMChannel)] = new ChannelTypeReader<IDMChannel>(),
[typeof(IGroupChannel)] = new ChannelTypeReader<IGroupChannel>(),
[typeof(IGuildChannel)] = new ChannelTypeReader<IGuildChannel>(),
[typeof(IMessageChannel)] = new ChannelTypeReader<IMessageChannel>(),
[typeof(IPrivateChannel)] = new ChannelTypeReader<IPrivateChannel>(),
[typeof(ITextChannel)] = new ChannelTypeReader<ITextChannel>(),
[typeof(IVoiceChannel)] = new ChannelTypeReader<IVoiceChannel>(),

[typeof(IRole)] = new RoleTypeReader<IRole>(),

[typeof(IUser)] = new UserTypeReader<IUser>(),
[typeof(IGroupUser)] = new UserTypeReader<IGroupUser>(),
[typeof(IGuildUser)] = new UserTypeReader<IGuildUser>(),
};
_moduleDefs = new HashSet<ModuleInfo>();
_map = new CommandMap(this);
_typeReaders = new ConcurrentDictionary<Type, ConcurrentDictionary<Type, TypeReader>>();

_defaultTypeReaders = new ConcurrentDictionary<Type, TypeReader>();
foreach (var type in PrimitiveParsers.SupportedTypes)
_defaultTypeReaders[type] = PrimitiveTypeReader.Create(type);

var entityTypeReaders = ImmutableList.CreateBuilder<Tuple<Type, Type>>();
entityTypeReaders.Add(new Tuple<Type, Type>(typeof(IMessage), typeof(MessageTypeReader<>)));
entityTypeReaders.Add(new Tuple<Type, Type>(typeof(IChannel), typeof(ChannelTypeReader<>)));
entityTypeReaders.Add(new Tuple<Type, Type>(typeof(IRole), typeof(RoleTypeReader<>)));
entityTypeReaders.Add(new Tuple<Type, Type>(typeof(IUser), typeof(UserTypeReader<>)));
_entityTypeReaders = entityTypeReaders.ToImmutable();
} }


//Modules //Modules
@@ -99,7 +89,7 @@ namespace Discord.Commands
throw new InvalidOperationException($"Could not build the module {typeof(T).FullName}, did you pass an invalid type?"); throw new InvalidOperationException($"Could not build the module {typeof(T).FullName}, did you pass an invalid type?");


_typedModuleDefs[module.Key] = module.Value; _typedModuleDefs[module.Key] = module.Value;
return LoadModuleInternal(module.Value); return LoadModuleInternal(module.Value);
} }
finally finally
@@ -137,7 +127,7 @@ namespace Discord.Commands


foreach (var submodule in module.Submodules) foreach (var submodule in module.Submodules)
LoadModuleInternal(submodule); LoadModuleInternal(submodule);
return module; return module;
} }


@@ -162,7 +152,7 @@ namespace Discord.Commands
_typedModuleDefs.TryGetValue(typeof(T), out module); _typedModuleDefs.TryGetValue(typeof(T), out module);
if (module == default(ModuleInfo)) if (module == default(ModuleInfo))
return false; return false;
return RemoveModuleInternal(module); return RemoveModuleInternal(module);
} }
finally finally
@@ -172,10 +162,9 @@ namespace Discord.Commands
} }
private bool RemoveModuleInternal(ModuleInfo module) private bool RemoveModuleInternal(ModuleInfo module)
{ {
var defsRemove = module;
if (!_moduleDefs.TryTake(out defsRemove))
if (!_moduleDefs.Remove(module))
return false; return false;
foreach (var cmd in module.Commands) foreach (var cmd in module.Commands)
_map.RemoveCommand(cmd); _map.RemoveCommand(cmd);


@@ -190,26 +179,56 @@ namespace Discord.Commands
//Type Readers //Type Readers
public void AddTypeReader<T>(TypeReader reader) public void AddTypeReader<T>(TypeReader reader)
{ {
_typeReaders[typeof(T)] = reader;
var readers = _typeReaders.GetOrAdd(typeof(T), x => new ConcurrentDictionary<Type, TypeReader>());
readers[reader.GetType()] = reader;
} }
public void AddTypeReader(Type type, TypeReader reader) public void AddTypeReader(Type type, TypeReader reader)
{ {
_typeReaders[type] = reader;
var readers = _typeReaders.GetOrAdd(type, x=> new ConcurrentDictionary<Type, TypeReader>());
readers[reader.GetType()] = reader;
} }
internal TypeReader GetTypeReader(Type type)
internal IDictionary<Type, TypeReader> GetTypeReaders(Type type)
{
ConcurrentDictionary<Type, TypeReader> definedTypeReaders;
if (_typeReaders.TryGetValue(type, out definedTypeReaders))
return definedTypeReaders;
return null;
}
internal TypeReader GetDefaultTypeReader(Type type)
{ {
TypeReader reader; TypeReader reader;
if (_typeReaders.TryGetValue(type, out reader))
if (_defaultTypeReaders.TryGetValue(type, out reader))
return reader;
var typeInfo = type.GetTypeInfo();

//Is this an enum?
if (typeInfo.IsEnum)
{
reader = EnumTypeReader.GetReader(type);
_defaultTypeReaders[type] = reader;
return reader; return reader;
}

//Is this an entity?
for (int i = 0; i < _entityTypeReaders.Count; i++)
{
if (type == _entityTypeReaders[i].Item1 || typeInfo.ImplementedInterfaces.Contains(_entityTypeReaders[i].Item1))
{
reader = Activator.CreateInstance(_entityTypeReaders[i].Item2.MakeGenericType(type)) as TypeReader;
_defaultTypeReaders[type] = reader;
return reader;
}
}
return null; return null;
} }


//Execution //Execution
public SearchResult Search(CommandContext context, int argPos) => Search(context, context.Message.Content.Substring(argPos));
public SearchResult Search(CommandContext context, string input)
public SearchResult Search(ICommandContext context, int argPos)
=> Search(context, context.Message.Content.Substring(argPos));
public SearchResult Search(ICommandContext context, string input)
{ {
string lowerInput = input.ToLowerInvariant();
var matches = _map.GetCommands(input).OrderByDescending(x => x.Priority).ToImmutableArray();
string searchInput = _caseSensitive ? input : input.ToLowerInvariant();
var matches = _map.GetCommands(searchInput).OrderByDescending(x => x.Command.Priority).ToImmutableArray();
if (matches.Length > 0) if (matches.Length > 0)
return SearchResult.FromSuccess(input, matches); return SearchResult.FromSuccess(input, matches);
@@ -217,9 +236,9 @@ namespace Discord.Commands
return SearchResult.FromError(CommandError.UnknownCommand, "Unknown command."); return SearchResult.FromError(CommandError.UnknownCommand, "Unknown command.");
} }


public Task<IResult> ExecuteAsync(CommandContext context, int argPos, IDependencyMap dependencyMap = null, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception)
public Task<IResult> ExecuteAsync(ICommandContext context, int argPos, IDependencyMap dependencyMap = null, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception)
=> ExecuteAsync(context, context.Message.Content.Substring(argPos), dependencyMap, multiMatchHandling); => ExecuteAsync(context, context.Message.Content.Substring(argPos), dependencyMap, multiMatchHandling);
public async Task<IResult> ExecuteAsync(CommandContext context, string input, IDependencyMap dependencyMap = null, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception)
public async Task<IResult> ExecuteAsync(ICommandContext context, string input, IDependencyMap dependencyMap = null, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception)
{ {
dependencyMap = dependencyMap ?? DependencyMap.Empty; dependencyMap = dependencyMap ?? DependencyMap.Empty;


@@ -264,9 +283,9 @@ namespace Discord.Commands
} }
} }


return await commands[i].Execute(context, parseResult, dependencyMap).ConfigureAwait(false);
return await commands[i].ExecuteAsync(context, parseResult, dependencyMap).ConfigureAwait(false);
} }
return SearchResult.FromError(CommandError.UnknownCommand, "This input does not match any overload."); return SearchResult.FromError(CommandError.UnknownCommand, "This input does not match any overload.");
} }
} }


+ 12
- 0
src/Discord.Net.Commands/CommandServiceConfig.cs View File

@@ -0,0 +1,12 @@
namespace Discord.Commands
{
public class CommandServiceConfig
{
/// <summary> The default RunMode commands should have, if one is not specified on the Command attribute or builder. </summary>
public RunMode DefaultRunMode { get; set; } = RunMode.Sync;

public char SeparatorChar { get; set; } = ' ';
/// <summary> Should commands be case-sensitive? </summary>
public bool CaseSensitiveCommands { get; set; } = false;
}
}

+ 46
- 5
src/Discord.Net.Commands/Dependencies/DependencyMap.cs View File

@@ -5,27 +5,59 @@ namespace Discord.Commands
{ {
public class DependencyMap : IDependencyMap public class DependencyMap : IDependencyMap
{ {
private Dictionary<Type, object> map;
private Dictionary<Type, Func<object>> map;


public static DependencyMap Empty => new DependencyMap(); public static DependencyMap Empty => new DependencyMap();


public DependencyMap() public DependencyMap()
{ {
map = new Dictionary<Type, object>();
map = new Dictionary<Type, Func<object>>();
} }


public void Add<T>(T obj)
/// <inheritdoc />
public void Add<T>(T obj) where T : class
=> AddFactory(() => obj);
/// <inheritdoc />
public bool TryAdd<T>(T obj) where T : class
=> TryAddFactory(() => obj);
/// <inheritdoc />
public void AddTransient<T>() where T : class, new()
=> AddFactory(() => new T());
/// <inheritdoc />
public bool TryAddTransient<T>() where T : class, new()
=> TryAddFactory(() => new T());
/// <inheritdoc />
public void AddTransient<TKey, TImpl>() where TKey : class
where TImpl : class, TKey, new()
=> AddFactory<TKey>(() => new TImpl());
public bool TryAddTransient<TKey, TImpl>() where TKey : class
where TImpl : class, TKey, new()
=> TryAddFactory<TKey>(() => new TImpl());
/// <inheritdoc />
public void AddFactory<T>(Func<T> factory) where T : class
{ {
var t = typeof(T); var t = typeof(T);
if (map.ContainsKey(t)) if (map.ContainsKey(t))
throw new InvalidOperationException($"The dependency map already contains \"{t.FullName}\""); throw new InvalidOperationException($"The dependency map already contains \"{t.FullName}\"");
map.Add(t, obj);
map.Add(t, factory);
}
/// <inheritdoc />
public bool TryAddFactory<T>(Func<T> factory) where T : class
{
var t = typeof(T);
if (map.ContainsKey(t))
return false;
map.Add(t, factory);
return true;
} }


/// <inheritdoc />
public T Get<T>() public T Get<T>()
{ {
return (T)Get(typeof(T)); return (T)Get(typeof(T));
} }
/// <inheritdoc />
public object Get(Type t) public object Get(Type t)
{ {
object result; object result;
@@ -35,6 +67,7 @@ namespace Discord.Commands
return result; return result;
} }


/// <inheritdoc />
public bool TryGet<T>(out T result) public bool TryGet<T>(out T result)
{ {
object untypedResult; object untypedResult;
@@ -49,9 +82,17 @@ namespace Discord.Commands
return false; return false;
} }
} }
/// <inheritdoc />
public bool TryGet(Type t, out object result) public bool TryGet(Type t, out object result)
{ {
return map.TryGetValue(t, out result);
Func<object> func;
if (map.TryGetValue(t, out func))
{
result = func();
return true;
}
result = null;
return false;
} }
} }
} }

+ 75
- 1
src/Discord.Net.Commands/Dependencies/IDependencyMap.cs View File

@@ -4,12 +4,86 @@ namespace Discord.Commands
{ {
public interface IDependencyMap public interface IDependencyMap
{ {
void Add<T>(T obj);
/// <summary>
/// Add an instance of a service to be injected.
/// </summary>
/// <typeparam name="T">The type of service.</typeparam>
/// <param name="obj">The instance of a service.</param>
void Add<T>(T obj) where T : class;
/// <summary>
/// Tries to add an instance of a service to be injected.
/// </summary>
/// <typeparam name="T">The type of service.</typeparam>
/// <param name="obj">The instance of a service.</param>
/// <returns>A bool, indicating if the service was successfully added to the DependencyMap.</returns>
bool TryAdd<T>(T obj) where T : class;
/// <summary>
/// Add a service that will be injected by a new instance every time.
/// </summary>
/// <typeparam name="T">The type of instance to inject.</typeparam>
void AddTransient<T>() where T : class, new();
/// <summary>
/// Tries to add a service that will be injected by a new instance every time.
/// </summary>
/// <typeparam name="T">The type of instance to inject.</typeparam>
/// <returns>A bool, indicating if the service was successfully added to the DependencyMap.</returns>
bool TryAddTransient<T>() where T : class, new();
/// <summary>
/// Add a service that will be injected by a new instance every time.
/// </summary>
/// <typeparam name="TKey">The type to look for when injecting.</typeparam>
/// <typeparam name="TImpl">The type to inject when injecting.</typeparam>
/// <example>
/// map.AddTransient&#60;IService, Service&#62;
/// </example>
void AddTransient<TKey, TImpl>() where TKey: class where TImpl : class, TKey, new();
/// <summary>
/// Tries to add a service that will be injected by a new instance every time.
/// </summary>
/// <typeparam name="TKey">The type to look for when injecting.</typeparam>
/// <typeparam name="TImpl">The type to inject when injecting.</typeparam>
/// <returns>A bool, indicating if the service was successfully added to the DependencyMap.</returns>
bool TryAddTransient<TKey, TImpl>() where TKey : class where TImpl : class, TKey, new();
/// <summary>
/// Add a service that will be injected by a factory.
/// </summary>
/// <typeparam name="T">The type to look for when injecting.</typeparam>
/// <param name="factory">The factory that returns a type of this service.</param>
void AddFactory<T>(Func<T> factory) where T : class;
/// <summary>
/// Tries to add a service that will be injected by a factory.
/// </summary>
/// <typeparam name="T">The type to look for when injecting.</typeparam>
/// <param name="factory">The factory that returns a type of this service.</param>
/// <returns>A bool, indicating if the service was successfully added to the DependencyMap.</returns>
bool TryAddFactory<T>(Func<T> factory) where T : class;


/// <summary>
/// Pull an object from the map.
/// </summary>
/// <typeparam name="T">The type of service.</typeparam>
/// <returns>An instance of this service.</returns>
T Get<T>(); T Get<T>();
/// <summary>
/// Try to pull an object from the map.
/// </summary>
/// <typeparam name="T">The type of service.</typeparam>
/// <param name="result">The instance of this service.</param>
/// <returns>Whether or not this object could be found in the map.</returns>
bool TryGet<T>(out T result); bool TryGet<T>(out T result);


/// <summary>
/// Pull an object from the map.
/// </summary>
/// <param name="t">The type of service.</param>
/// <returns>An instance of this service.</returns>
object Get(Type t); object Get(Type t);
/// <summary>
/// Try to pull an object from the map.
/// </summary>
/// <param name="t">The type of service.</param>
/// <param name="result">An instance of this service.</param>
/// <returns>Whether or not this object could be found in the map.</returns>
bool TryGet(Type t, out object result); bool TryGet(Type t, out object result);
} }
} }

+ 9
- 24
src/Discord.Net.Commands/Discord.Net.Commands.csproj View File

@@ -1,41 +1,26 @@
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" />
<Project ToolsVersion="15.0" Sdk="Microsoft.NET.Sdk" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup> <PropertyGroup>
<Description>A Discord.Net extension adding support for bot commands.</Description>
<VersionPrefix>1.0.0-beta2</VersionPrefix>
<TargetFramework>netstandard1.3</TargetFramework>
<VersionPrefix>1.0.0</VersionPrefix>
<VersionSuffix Condition="'$(BuildNumber)' == ''">rc-dev</VersionSuffix>
<VersionSuffix Condition="'$(BuildNumber)' != ''">rc-$(BuildNumber)</VersionSuffix>
<TargetFrameworks>netstandard1.1;netstandard1.3</TargetFrameworks>
<AssemblyName>Discord.Net.Commands</AssemblyName> <AssemblyName>Discord.Net.Commands</AssemblyName>
<Authors>RogueException</Authors>
<Description>A Discord.Net extension adding support for bot commands.</Description>
<PackageTags>discord;discordapp</PackageTags> <PackageTags>discord;discordapp</PackageTags>
<PackageProjectUrl>https://github.com/RogueException/Discord.Net</PackageProjectUrl> <PackageProjectUrl>https://github.com/RogueException/Discord.Net</PackageProjectUrl>
<PackageLicenseUrl>http://opensource.org/licenses/MIT</PackageLicenseUrl> <PackageLicenseUrl>http://opensource.org/licenses/MIT</PackageLicenseUrl>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
<RepositoryUrl>git://github.com/RogueException/Discord.Net</RepositoryUrl> <RepositoryUrl>git://github.com/RogueException/Discord.Net</RepositoryUrl>
<PackageTargetFallback Condition=" '$(TargetFramework)' == 'netstandard1.3' ">$(PackageTargetFallback);dotnet5.4;dnxcore50;portable-net45+win8</PackageTargetFallback>
<RootNamespace>Discord.Commands</RootNamespace>
<DisableImplicitFrameworkReferences>true</DisableImplicitFrameworkReferences>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Compile Include="**\*.cs" />
<EmbeddedResource Include="**\*.resx" />
<EmbeddedResource Include="compiler\resources\**\*" />
</ItemGroup>
<ItemGroup />
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Discord.Net.Core\Discord.Net.Core.csproj" /> <ProjectReference Include="..\Discord.Net.Core\Discord.Net.Core.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Sdk">
<Version>1.0.0-alpha-20161104-2</Version>
<PrivateAssets>All</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup />
<PropertyGroup Label="Configuration">
<SignAssembly>False</SignAssembly>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' "> <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DefineConstants>$(DefineConstants);RELEASE</DefineConstants>
<NoWarn>$(NoWarn);CS1573;CS1591</NoWarn> <NoWarn>$(NoWarn);CS1573;CS1591</NoWarn>
<WarningsAsErrors>true</WarningsAsErrors> <WarningsAsErrors>true</WarningsAsErrors>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup> </PropertyGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project> </Project>

+ 11
- 0
src/Discord.Net.Commands/IModuleBase.cs View File

@@ -0,0 +1,11 @@
namespace Discord.Commands
{
internal interface IModuleBase
{
void SetContext(ICommandContext context);

void BeforeExecute();
void AfterExecute();
}
}

+ 31
- 24
src/Discord.Net.Commands/Info/CommandInfo.cs View File

@@ -7,15 +7,17 @@ using System.Threading.Tasks;
using System.Reflection; using System.Reflection;


using Discord.Commands.Builders; using Discord.Commands.Builders;
using System.Diagnostics;


namespace Discord.Commands namespace Discord.Commands
{ {
[DebuggerDisplay("{Name,nq}")]
public class CommandInfo public class CommandInfo
{ {
private static readonly System.Reflection.MethodInfo _convertParamsMethod = typeof(CommandInfo).GetTypeInfo().GetDeclaredMethod(nameof(ConvertParamsList)); private static readonly System.Reflection.MethodInfo _convertParamsMethod = typeof(CommandInfo).GetTypeInfo().GetDeclaredMethod(nameof(ConvertParamsList));
private static readonly ConcurrentDictionary<Type, Func<IEnumerable<object>, object>> _arrayConverters = new ConcurrentDictionary<Type, Func<IEnumerable<object>, object>>(); private static readonly ConcurrentDictionary<Type, Func<IEnumerable<object>, object>> _arrayConverters = new ConcurrentDictionary<Type, Func<IEnumerable<object>, object>>();


private readonly Func<CommandContext, object[], IDependencyMap, Task> _action;
private readonly Func<ICommandContext, object[], IDependencyMap, Task> _action;


public ModuleInfo Module { get; } public ModuleInfo Module { get; }
public string Name { get; } public string Name { get; }
@@ -37,13 +39,21 @@ namespace Discord.Commands
Summary = builder.Summary; Summary = builder.Summary;
Remarks = builder.Remarks; Remarks = builder.Remarks;


RunMode = builder.RunMode;
RunMode = (builder.RunMode == RunMode.Default ? service._defaultRunMode : builder.RunMode);
Priority = builder.Priority; Priority = builder.Priority;
if (module.Aliases.Count != 0)
Aliases = module.Aliases.Permutate(builder.Aliases, (first, second) => first + " " + second).ToImmutableArray();
else
Aliases = builder.Aliases.ToImmutableArray();
Aliases = module.Aliases
.Permutate(builder.Aliases, (first, second) =>
{
if (first == "")
return second;
else if (second == "")
return first;
else
return first + service._separatorChar + second;
})
.Select(x => service._caseSensitive ? x : x.ToLowerInvariant())
.ToImmutableArray();


Preconditions = builder.Preconditions.ToImmutableArray(); Preconditions = builder.Preconditions.ToImmutableArray();


@@ -53,7 +63,7 @@ namespace Discord.Commands
_action = builder.Callback; _action = builder.Callback;
} }


public async Task<PreconditionResult> CheckPreconditionsAsync(CommandContext context, IDependencyMap map = null)
public async Task<PreconditionResult> CheckPreconditionsAsync(ICommandContext context, IDependencyMap map = null)
{ {
if (map == null) if (map == null)
map = DependencyMap.Empty; map = DependencyMap.Empty;
@@ -74,30 +84,19 @@ namespace Discord.Commands


return PreconditionResult.FromSuccess(); return PreconditionResult.FromSuccess();
} }
public async Task<ParseResult> ParseAsync(CommandContext context, SearchResult searchResult, PreconditionResult? preconditionResult = null)
public async Task<ParseResult> ParseAsync(ICommandContext context, int startIndex, SearchResult searchResult, PreconditionResult? preconditionResult = null)
{ {
if (!searchResult.IsSuccess) if (!searchResult.IsSuccess)
return ParseResult.FromError(searchResult); return ParseResult.FromError(searchResult);
if (preconditionResult != null && !preconditionResult.Value.IsSuccess) if (preconditionResult != null && !preconditionResult.Value.IsSuccess)
return ParseResult.FromError(preconditionResult.Value); 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);

string input = searchResult.Text.Substring(startIndex);
return await CommandParser.ParseArgs(this, context, input, 0).ConfigureAwait(false); return await CommandParser.ParseArgs(this, context, input, 0).ConfigureAwait(false);
} }


public Task<ExecuteResult> Execute(CommandContext context, ParseResult parseResult, IDependencyMap map)
public Task<ExecuteResult> ExecuteAsync(ICommandContext context, ParseResult parseResult, IDependencyMap map)
{ {
if (!parseResult.IsSuccess) if (!parseResult.IsSuccess)
return Task.FromResult(ExecuteResult.FromError(parseResult)); return Task.FromResult(ExecuteResult.FromError(parseResult));
@@ -120,14 +119,22 @@ namespace Discord.Commands


return ExecuteAsync(context, argList, paramList, map); return ExecuteAsync(context, argList, paramList, map);
} }
public async Task<ExecuteResult> ExecuteAsync(CommandContext context, IEnumerable<object> argList, IEnumerable<object> paramList, IDependencyMap map)
public async Task<ExecuteResult> ExecuteAsync(ICommandContext context, IEnumerable<object> argList, IEnumerable<object> paramList, IDependencyMap map)
{ {
if (map == null) if (map == null)
map = DependencyMap.Empty; map = DependencyMap.Empty;


try try
{ {
var args = GenerateArgs(argList, paramList);
object[] args = GenerateArgs(argList, paramList);

foreach (var parameter in Parameters)
{
var result = await parameter.CheckPreconditionsAsync(context, args, map).ConfigureAwait(false);
if (!result.IsSuccess)
return ExecuteResult.FromError(result);
}

switch (RunMode) switch (RunMode)
{ {
case RunMode.Sync: //Always sync case RunMode.Sync: //Always sync


+ 28
- 28
src/Discord.Net.Commands/Info/ModuleInfo.cs View File

@@ -14,61 +14,61 @@ namespace Discord.Commands
public string Remarks { get; } public string Remarks { get; }


public IReadOnlyList<string> Aliases { get; } public IReadOnlyList<string> Aliases { get; }
public IEnumerable<CommandInfo> Commands { get; }
public IReadOnlyList<CommandInfo> Commands { get; }
public IReadOnlyList<PreconditionAttribute> Preconditions { get; } public IReadOnlyList<PreconditionAttribute> Preconditions { get; }
public IReadOnlyList<ModuleInfo> Submodules { get; } public IReadOnlyList<ModuleInfo> Submodules { get; }
public ModuleInfo Parent { get; }
public bool IsSubmodule => Parent != null;


internal ModuleInfo(ModuleBuilder builder, CommandService service)
internal ModuleInfo(ModuleBuilder builder, CommandService service, ModuleInfo parent = null)
{ {
Service = service; Service = service;


Name = builder.Name; Name = builder.Name;
Summary = builder.Summary; Summary = builder.Summary;
Remarks = builder.Remarks; Remarks = builder.Remarks;
Parent = parent;


Aliases = BuildAliases(builder).ToImmutableArray();
Commands = builder.Commands.Select(x => x.Build(this, service));
Aliases = BuildAliases(builder, service).ToImmutableArray();
Commands = builder.Commands.Select(x => x.Build(this, service)).ToImmutableArray();
Preconditions = BuildPreconditions(builder).ToImmutableArray(); Preconditions = BuildPreconditions(builder).ToImmutableArray();


Submodules = BuildSubmodules(builder, service).ToImmutableArray(); Submodules = BuildSubmodules(builder, service).ToImmutableArray();
} }


private static IEnumerable<string> BuildAliases(ModuleBuilder builder)
private static IEnumerable<string> BuildAliases(ModuleBuilder builder, CommandService service)
{ {
IEnumerable<string> result = null;
var result = builder.Aliases.ToList();
var builderQueue = new Queue<ModuleBuilder>();


Stack<ModuleBuilder> builderStack = new Stack<ModuleBuilder>();
builderStack.Push(builder);
var parent = builder;
while ((parent = parent.Parent) != null)
builderQueue.Enqueue(parent);


ModuleBuilder parent = builder.Parent;
while (parent != null)
while (builderQueue.Count > 0)
{ {
builderStack.Push(parent);
parent = parent.Parent;
}

while (builderStack.Count() > 0)
{
ModuleBuilder level = builderStack.Pop(); // get the topmost builder
if (result == null)
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);
var level = builderQueue.Dequeue();
// permute in reverse because we want to *prefix* our aliases
result = level.Aliases.Permutate(result, (first, second) =>
{
if (first == "")
return second;
else if (second == "")
return first;
else
return first + service._separatorChar + second;
}).ToList();
} }


return result; return result;
} }


private static List<ModuleInfo> BuildSubmodules(ModuleBuilder parent, CommandService service)
private List<ModuleInfo> BuildSubmodules(ModuleBuilder parent, CommandService service)
{ {
var result = new List<ModuleInfo>(); var result = new List<ModuleInfo>();


foreach (var submodule in parent.Modules) foreach (var submodule in parent.Modules)
{
result.Add(submodule.Build(service));
}
result.Add(submodule.Build(service, this));


return result; return result;
} }
@@ -87,4 +87,4 @@ namespace Discord.Commands
return result; return result;
} }
} }
}
}

+ 35
- 10
src/Discord.Net.Commands/Info/ParameterInfo.cs View File

@@ -1,5 +1,6 @@
using System; using System;
using System.Linq;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading.Tasks; using System.Threading.Tasks;


using Discord.Commands.Builders; using Discord.Commands.Builders;
@@ -10,6 +11,17 @@ namespace Discord.Commands
{ {
private readonly TypeReader _reader; private readonly TypeReader _reader;


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 Type { get; }
public object DefaultValue { get; }

public IReadOnlyList<ParameterPreconditionAttribute> Preconditions { get; }

internal ParameterInfo(ParameterBuilder builder, CommandInfo command, CommandService service) internal ParameterInfo(ParameterBuilder builder, CommandInfo command, CommandService service)
{ {
Command = command; Command = command;
@@ -23,19 +35,32 @@ namespace Discord.Commands
Type = builder.ParameterType; Type = builder.ParameterType;
DefaultValue = builder.DefaultValue; DefaultValue = builder.DefaultValue;


Preconditions = builder.Preconditions.ToImmutableArray();

_reader = builder.TypeReader; _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 Type { get; }
public object DefaultValue { get; }
public async Task<PreconditionResult> CheckPreconditionsAsync(ICommandContext context, object[] args, IDependencyMap map = null)
{
if (map == null)
map = DependencyMap.Empty;

int position = 0;
for(position = 0; position < Command.Parameters.Count; position++)
if (Command.Parameters[position] == this)
break;

foreach (var precondition in Preconditions)
{
var result = await precondition.CheckPermissions(context, this, args[position], map).ConfigureAwait(false);
if (!result.IsSuccess)
return result;
}

return PreconditionResult.FromSuccess();
}


public async Task<TypeReaderResult> Parse(CommandContext context, string input)
public async Task<TypeReaderResult> Parse(ICommandContext context, string input)
{ {
return await _reader.Read(context, input).ConfigureAwait(false); return await _reader.Read(context, input).ConfigureAwait(false);
} }


+ 9
- 15
src/Discord.Net.Commands/Map/CommandMap.cs View File

@@ -4,36 +4,30 @@ namespace Discord.Commands
{ {
internal class CommandMap internal class CommandMap
{ {
private readonly CommandService _service;
private readonly CommandMapNode _root; private readonly CommandMapNode _root;
private static readonly string[] _blankAliases = new[] { "" }; private static readonly string[] _blankAliases = new[] { "" };


public CommandMap()
public CommandMap(CommandService service)
{ {
_service = service;
_root = new CommandMapNode(""); _root = new CommandMapNode("");
} }


public void AddCommand(CommandInfo command) public void AddCommand(CommandInfo command)
{ {
foreach (string text in GetAliases(command))
_root.AddCommand(text, 0, command);
foreach (string text in command.Aliases)
_root.AddCommand(_service, text, 0, command);
} }
public void RemoveCommand(CommandInfo command) public void RemoveCommand(CommandInfo command)
{ {
foreach (string text in GetAliases(command))
_root.RemoveCommand(text, 0, command);
foreach (string text in command.Aliases)
_root.RemoveCommand(_service, text, 0, command);
} }


public IEnumerable<CommandInfo> GetCommands(string text)
public IEnumerable<CommandMatch> GetCommands(string text)
{ {
return _root.GetCommands(text, 0);
}

private IReadOnlyList<string> GetAliases(CommandInfo command)
{
var aliases = command.Aliases;
if (aliases.Count == 0)
return _blankAliases;
return aliases;
return _root.GetCommands(_service, text, 0, text != "");
} }
} }
} }

+ 48
- 28
src/Discord.Net.Commands/Map/CommandMapNode.cs View File

@@ -7,7 +7,7 @@ namespace Discord.Commands
{ {
internal class CommandMapNode internal class CommandMapNode
{ {
private static readonly char[] _whitespaceChars = new char[] { ' ', '\r', '\n' };
private static readonly char[] _whitespaceChars = new[] { ' ', '\r', '\n' };


private readonly ConcurrentDictionary<string, CommandMapNode> _nodes; private readonly ConcurrentDictionary<string, CommandMapNode> _nodes;
private readonly string _name; private readonly string _name;
@@ -23,9 +23,9 @@ namespace Discord.Commands
_commands = ImmutableArray.Create<CommandInfo>(); _commands = ImmutableArray.Create<CommandInfo>();
} }


public void AddCommand(string text, int index, CommandInfo command)
public void AddCommand(CommandService service, string text, int index, CommandInfo command)
{ {
int nextSpace = NextWhitespace(text, index);
int nextSegment = NextSegment(text, index, service._separatorChar);
string name; string name;


lock (_lockObj) lock (_lockObj)
@@ -38,19 +38,20 @@ namespace Discord.Commands
} }
else else
{ {
if (nextSpace == -1)
if (nextSegment == -1)
name = text.Substring(index); name = text.Substring(index);
else else
name = text.Substring(index, nextSpace - index);
name = text.Substring(index, nextSegment - index);


var nextNode = _nodes.GetOrAdd(name, x => new CommandMapNode(x));
nextNode.AddCommand(nextSpace == -1 ? "" : text, nextSpace + 1, command);
string fullName = _name == "" ? name : _name + service._separatorChar + name;
var nextNode = _nodes.GetOrAdd(name, x => new CommandMapNode(fullName));
nextNode.AddCommand(service, nextSegment == -1 ? "" : text, nextSegment + 1, command);
} }
} }
} }
public void RemoveCommand(string text, int index, CommandInfo command)
public void RemoveCommand(CommandService service, string text, int index, CommandInfo command)
{ {
int nextSpace = NextWhitespace(text, index);
int nextSegment = NextSegment(text, index, service._separatorChar);
string name; string name;


lock (_lockObj) lock (_lockObj)
@@ -59,15 +60,15 @@ namespace Discord.Commands
_commands = _commands.Remove(command); _commands = _commands.Remove(command);
else else
{ {
if (nextSpace == -1)
if (nextSegment == -1)
name = text.Substring(index); name = text.Substring(index);
else else
name = text.Substring(index, nextSpace - index);
name = text.Substring(index, nextSegment - index);


CommandMapNode nextNode; CommandMapNode nextNode;
if (_nodes.TryGetValue(name, out nextNode)) if (_nodes.TryGetValue(name, out nextNode))
{ {
nextNode.RemoveCommand(nextSpace == -1 ? "" : text, nextSpace + 1, command);
nextNode.RemoveCommand(service, nextSegment == -1 ? "" : text, nextSegment + 1, command);
if (nextNode.IsEmpty) if (nextNode.IsEmpty)
_nodes.TryRemove(name, out nextNode); _nodes.TryRemove(name, out nextNode);
} }
@@ -75,39 +76,58 @@ namespace Discord.Commands
} }
} }


public IEnumerable<CommandInfo> GetCommands(string text, int index)
public IEnumerable<CommandMatch> GetCommands(CommandService service, string text, int index, bool visitChildren = true)
{ {
int nextSpace = NextWhitespace(text, index);
string name;

var commands = _commands; var commands = _commands;
for (int i = 0; i < commands.Length; i++) for (int i = 0; i < commands.Length; i++)
yield return _commands[i];
yield return new CommandMatch(_commands[i], _name);


if (text != "")
if (visitChildren)
{ {
if (nextSpace == -1)
string name;
CommandMapNode nextNode;

//Search for next segment
int nextSegment = NextSegment(text, index, service._separatorChar);
if (nextSegment == -1)
name = text.Substring(index); name = text.Substring(index);
else else
name = text.Substring(index, nextSpace - index);

CommandMapNode nextNode;
name = text.Substring(index, nextSegment - index);
if (_nodes.TryGetValue(name, out nextNode)) if (_nodes.TryGetValue(name, out nextNode))
{ {
foreach (var cmd in nextNode.GetCommands(nextSpace == -1 ? "" : text, nextSpace + 1))
foreach (var cmd in nextNode.GetCommands(service, nextSegment == -1 ? "" : text, nextSegment + 1, true))
yield return cmd; yield return cmd;
} }

//Check if this is the last command segment before args
nextSegment = NextSegment(text, index, _whitespaceChars, service._separatorChar);
if (nextSegment != -1)
{
name = text.Substring(index, nextSegment - index);
if (_nodes.TryGetValue(name, out nextNode))
{
foreach (var cmd in nextNode.GetCommands(service, nextSegment == -1 ? "" : text, nextSegment + 1, false))
yield return cmd;
}
}
} }
} }


private static int NextWhitespace(string text, int startIndex)
private static int NextSegment(string text, int startIndex, char separator)
{
return text.IndexOf(separator, startIndex);
}
private static int NextSegment(string text, int startIndex, char[] separators, char except)
{ {
int lowest = int.MaxValue; int lowest = int.MaxValue;
for (int i = 0; i < _whitespaceChars.Length; i++)
for (int i = 0; i < separators.Length; i++)
{ {
int index = text.IndexOf(_whitespaceChars[i], startIndex);
if (index != -1 && index < lowest)
lowest = index;
if (separators[i] != except)
{
int index = text.IndexOf(separators[i], startIndex);
if (index != -1 && index < lowest)
lowest = index;
}
} }
return (lowest != int.MaxValue) ? lowest : -1; return (lowest != int.MaxValue) ? lowest : -1;
} }


+ 29
- 4
src/Discord.Net.Commands/ModuleBase.cs View File

@@ -1,14 +1,39 @@
using System.Threading.Tasks;
using System;
using System.Threading.Tasks;


namespace Discord.Commands namespace Discord.Commands
{ {
public abstract class ModuleBase
public abstract class ModuleBase : ModuleBase<CommandContext> { }

public abstract class ModuleBase<T> : IModuleBase
where T : class, ICommandContext
{ {
public CommandContext Context { get; internal set; }
public T Context { get; private set; }


protected virtual async Task<IUserMessage> ReplyAsync(string message, bool isTTS = false, EmbedBuilder embed = null, RequestOptions options = null)
protected virtual async Task<IUserMessage> ReplyAsync(string message, bool isTTS = false, Embed embed = null, RequestOptions options = null)
{ {
return await Context.Channel.SendMessageAsync(message, isTTS, embed, options).ConfigureAwait(false); return await Context.Channel.SendMessageAsync(message, isTTS, embed, options).ConfigureAwait(false);
} }

protected virtual void BeforeExecute()
{
}

protected virtual void AfterExecute()
{
}

//IModuleBase
void IModuleBase.SetContext(ICommandContext context)
{
var newValue = context as T;
if (newValue == null)
throw new InvalidOperationException($"Invalid context type. Expected {typeof(T).Name}, got {context.GetType().Name}");
Context = newValue;
}

void IModuleBase.BeforeExecute() => BeforeExecute();

void IModuleBase.AfterExecute() => AfterExecute();
} }
} }

+ 8
- 5
src/Discord.Net.Commands/PrimitiveParsers.cs View File

@@ -8,9 +8,11 @@ namespace Discord.Commands


internal static class PrimitiveParsers internal static class PrimitiveParsers
{ {
private static readonly IReadOnlyDictionary<Type, Delegate> _parsers;
private static readonly Lazy<IReadOnlyDictionary<Type, Delegate>> _parsers = new Lazy<IReadOnlyDictionary<Type, Delegate>>(CreateParsers);


static PrimitiveParsers()
public static IEnumerable<Type> SupportedTypes = _parsers.Value.Keys;

static IReadOnlyDictionary<Type, Delegate> CreateParsers()
{ {
var parserBuilder = ImmutableDictionary.CreateBuilder<Type, Delegate>(); var parserBuilder = ImmutableDictionary.CreateBuilder<Type, Delegate>();
parserBuilder[typeof(bool)] = (TryParseDelegate<bool>)bool.TryParse; parserBuilder[typeof(bool)] = (TryParseDelegate<bool>)bool.TryParse;
@@ -27,16 +29,17 @@ namespace Discord.Commands
parserBuilder[typeof(decimal)] = (TryParseDelegate<decimal>)decimal.TryParse; parserBuilder[typeof(decimal)] = (TryParseDelegate<decimal>)decimal.TryParse;
parserBuilder[typeof(DateTime)] = (TryParseDelegate<DateTime>)DateTime.TryParse; parserBuilder[typeof(DateTime)] = (TryParseDelegate<DateTime>)DateTime.TryParse;
parserBuilder[typeof(DateTimeOffset)] = (TryParseDelegate<DateTimeOffset>)DateTimeOffset.TryParse; parserBuilder[typeof(DateTimeOffset)] = (TryParseDelegate<DateTimeOffset>)DateTimeOffset.TryParse;
parserBuilder[typeof(TimeSpan)] = (TryParseDelegate<TimeSpan>)TimeSpan.TryParse;
parserBuilder[typeof(char)] = (TryParseDelegate<char>)char.TryParse; parserBuilder[typeof(char)] = (TryParseDelegate<char>)char.TryParse;
parserBuilder[typeof(string)] = (TryParseDelegate<string>)delegate (string str, out string value) parserBuilder[typeof(string)] = (TryParseDelegate<string>)delegate (string str, out string value)
{ {
value = str; value = str;
return true; return true;
}; };
_parsers = parserBuilder.ToImmutable();
return parserBuilder.ToImmutable();
} }


public static TryParseDelegate<T> Get<T>() => (TryParseDelegate<T>)_parsers[typeof(T)];
public static Delegate Get(Type type) => _parsers[type];
public static TryParseDelegate<T> Get<T>() => (TryParseDelegate<T>)_parsers.Value[typeof(T)];
public static Delegate Get(Type type) => _parsers.Value[type];
} }
} }

+ 1
- 1
src/Discord.Net.Commands/Readers/ChannelTypeReader.cs View File

@@ -9,7 +9,7 @@ namespace Discord.Commands
internal class ChannelTypeReader<T> : TypeReader internal class ChannelTypeReader<T> : TypeReader
where T : class, IChannel where T : class, IChannel
{ {
public override async Task<TypeReaderResult> Read(CommandContext context, string input)
public override async Task<TypeReaderResult> Read(ICommandContext context, string input)
{ {
if (context.Guild != null) if (context.Guild != null)
{ {


+ 1
- 1
src/Discord.Net.Commands/Readers/EnumTypeReader.cs View File

@@ -44,7 +44,7 @@ namespace Discord.Commands
_enumsByValue = byValueBuilder.ToImmutable(); _enumsByValue = byValueBuilder.ToImmutable();
} }


public override Task<TypeReaderResult> Read(CommandContext context, string input)
public override Task<TypeReaderResult> Read(ICommandContext context, string input)
{ {
T baseValue; T baseValue;
object enumValue; object enumValue;


+ 1
- 1
src/Discord.Net.Commands/Readers/MessageTypeReader.cs View File

@@ -6,7 +6,7 @@ namespace Discord.Commands
internal class MessageTypeReader<T> : TypeReader internal class MessageTypeReader<T> : TypeReader
where T : class, IMessage where T : class, IMessage
{ {
public override async Task<TypeReaderResult> Read(CommandContext context, string input)
public override async Task<TypeReaderResult> Read(ICommandContext context, string input)
{ {
ulong id; ulong id;




+ 32
- 0
src/Discord.Net.Commands/Readers/PrimitiveTypeReader.cs View File

@@ -0,0 +1,32 @@
using System;
using System.Threading.Tasks;

namespace Discord.Commands
{
internal static class PrimitiveTypeReader
{
public static TypeReader Create(Type type)
{
type = typeof(PrimitiveTypeReader<>).MakeGenericType(type);
return Activator.CreateInstance(type) as TypeReader;
}
}

internal class PrimitiveTypeReader<T> : TypeReader
{
private readonly TryParseDelegate<T> _tryParse;

public PrimitiveTypeReader()
{
_tryParse = PrimitiveParsers.Get<T>();
}

public override Task<TypeReaderResult> Read(ICommandContext context, string input)
{
T value;
if (_tryParse(input, out value))
return Task.FromResult(TypeReaderResult.FromSuccess(value));
return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, $"Failed to parse {typeof(T).Name}"));
}
}
}

+ 1
- 1
src/Discord.Net.Commands/Readers/RoleTypeReader.cs View File

@@ -9,7 +9,7 @@ namespace Discord.Commands
internal class RoleTypeReader<T> : TypeReader internal class RoleTypeReader<T> : TypeReader
where T : class, IRole where T : class, IRole
{ {
public override Task<TypeReaderResult> Read(CommandContext context, string input)
public override Task<TypeReaderResult> Read(ICommandContext context, string input)
{ {
ulong id; ulong id;




+ 0
- 22
src/Discord.Net.Commands/Readers/SimpleTypeReader.cs View File

@@ -1,22 +0,0 @@
using System.Threading.Tasks;

namespace Discord.Commands
{
internal class SimpleTypeReader<T> : TypeReader
{
private readonly TryParseDelegate<T> _tryParse;

public SimpleTypeReader()
{
_tryParse = PrimitiveParsers.Get<T>();
}

public override Task<TypeReaderResult> Read(CommandContext context, string input)
{
T value;
if (_tryParse(input, out value))
return Task.FromResult(TypeReaderResult.FromSuccess(value));
return Task.FromResult(TypeReaderResult.FromError(CommandError.ParseFailed, $"Failed to parse {typeof(T).Name}"));
}
}
}

+ 1
- 1
src/Discord.Net.Commands/Readers/TypeReader.cs View File

@@ -4,6 +4,6 @@ namespace Discord.Commands
{ {
public abstract class TypeReader public abstract class TypeReader
{ {
public abstract Task<TypeReaderResult> Read(CommandContext context, string input);
public abstract Task<TypeReaderResult> Read(ICommandContext context, string input);
} }
} }

+ 7
- 7
src/Discord.Net.Commands/Readers/UserTypeReader.cs View File

@@ -10,7 +10,7 @@ namespace Discord.Commands
internal class UserTypeReader<T> : TypeReader internal class UserTypeReader<T> : TypeReader
where T : class, IUser where T : class, IUser
{ {
public override async Task<TypeReaderResult> Read(CommandContext context, string input)
public override async Task<TypeReaderResult> Read(ICommandContext context, string input)
{ {
var results = new Dictionary<ulong, TypeReaderValue>(); var results = new Dictionary<ulong, TypeReaderValue>();
IReadOnlyCollection<IUser> channelUsers = (await context.Channel.GetUsersAsync(CacheMode.CacheOnly).Flatten().ConfigureAwait(false)).ToArray(); //TODO: must be a better way? IReadOnlyCollection<IUser> channelUsers = (await context.Channel.GetUsersAsync(CacheMode.CacheOnly).Flatten().ConfigureAwait(false)).ToArray(); //TODO: must be a better way?
@@ -46,13 +46,13 @@ namespace Discord.Commands
ushort discriminator; ushort discriminator;
if (ushort.TryParse(input.Substring(index + 1), out discriminator)) if (ushort.TryParse(input.Substring(index + 1), out discriminator))
{ {
var channelUser = channelUsers.Where(x => x.DiscriminatorValue == discriminator &&
string.Equals(username, x.Username, StringComparison.OrdinalIgnoreCase)).FirstOrDefault();
AddResult(results, channelUser as T, channelUser.Username == username ? 0.85f : 0.75f);
var channelUser = channelUsers.FirstOrDefault(x => x.DiscriminatorValue == discriminator &&
string.Equals(username, x.Username, StringComparison.OrdinalIgnoreCase));
AddResult(results, channelUser as T, channelUser?.Username == username ? 0.85f : 0.75f);


var guildUser = channelUsers.Where(x => x.DiscriminatorValue == discriminator &&
string.Equals(username, x.Username, StringComparison.OrdinalIgnoreCase)).FirstOrDefault();
AddResult(results, guildUser as T, guildUser.Username == username ? 0.80f : 0.70f);
var guildUser = channelUsers.FirstOrDefault(x => x.DiscriminatorValue == discriminator &&
string.Equals(username, x.Username, StringComparison.OrdinalIgnoreCase));
AddResult(results, guildUser as T, guildUser?.Username == username ? 0.80f : 0.70f);
} }
} }




+ 2
- 2
src/Discord.Net.Commands/Results/ParseResult.cs View File

@@ -14,10 +14,10 @@ namespace Discord.Commands


public bool IsSuccess => !Error.HasValue; public bool IsSuccess => !Error.HasValue;


private ParseResult(IReadOnlyList<TypeReaderResult> argValues, IReadOnlyList<TypeReaderResult> paramValue, CommandError? error, string errorReason)
private ParseResult(IReadOnlyList<TypeReaderResult> argValues, IReadOnlyList<TypeReaderResult> paramValues, CommandError? error, string errorReason)
{ {
ArgValues = argValues; ArgValues = argValues;
ParamValues = paramValue;
ParamValues = paramValues;
Error = error; Error = error;
ErrorReason = errorReason; ErrorReason = errorReason;
} }


+ 3
- 3
src/Discord.Net.Commands/Results/SearchResult.cs View File

@@ -7,14 +7,14 @@ namespace Discord.Commands
public struct SearchResult : IResult public struct SearchResult : IResult
{ {
public string Text { get; } public string Text { get; }
public IReadOnlyList<CommandInfo> Commands { get; }
public IReadOnlyList<CommandMatch> Commands { get; }


public CommandError? Error { get; } public CommandError? Error { get; }
public string ErrorReason { get; } public string ErrorReason { get; }


public bool IsSuccess => !Error.HasValue; public bool IsSuccess => !Error.HasValue;


private SearchResult(string text, IReadOnlyList<CommandInfo> commands, CommandError? error, string errorReason)
private SearchResult(string text, IReadOnlyList<CommandMatch> commands, CommandError? error, string errorReason)
{ {
Text = text; Text = text;
Commands = commands; Commands = commands;
@@ -22,7 +22,7 @@ namespace Discord.Commands
ErrorReason = errorReason; ErrorReason = errorReason;
} }


public static SearchResult FromSuccess(string text, IReadOnlyList<CommandInfo> commands)
public static SearchResult FromSuccess(string text, IReadOnlyList<CommandMatch> commands)
=> new SearchResult(text, commands, null, null); => new SearchResult(text, commands, null, null);
public static SearchResult FromError(CommandError error, string reason) public static SearchResult FromError(CommandError error, string reason)
=> new SearchResult(null, null, error, reason); => new SearchResult(null, null, error, reason);


+ 1
- 0
src/Discord.Net.Commands/RunMode.cs View File

@@ -2,6 +2,7 @@
{ {
public enum RunMode public enum RunMode
{ {
Default,
Sync, Sync,
Mixed, Mixed,
Async Async


+ 27
- 12
src/Discord.Net.Commands/Utilities/ReflectionUtils.cs View File

@@ -19,6 +19,9 @@ namespace Discord.Commands


var constructor = constructors[0]; var constructor = constructors[0];
System.Reflection.ParameterInfo[] parameters = constructor.GetParameters(); System.Reflection.ParameterInfo[] parameters = constructor.GetParameters();
System.Reflection.PropertyInfo[] properties = typeInfo.DeclaredProperties
.Where(p => p.SetMethod?.IsPublic == true && p.GetCustomAttribute<DontInjectAttribute>() == null)
.ToArray();


return (map) => return (map) =>
{ {
@@ -27,28 +30,40 @@ namespace Discord.Commands
for (int i = 0; i < parameters.Length; i++) for (int i = 0; i < parameters.Length; i++)
{ {
var parameter = parameters[i]; var parameter = parameters[i];
object arg;
if (map == null || !map.TryGet(parameter.ParameterType, out arg))
{
if (parameter.ParameterType == typeof(CommandService))
arg = service;
else if (parameter.ParameterType == typeof(IDependencyMap))
arg = map;
else
throw new InvalidOperationException($"Failed to create \"{typeInfo.FullName}\", dependency \"{parameter.ParameterType.Name}\" was not found.");
}
args[i] = arg;
args[i] = GetMember(parameter.ParameterType, map, service, typeInfo);
} }


T obj;
try try
{ {
return (T)constructor.Invoke(args);
obj = (T)constructor.Invoke(args);
} }
catch (Exception ex) catch (Exception ex)
{ {
throw new Exception($"Failed to create \"{typeInfo.FullName}\"", ex); throw new Exception($"Failed to create \"{typeInfo.FullName}\"", ex);
} }

foreach(var property in properties)
{
property.SetValue(obj, GetMember(property.PropertyType, map, service, typeInfo));
}
return obj;
}; };
} }

internal static object GetMember(Type targetType, IDependencyMap map, CommandService service, TypeInfo baseType)
{
object arg;
if (map == null || !map.TryGet(targetType, out arg))
{
if (targetType == typeof(CommandService))
arg = service;
else if (targetType == typeof(IDependencyMap))
arg = map;
else
throw new InvalidOperationException($"Failed to create \"{baseType.FullName}\", dependency \"{targetType.Name}\" was not found.");
}
return arg;
}
} }
} }

+ 0
- 43
src/Discord.Net.Commands/project.json View File

@@ -1,43 +0,0 @@
{
"version": "1.0.0-*",
"description": "A Discord.Net extension adding support for bot commands.",
"authors": [ "RogueException" ],

"packOptions": {
"tags": [ "discord", "discordapp" ],
"licenseUrl": "http://opensource.org/licenses/MIT",
"projectUrl": "https://github.com/RogueException/Discord.Net",
"repository": {
"type": "git",
"url": "git://github.com/RogueException/Discord.Net"
}
},

"configurations": {
"Release": {
"buildOptions": {
"define": [ "RELEASE" ],
"nowarn": [ "CS1573", "CS1591" ],
"optimize": true,
"warningsAsErrors": true,
"xmlDoc": true
}
}
},

"dependencies": {
"Discord.Net.Core": {
"target": "project"
}
},

"frameworks": {
"netstandard1.3": {
"imports": [
"dotnet5.4",
"dnxcore50",
"portable-net45+win8"
]
}
}
}

+ 1
- 0
src/Discord.Net.Core/AssemblyInfo.cs View File

@@ -1,5 +1,6 @@
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;


[assembly: InternalsVisibleTo("Discord.Net.Relay")]
[assembly: InternalsVisibleTo("Discord.Net.Rest")] [assembly: InternalsVisibleTo("Discord.Net.Rest")]
[assembly: InternalsVisibleTo("Discord.Net.Rpc")] [assembly: InternalsVisibleTo("Discord.Net.Rpc")]
[assembly: InternalsVisibleTo("Discord.Net.WebSocket")] [assembly: InternalsVisibleTo("Discord.Net.WebSocket")]


+ 9
- 0
src/Discord.Net.Core/Audio/AudioApplication.cs View File

@@ -0,0 +1,9 @@
namespace Discord.Audio
{
public enum AudioApplication : int
{
Voice,
Music,
Mixed
}
}

+ 41
- 0
src/Discord.Net.Core/Audio/AudioInStream.cs View File

@@ -0,0 +1,41 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace Discord.Audio
{
public abstract class AudioInStream : Stream
{
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => true;

public abstract Task<RTPFrame?> ReadFrameAsync(CancellationToken cancelToken);

public RTPFrame? ReadFrame()
{
return ReadFrameAsync(CancellationToken.None).GetAwaiter().GetResult();
}
public override int Read(byte[] buffer, int offset, int count)
{
return ReadAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult();
}
public override void Write(byte[] buffer, int offset, int count)
{
WriteAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult();
}

public override void Flush() { throw new NotSupportedException(); }

public override long Length { get { throw new NotSupportedException(); } }
public override long Position
{
get { throw new NotSupportedException(); }
set { throw new NotSupportedException(); }
}

public override void SetLength(long value) { throw new NotSupportedException(); }
public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); }
}
}

+ 41
- 0
src/Discord.Net.Core/Audio/AudioOutStream.cs View File

@@ -0,0 +1,41 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace Discord.Audio
{
public abstract class AudioOutStream : Stream
{
public override bool CanRead => false;
public override bool CanSeek => false;
public override bool CanWrite => true;

public override void Write(byte[] buffer, int offset, int count)
{
WriteAsync(buffer, offset, count, CancellationToken.None).GetAwaiter().GetResult();
}
public override void Flush()
{
FlushAsync(CancellationToken.None).GetAwaiter().GetResult();
}
public void Clear()
{
ClearAsync(CancellationToken.None).GetAwaiter().GetResult();
}

public virtual Task ClearAsync(CancellationToken cancellationToken) { return Task.Delay(0); }
//public virtual Task WriteSilenceAsync(CancellationToken cancellationToken) { return Task.Delay(0); }

public override long Length { get { throw new NotSupportedException(); } }
public override long Position
{
get { throw new NotSupportedException(); }
set { throw new NotSupportedException(); }
}

public override int Read(byte[] buffer, int offset, int count) { throw new NotSupportedException(); }
public override void SetLength(long value) { throw new NotSupportedException(); }
public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); }
}
}

+ 28
- 5
src/Discord.Net.Core/Audio/IAudioClient.cs View File

@@ -1,10 +1,9 @@
using System; using System;
using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;


namespace Discord.Audio namespace Discord.Audio
{ {
public interface IAudioClient
public interface IAudioClient : IDisposable
{ {
event Func<Task> Connected; event Func<Task> Connected;
event Func<Exception, Task> Disconnected; event Func<Exception, Task> Disconnected;
@@ -15,9 +14,33 @@ namespace Discord.Audio
/// <summary> Gets the estimated round-trip latency, in milliseconds, to the gateway server. </summary> /// <summary> Gets the estimated round-trip latency, in milliseconds, to the gateway server. </summary>
int Latency { get; } int Latency { get; }


Task DisconnectAsync();
Task StopAsync();


Stream CreateOpusStream(int samplesPerFrame, int bufferSize = 4000);
Stream CreatePCMStream(int samplesPerFrame, int? bitrate = null, OpusApplication application = OpusApplication.MusicOrMixed, int bufferSize = 4000);
/// <summary>
/// Creates a new outgoing stream accepting Opus-encoded data.
/// </summary>
/// <param name="samplesPerFrame">Samples per frame. Must be 120, 240, 480, 960, 1920 or 2880, representing 2.5, 5, 10, 20, 40 or 60 milliseconds respectively.</param>
/// <returns></returns>
AudioOutStream CreateOpusStream(int samplesPerFrame, int bufferMillis = 1000);
/// <summary>
/// Creates a new outgoing stream accepting Opus-encoded data. This is a direct stream with no internal timer.
/// </summary>
/// <param name="samplesPerFrame">Samples per frame. Must be 120, 240, 480, 960, 1920 or 2880, representing 2.5, 5, 10, 20, 40 or 60 milliseconds respectively.</param>
/// <returns></returns>
AudioOutStream CreateDirectOpusStream(int samplesPerFrame);
/// <summary>
/// Creates a new outgoing stream accepting PCM (raw) data.
/// </summary>
/// <param name="samplesPerFrame">Samples per frame. Must be 120, 240, 480, 960, 1920 or 2880, representing 2.5, 5, 10, 20, 40 or 60 milliseconds respectively.</param>
/// <param name="bitrate"></param>
/// <returns></returns>
AudioOutStream CreatePCMStream(AudioApplication application, int samplesPerFrame, int channels = 2, int? bitrate = null, int bufferMillis = 1000);
/// <summary>
/// Creates a new direct outgoing stream accepting PCM (raw) data. This is a direct stream with no internal timer.
/// </summary>
/// <param name="samplesPerFrame">Samples per frame. Must be 120, 240, 480, 960, 1920 or 2880, representing 2.5, 5, 10, 20, 40 or 60 milliseconds respectively.</param>
/// <param name="bitrate"></param>
/// <returns></returns>
AudioOutStream CreateDirectPCMStream(AudioApplication application, int samplesPerFrame, int channels = 2, int? bitrate = null);
} }
} }

+ 16
- 0
src/Discord.Net.Core/Audio/RTPFrame.cs View File

@@ -0,0 +1,16 @@
namespace Discord.Audio
{
public struct RTPFrame
{
public readonly ushort Sequence;
public readonly uint Timestamp;
public readonly byte[] Payload;

public RTPFrame(ushort sequence, uint timestamp, byte[] payload)
{
Sequence = sequence;
Timestamp = timestamp;
Payload = payload;
}
}
}

src/Discord.Net.Core/API/CDN.cs → src/Discord.Net.Core/CDN.cs View File

@@ -1,11 +1,11 @@
namespace Discord.API
namespace Discord
{ {
public static class CDN public static class CDN
{ {
public static string GetApplicationIconUrl(ulong appId, string iconId) public static string GetApplicationIconUrl(ulong appId, string iconId)
=> iconId != null ? $"{DiscordConfig.CDNUrl}app-icons/{appId}/{iconId}.jpg" : null; => iconId != null ? $"{DiscordConfig.CDNUrl}app-icons/{appId}/{iconId}.jpg" : null;
public static string GetUserAvatarUrl(ulong userId, string avatarId)
=> avatarId != null ? $"{DiscordConfig.CDNUrl}avatars/{userId}/{avatarId}.jpg" : null;
public static string GetUserAvatarUrl(ulong userId, string avatarId, ushort size, AvatarFormat format)
=> avatarId != null ? $"{DiscordConfig.CDNUrl}avatars/{userId}/{avatarId}.{format.ToString().ToLower()}?size={size}" : null;
public static string GetGuildIconUrl(ulong guildId, string iconId) public static string GetGuildIconUrl(ulong guildId, string iconId)
=> iconId != null ? $"{DiscordConfig.CDNUrl}icons/{guildId}/{iconId}.jpg" : null; => iconId != null ? $"{DiscordConfig.CDNUrl}icons/{guildId}/{iconId}.jpg" : null;
public static string GetGuildSplashUrl(ulong guildId, string splashId) public static string GetGuildSplashUrl(ulong guildId, string splashId)

+ 11
- 0
src/Discord.Net.Core/Commands/ICommandContext.cs View File

@@ -0,0 +1,11 @@
namespace Discord.Commands
{
public interface ICommandContext
{
IDiscordClient Client { get; }
IGuild Guild { get; }
IMessageChannel Channel { get; }
IUser User { get; }
IUserMessage Message { get; }
}
}

+ 20
- 44
src/Discord.Net.Core/Discord.Net.Core.csproj View File

@@ -1,60 +1,36 @@
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" />
<Project ToolsVersion="15.0" Sdk="Microsoft.NET.Sdk" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup> <PropertyGroup>
<Description>A .Net API wrapper and bot framework for Discord.</Description>
<VersionPrefix>1.0.0-beta2</VersionPrefix>
<TargetFramework>netstandard1.3</TargetFramework>
<VersionPrefix>1.0.0</VersionPrefix>
<VersionSuffix Condition="'$(BuildNumber)' == ''">rc-dev</VersionSuffix>
<VersionSuffix Condition="'$(BuildNumber)' != ''">rc-$(BuildNumber)</VersionSuffix>
<TargetFrameworks>netstandard1.1;netstandard1.3</TargetFrameworks>
<AssemblyName>Discord.Net.Core</AssemblyName> <AssemblyName>Discord.Net.Core</AssemblyName>
<Authors>RogueException</Authors>
<Description>A .Net API wrapper and bot framework for Discord.</Description>
<PackageTags>discord;discordapp</PackageTags> <PackageTags>discord;discordapp</PackageTags>
<PackageProjectUrl>https://github.com/RogueException/Discord.Net</PackageProjectUrl> <PackageProjectUrl>https://github.com/RogueException/Discord.Net</PackageProjectUrl>
<PackageLicenseUrl>http://opensource.org/licenses/MIT</PackageLicenseUrl> <PackageLicenseUrl>http://opensource.org/licenses/MIT</PackageLicenseUrl>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>
<RepositoryUrl>git://github.com/RogueException/Discord.Net</RepositoryUrl> <RepositoryUrl>git://github.com/RogueException/Discord.Net</RepositoryUrl>
<PackageTargetFallback Condition=" '$(TargetFramework)' == 'netstandard1.3' ">$(PackageTargetFallback);dotnet5.4;dnxcore50;portable-net45+win8</PackageTargetFallback>
<RootNamespace>Discord</RootNamespace>
<DisableImplicitFrameworkReferences>true</DisableImplicitFrameworkReferences>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="**\*.cs" />
<EmbeddedResource Include="**\*.resx" />
<EmbeddedResource Include="compiler\resources\**\*" />
<PackageReference Include="Newtonsoft.Json" Version="9.0.1" />
<PackageReference Include="System.Collections.Concurrent" Version="4.3.0" />
<PackageReference Include="System.Collections.Immutable" Version="1.3.0" />
<PackageReference Include="System.Interactive.Async" Version="3.1.0" />
<PackageReference Include="System.Linq" Version="4.3.0" />
<PackageReference Include="System.Linq.Expressions" Version="4.3.0" />
<PackageReference Include="System.Net.Http" Version="4.3.0" />
<PackageReference Include="System.Reflection" Version="4.3.0" />
<PackageReference Include="System.Resources.ResourceManager" Version="4.3.0" />
<PackageReference Include="System.Runtime" Version="4.3.0" />
<PackageReference Include="System.Runtime.Serialization.Primitives" Version="4.3.0" />
</ItemGroup> </ItemGroup>
<ItemGroup />
<ItemGroup>
<PackageReference Include="Microsoft.NET.Sdk">
<Version>1.0.0-alpha-20161104-2</Version>
<PrivateAssets>All</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.Win32.Primitives">
<Version>4.3.0</Version>
</PackageReference>
<PackageReference Include="Newtonsoft.Json">
<Version>9.0.1</Version>
</PackageReference>
<PackageReference Include="System.Collections.Concurrent">
<Version>4.3.0</Version>
</PackageReference>
<PackageReference Include="System.Collections.Immutable">
<Version>1.3.0</Version>
</PackageReference>
<PackageReference Include="System.Interactive.Async">
<Version>3.1.0</Version>
</PackageReference>
<PackageReference Include="System.Net.Http">
<Version>4.3.0</Version>
</PackageReference>
<PackageReference Include="System.Net.WebSockets.Client">
<Version>4.3.0</Version>
<PrivateAssets>All</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup />
<PropertyGroup Label="Configuration">
<SignAssembly>False</SignAssembly>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' "> <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DefineConstants>$(DefineConstants);RELEASE</DefineConstants>
<NoWarn>$(NoWarn);CS1573;CS1591</NoWarn> <NoWarn>$(NoWarn);CS1573;CS1591</NoWarn>
<WarningsAsErrors>true</WarningsAsErrors> <WarningsAsErrors>true</WarningsAsErrors>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup> </PropertyGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project> </Project>

+ 6
- 0
src/Discord.Net.Core/DiscordConfig.cs View File

@@ -19,7 +19,13 @@ namespace Discord
public const int MaxMessagesPerBatch = 100; public const int MaxMessagesPerBatch = 100;
public const int MaxUsersPerBatch = 1000; public const int MaxUsersPerBatch = 1000;


/// <summary> Gets or sets how a request should act in the case of an error, by default. </summary>
public RetryMode DefaultRetryMode { get; set; } = RetryMode.AlwaysRetry;
/// <summary> Gets or sets the minimum log level severity that will be sent to the LogMessage event. </summary> /// <summary> Gets or sets the minimum log level severity that will be sent to the LogMessage event. </summary>
public LogSeverity LogLevel { get; set; } = LogSeverity.Info; public LogSeverity LogLevel { get; set; } = LogSeverity.Info;

/// <summary> Gets or sets whether the initial log entry should be printed. </summary>
internal bool DisplayInitialLog { get; set; } = true;
} }
} }

+ 20
- 0
src/Discord.Net.Core/Entities/Channels/BulkGuildChannelProperties.cs View File

@@ -0,0 +1,20 @@
namespace Discord
{
public class BulkGuildChannelProperties
{
/// <summary>
/// The id of the channel to apply this position to.
/// </summary>
public ulong Id { get; set; }
/// <summary>
/// The new zero-based position of this channel.
/// </summary>
public int Position { get; set; }

public BulkGuildChannelProperties(ulong id, int position)
{
Id = id;
Position = position;
}
}
}

src/Discord.Net.Core/Entities/Messages/Direction.cs → src/Discord.Net.Core/Entities/Channels/Direction.cs View File


+ 30
- 0
src/Discord.Net.Core/Entities/Channels/GuildChannelProperties.cs View File

@@ -0,0 +1,30 @@
namespace Discord
{
/// <summary>
/// Modify an IGuildChannel with the specified changes.
/// </summary>
/// <example>
/// <code language="c#">
/// await (Context.Channel as ITextChannel)?.ModifyAsync(x =>
/// {
/// x.Name = "do-not-enter";
/// });
/// </code>
/// </example>
public class GuildChannelProperties
{
/// <summary>
/// Set the channel to this name
/// </summary>
/// <remarks>
/// When modifying an ITextChannel, the Name MUST be alphanumeric with dashes.
/// It must match the following RegEx: [a-z0-9-_]{2,100}
/// </remarks>
/// <exception cref="Net.HttpException">A BadRequest will be thrown if the name does not match the above RegEx.</exception>
public Optional<string> Name { get; set; }
/// <summary>
/// Move the channel to the following position. This is 0-based!
/// </summary>
public Optional<int> Position { get; set; }
}
}

+ 1
- 1
src/Discord.Net.Core/Entities/Channels/IAudioChannel.cs View File

@@ -1,6 +1,6 @@
namespace Discord namespace Discord
{ {
public interface IAudioChannel
public interface IAudioChannel : IChannel
{ {
} }
} }

+ 3
- 4
src/Discord.Net.Core/Entities/Channels/IGuildChannel.cs View File

@@ -1,5 +1,4 @@
using Discord.API.Rest;
using System;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;


@@ -21,12 +20,12 @@ namespace Discord
/// <param name="maxAge"> The time (in seconds) until the invite expires. Set to null to never expire. </param> /// <param name="maxAge"> The time (in seconds) until the invite expires. Set to null to never expire. </param>
/// <param name="maxUses"> The max amount of times this invite may be used. Set to null to have unlimited uses. </param> /// <param name="maxUses"> The max amount of times this invite may be used. Set to null to have unlimited uses. </param>
/// <param name="isTemporary"> If true, a user accepting this invite will be kicked from the guild after closing their client. </param> /// <param name="isTemporary"> If true, a user accepting this invite will be kicked from the guild after closing their client. </param>
Task<IInviteMetadata> CreateInviteAsync(int? maxAge = 1800, int? maxUses = default(int?), bool isTemporary = false, RequestOptions options = null);
Task<IInviteMetadata> CreateInviteAsync(int? maxAge = 1800, int? maxUses = default(int?), bool isTemporary = false, bool isUnique = false, RequestOptions options = null);
/// <summary> Returns a collection of all invites to this channel. </summary> /// <summary> Returns a collection of all invites to this channel. </summary>
Task<IReadOnlyCollection<IInviteMetadata>> GetInvitesAsync(RequestOptions options = null); Task<IReadOnlyCollection<IInviteMetadata>> GetInvitesAsync(RequestOptions options = null);
/// <summary> Modifies this guild channel. </summary> /// <summary> Modifies this guild channel. </summary>
Task ModifyAsync(Action<ModifyGuildChannelParams> func, RequestOptions options = null);
Task ModifyAsync(Action<GuildChannelProperties> func, RequestOptions options = null);


/// <summary> Gets the permission overwrite for a specific role, or null if one does not exist. </summary> /// <summary> Gets the permission overwrite for a specific role, or null if one does not exist. </summary>
OverwritePermissions? GetPermissionOverwrite(IRole role); OverwritePermissions? GetPermissionOverwrite(IRole role);


+ 3
- 1
src/Discord.Net.Core/Entities/Channels/IMessageChannel.cs View File

@@ -8,9 +8,11 @@ namespace Discord
public interface IMessageChannel : IChannel public interface IMessageChannel : IChannel
{ {
/// <summary> Sends a message to this message channel. </summary> /// <summary> Sends a message to this message channel. </summary>
Task<IUserMessage> SendMessageAsync(string text, bool isTTS = false, EmbedBuilder embed = null, RequestOptions options = null);
Task<IUserMessage> SendMessageAsync(string text, bool isTTS = false, Embed embed = null, RequestOptions options = null);
#if NETSTANDARD1_3
/// <summary> Sends a file to this text channel, with an optional caption. </summary> /// <summary> Sends a file to this text channel, with an optional caption. </summary>
Task<IUserMessage> SendFileAsync(string filePath, string text = null, bool isTTS = false, RequestOptions options = null); Task<IUserMessage> SendFileAsync(string filePath, string text = null, bool isTTS = false, RequestOptions options = null);
#endif
/// <summary> Sends a file to this text channel, with an optional caption. </summary> /// <summary> Sends a file to this text channel, with an optional caption. </summary>
Task<IUserMessage> SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, RequestOptions options = null); Task<IUserMessage> SendFileAsync(Stream stream, string filename, string text = null, bool isTTS = false, RequestOptions options = null);




+ 2
- 3
src/Discord.Net.Core/Entities/Channels/ITextChannel.cs View File

@@ -1,5 +1,4 @@
using Discord.API.Rest;
using System;
using System;
using System.Threading.Tasks; using System.Threading.Tasks;


namespace Discord namespace Discord
@@ -10,6 +9,6 @@ namespace Discord
string Topic { get; } string Topic { get; }


/// <summary> Modifies this text channel. </summary> /// <summary> Modifies this text channel. </summary>
Task ModifyAsync(Action<ModifyTextChannelParams> func, RequestOptions options = null);
Task ModifyAsync(Action<TextChannelProperties> func, RequestOptions options = null);
} }
} }

+ 4
- 5
src/Discord.Net.Core/Entities/Channels/IVoiceChannel.cs View File

@@ -1,5 +1,4 @@
using Discord.API.Rest;
using Discord.Audio;
using Discord.Audio;
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;


@@ -9,11 +8,11 @@ namespace Discord
{ {
/// <summary> Gets the bitrate, in bits per second, clients in this voice channel are requested to use. </summary> /// <summary> Gets the bitrate, in bits per second, clients in this voice channel are requested to use. </summary>
int Bitrate { get; } int Bitrate { get; }
/// <summary> Gets the max amount of users allowed to be connected to this channel at one time. A value of 0 represents no limit. </summary>
int UserLimit { get; }
/// <summary> Gets the max amount of users allowed to be connected to this channel at one time. </summary>
int? UserLimit { get; }


/// <summary> Modifies this voice channel. </summary> /// <summary> Modifies this voice channel. </summary>
Task ModifyAsync(Action<ModifyVoiceChannelParams> func, RequestOptions options = null);
Task ModifyAsync(Action<VoiceChannelProperties> func, RequestOptions options = null);
/// <summary> Connects to this voice channel. </summary> /// <summary> Connects to this voice channel. </summary>
Task<IAudioClient> ConnectAsync(); Task<IAudioClient> ConnectAsync();
} }

+ 11
- 0
src/Discord.Net.Core/Entities/Channels/TextChannelProperties.cs View File

@@ -0,0 +1,11 @@
namespace Discord
{
/// <inheritdoc />
public class TextChannelProperties : GuildChannelProperties
{
/// <summary>
/// What the topic of the channel should be set to.
/// </summary>
public Optional<string> Topic { get; set; }
}
}

+ 15
- 0
src/Discord.Net.Core/Entities/Channels/VoiceChannelProperties.cs View File

@@ -0,0 +1,15 @@
namespace Discord
{
/// <inheritdoc />
public class VoiceChannelProperties : GuildChannelProperties
{
/// <summary>
/// The bitrate of the voice connections in this channel. Must be greater than 8000
/// </summary>
public Optional<int> Bitrate { get; set; }
/// <summary>
/// The maximum number of users that can be present in a channel.
/// </summary>
public Optional<int?> UserLimit { get; set; }
}
}

+ 21
- 0
src/Discord.Net.Core/Entities/Guilds/GuildEmbedProperties.cs View File

@@ -0,0 +1,21 @@
namespace Discord
{
/// <summary>
/// Modify the widget of an IGuild with the specified parameters
/// </summary>
public class GuildEmbedProperties
{
/// <summary>
/// Should the widget be enabled?
/// </summary>
public Optional<bool> Enabled { get; set; }
/// <summary>
/// What channel should the invite place users in, if not null.
/// </summary>
public Optional<IChannel> Channel { get; set; }
/// <summary>
/// What channel should the invite place users in, if not null.
/// </summary>
public Optional<ulong?> ChannelId { get; set; }
}
}

+ 1
- 7
src/Discord.Net.Core/Entities/Guilds/GuildEmoji.cs View File

@@ -1,7 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics; using System.Diagnostics;
using Model = Discord.API.Emoji;


namespace Discord namespace Discord
{ {
@@ -14,7 +12,7 @@ namespace Discord
public bool RequireColons { get; } public bool RequireColons { get; }
public IReadOnlyList<ulong> RoleIds { get; } public IReadOnlyList<ulong> RoleIds { get; }


private GuildEmoji(ulong id, string name, bool isManaged, bool requireColons, IReadOnlyList<ulong> roleIds)
internal GuildEmoji(ulong id, string name, bool isManaged, bool requireColons, IReadOnlyList<ulong> roleIds)
{ {
Id = id; Id = id;
Name = name; Name = name;
@@ -22,10 +20,6 @@ namespace Discord
RequireColons = requireColons; RequireColons = requireColons;
RoleIds = roleIds; RoleIds = roleIds;
} }
internal static GuildEmoji Create(Model model)
{
return new GuildEmoji(model.Id, model.Name, model.Managed, model.RequireColons, ImmutableArray.Create(model.Roles));
}


public override string ToString() => Name; public override string ToString() => Name;
private string DebuggerDisplay => $"{Name} ({Id})"; private string DebuggerDisplay => $"{Name} ({Id})";


+ 9
- 0
src/Discord.Net.Core/Entities/Guilds/GuildIntegrationProperties.cs View File

@@ -0,0 +1,9 @@
namespace Discord
{
public class GuildIntegrationProperties
{
public Optional<int> ExpireBehavior { get; set; }
public Optional<int> ExpireGracePeriod { get; set; }
public Optional<bool> EnableEmoticons { get; set; }
}
}

+ 71
- 0
src/Discord.Net.Core/Entities/Guilds/GuildProperties.cs View File

@@ -0,0 +1,71 @@
namespace Discord
{
/// <summary>
/// Modify an IGuild with the specified changes
/// </summary>
/// <example>
/// <code language="c#">
/// await Context.Guild.ModifyAsync(async x =>
/// {
/// x.Name = "aaaaaah";
/// x.RegionId = (await Context.Client.GetOptimalVoiceRegionAsync()).Id;
/// });
/// </code>
/// </example>
/// <see cref="IGuild"/>
public class GuildProperties
{
public Optional<string> Username { get; set; }
/// <summary>
/// The name of the Guild
/// </summary>
public Optional<string> Name { get; set; }
/// <summary>
/// The region for the Guild's voice connections
/// </summary>
public Optional<IVoiceRegion> Region { get; set; }
/// <summary>
/// The ID of the region for the Guild's voice connections
/// </summary>
public Optional<string> RegionId { get; set; }
/// <summary>
/// What verification level new users need to achieve before speaking
/// </summary>
public Optional<VerificationLevel> VerificationLevel { get; set; }
/// <summary>
/// The default message notification state for the guild
/// </summary>
public Optional<DefaultMessageNotifications> DefaultMessageNotifications { get; set; }
/// <summary>
/// How many seconds before a user is sent to AFK. This value MUST be one of: (60, 300, 900, 1800, 3600).
/// </summary>
public Optional<int> AfkTimeout { get; set; }
/// <summary>
/// The icon of the guild
/// </summary>
public Optional<Image?> Icon { get; set; }
/// <summary>
/// The guild's splash image
/// </summary>
/// <remarks>
/// The guild must be partnered for this value to have any effect.
/// </remarks>
public Optional<Image?> Splash { get; set; }
/// <summary>
/// The IVoiceChannel where AFK users should be sent.
/// </summary>
public Optional<IVoiceChannel> AfkChannel { get; set; }
/// <summary>
/// The ID of the IVoiceChannel where AFK users should be sent.
/// </summary>
public Optional<ulong?> AfkChannelId { get; set; }
/// <summary>
/// The owner of this guild.
/// </summary>
public Optional<IUser> Owner { get; set; }
/// <summary>
/// The ID of the owner of this guild.
/// </summary>
public Optional<ulong> OwnerId { get; set; }
}
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save