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
project.lock.json
/test/Discord.Net.Tests/config.json
/test/Discord.Net.Tests/cache.db*
/docs/_build
*.pyc
/.editorconfig
.vscode/

+ 87
- 29
Discord.Net.sln View File

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

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.25914.0
VisualStudioVersion = 15.0.26014.0
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}"
EndProject
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
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.WebSocket", "src\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj", "{688FD1D8-7F01-4539-B2E9-F473C5D699C7}"
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
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -33,26 +39,14 @@ Global
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{496DB20A-A455-4D01-B6BC-90FE6D7C6B81}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{496DB20A-A455-4D01-B6BC-90FE6D7C6B81}.Debug|Any CPU.Build.0 = Debug|Any CPU
{496DB20A-A455-4D01-B6BC-90FE6D7C6B81}.Debug|x64.ActiveCfg = Debug|Any CPU
{496DB20A-A455-4D01-B6BC-90FE6D7C6B81}.Debug|x64.Build.0 = Debug|Any CPU
{496DB20A-A455-4D01-B6BC-90FE6D7C6B81}.Debug|x86.ActiveCfg = Debug|Any CPU
{496DB20A-A455-4D01-B6BC-90FE6D7C6B81}.Debug|x86.Build.0 = Debug|Any CPU
{496DB20A-A455-4D01-B6BC-90FE6D7C6B81}.Release|Any CPU.ActiveCfg = Debug|Any CPU
{496DB20A-A455-4D01-B6BC-90FE6D7C6B81}.Release|Any CPU.Build.0 = Debug|Any CPU
{496DB20A-A455-4D01-B6BC-90FE6D7C6B81}.Release|x64.ActiveCfg = Debug|Any CPU
{496DB20A-A455-4D01-B6BC-90FE6D7C6B81}.Release|x64.Build.0 = Debug|Any CPU
{496DB20A-A455-4D01-B6BC-90FE6D7C6B81}.Release|x86.ActiveCfg = Debug|Any CPU
{496DB20A-A455-4D01-B6BC-90FE6D7C6B81}.Release|x86.Build.0 = Debug|Any CPU
{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|x64.ActiveCfg = Debug|x64
{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|x64.Build.0 = Debug|x64
{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|x86.ActiveCfg = Debug|x86
{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|x86.Build.0 = Debug|x86
{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|Any CPU.ActiveCfg = Debug|Any CPU
{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|Any CPU.Build.0 = Debug|Any CPU
{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|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.Build.0 = Release|x64
{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|x86.ActiveCfg = Debug|Any CPU
{BFC6DC28-0351-4573-926A-D4124244C04F}.Debug|x86.Build.0 = Debug|Any CPU
{BFC6DC28-0351-4573-926A-D4124244C04F}.Release|Any CPU.ActiveCfg = Debug|Any CPU
{BFC6DC28-0351-4573-926A-D4124244C04F}.Release|Any CPU.Build.0 = Debug|Any CPU
{BFC6DC28-0351-4573-926A-D4124244C04F}.Release|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.Build.0 = 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|x86.ActiveCfg = Debug|Any CPU
{5688A353-121E-40A1-8BFA-B17B91FB48FB}.Debug|x86.Build.0 = Debug|Any CPU
{5688A353-121E-40A1-8BFA-B17B91FB48FB}.Release|Any CPU.ActiveCfg = Debug|Any CPU
{5688A353-121E-40A1-8BFA-B17B91FB48FB}.Release|Any CPU.Build.0 = Debug|Any CPU
{5688A353-121E-40A1-8BFA-B17B91FB48FB}.Release|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.Build.0 = 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|x86.ActiveCfg = Debug|Any CPU
{078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Debug|x86.Build.0 = Debug|Any CPU
{078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Release|Any CPU.ActiveCfg = Debug|Any CPU
{078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Release|Any CPU.Build.0 = Debug|Any CPU
{078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Release|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.Build.0 = 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|x86.ActiveCfg = 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
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -112,6 +166,10 @@ Global
GlobalSection(NestedProjects) = preSolution
{BFC6DC28-0351-4573-926A-D4124244C04F} = {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}
{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
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 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).

## Installation
### 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/)

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/)

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)
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
In order to compile Discord.Net, you require the following:

### Using Visual Studio
- [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.

### 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

### 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

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`.

+ 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
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.

[!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
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;
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
[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

[!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

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

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.

[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

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.

### 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

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.

### 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.

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

### 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`.

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]
>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)]

# 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.

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

## 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

To write your own preconditions, create a new class that inherits from
@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.

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.

[!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 allow you to parse different types of arguments in
Type Readers allow you to parse different types of arguments in
your commands.

By default, the following Types are supported arguments:

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

### 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

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

>[!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.

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`.

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

#### Sample

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

### 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.

[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]
>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).

>[!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

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)

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

>[!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 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 DiscordSocketClient client;
private DependencyMap map;

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

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

string token = "bot token here";

map = new DependencyMap();

await InstallCommands();

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

await Task.Delay(-1);
}

public async Task InstallCommands()
{
// Hook the MessageReceived Event into our Command Handler
client.MessageReceived += HandleCommand;
// 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)
{
@@ -41,16 +43,14 @@ public class Program
// Create a number to track where the prefix ends and the command begins
int argPos = 0;
// 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)
{
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.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
{
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)
{
var channel = _client.GetChannel(ChannelId) as ISocketMessageChannel;
var channel = _client.GetChannel(ChannelId) as SocketMessageChannel;
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. */
}

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

@@ -1,5 +1,5 @@
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.Commands;
using Discord.WebSocket;

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);
}

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")]
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")]
public async Task JoinChannel(IUserMessage msg,
IVoiceChannel channel = null)
public async Task JoinChannel(IVoiceChannel channel = null)
{
// Get the audio channel
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; }

// 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
{
// 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();
public async Task Start()
{
_client = new DiscordClient(new DiscordConfig() {
_client = new DiscordSocketClient(new DiscordSocketConfig() {
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
[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
await ReplyAsync(echo);
@@ -39,4 +39,4 @@ public class Sample : ModuleBase
var userInfo = user ?? Context.Client.CurrentUser;
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
public class RequireOwnerAttribute : PreconditionAttribute
{
// 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
var ownerId = (await map.Get<DiscordSocketClient>().GetApplicationInfoAsync()).Owner.Id;
@@ -15,4 +15,4 @@ public class RequireOwnerAttribute : PreconditionAttribute
else
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.Commands;



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

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

## 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

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 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)]

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

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
{
/// <summary> Provides aliases for a command. </summary>
[AttributeUsage(AttributeTargets.Method)]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AliasAttribute : Attribute
{
/// <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 string Text { get; }
public RunMode RunMode { get; set; } = RunMode.Sync;
public RunMode RunMode { get; set; } = RunMode.Default;

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)]
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
}

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



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

public RequirePermissionAttribute(GuildPermission permission)
/// <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;
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;
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;


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

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

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 Summary { get; set; }
@@ -34,7 +34,7 @@ namespace Discord.Commands.Builders
_aliases = new List<string>();
}
//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)
{
Discord.Preconditions.NotNull(primaryAlias, nameof(primaryAlias));
@@ -72,7 +72,12 @@ namespace Discord.Commands.Builders

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;
}
public CommandBuilder AddPrecondition(PreconditionAttribute precondition)
@@ -80,6 +85,13 @@ namespace Discord.Commands.Builders
_preconditions.Add(precondition);
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)
{
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;
}

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;
}
public ModuleBuilder AddPrecondition(PreconditionAttribute precondition)
@@ -68,7 +73,7 @@ namespace Discord.Commands.Builders
_preconditions.Add(precondition);
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);
createFunc(builder);
@@ -97,13 +102,17 @@ namespace Discord.Commands.Builders
return this;
}

public ModuleInfo Build(CommandService service)
private ModuleInfo BuildImpl(CommandService service, ModuleInfo parent = null)
{
//Default name to first alias
if (Name == null)
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
{
private static readonly TypeInfo _moduleTypeInfo = typeof(ModuleBase).GetTypeInfo();
private static readonly TypeInfo _moduleTypeInfo = typeof(IModuleBase).GetTypeInfo();

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(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 subGroups = validTypes.Intersect(topLevelGroups);
@@ -65,7 +65,8 @@ namespace Discord.Commands
if (builtTypes.Contains(typeInfo))
continue;
builder.AddModule((module) => {
builder.AddModule((module) =>
{
BuildModule(module, typeInfo, service);
BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service);
});
@@ -88,17 +89,20 @@ namespace Discord.Commands
else if (attribute is RemarksAttribute)
builder.Remarks = (attribute as RemarksAttribute).Text;
else if (attribute is AliasAttribute)
builder.AddAlias((attribute as AliasAttribute).Aliases);
builder.AddAliases((attribute as AliasAttribute).Aliases);
else if (attribute is GroupAttribute)
{
var groupAttr = attribute as GroupAttribute;
builder.Name = builder.Name ?? groupAttr.Prefix;
builder.AddAlias(groupAttr.Prefix);
builder.AddAliases(groupAttr.Prefix);
}
else if (attribute is PreconditionAttribute)
builder.AddPrecondition(attribute as PreconditionAttribute);
}

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

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

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

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

var parameters = method.GetParameters();
int pos = 0, count = parameters.Length;
foreach (var paramInfo in parameters)
{
builder.AddParameter((parameter) => {
builder.AddParameter((parameter) =>
{
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);
instance.Context = ctx;
instance.SetContext(ctx);
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();
}
};
@@ -179,6 +192,10 @@ namespace Discord.Commands
// TODO: C#7 type switch
if (attribute is SummaryAttribute)
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)
{
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
{
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)


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

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

using System.Collections.Generic;

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

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

public IReadOnlyList<ParameterPreconditionAttribute> Preconditions => _preconditions;

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

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

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

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

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

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


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

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

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)
{
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
}
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;
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
{
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;

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

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

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

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

@@ -190,26 +179,56 @@ namespace Discord.Commands
//Type Readers
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)
{
_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;
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;
}

//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;
}

//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)
return SearchResult.FromSuccess(input, matches);
@@ -217,9 +236,9 @@ namespace Discord.Commands
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);
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;

@@ -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.");
}
}


+ 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
{
private Dictionary<Type, object> map;
private Dictionary<Type, Func<object>> map;

public static DependencyMap Empty => new 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);
if (map.ContainsKey(t))
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>()
{
return (T)Get(typeof(T));
}
/// <inheritdoc />
public object Get(Type t)
{
object result;
@@ -35,6 +67,7 @@ namespace Discord.Commands
return result;
}

/// <inheritdoc />
public bool TryGet<T>(out T result)
{
object untypedResult;
@@ -49,9 +82,17 @@ namespace Discord.Commands
return false;
}
}
/// <inheritdoc />
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
{
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>();
/// <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);

/// <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);
/// <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);
}
}

+ 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>
<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>
<Authors>RogueException</Authors>
<Description>A Discord.Net extension adding support for bot commands.</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>
<PackageTargetFallback Condition=" '$(TargetFramework)' == 'netstandard1.3' ">$(PackageTargetFallback);dotnet5.4;dnxcore50;portable-net45+win8</PackageTargetFallback>
<RootNamespace>Discord.Commands</RootNamespace>
<DisableImplicitFrameworkReferences>true</DisableImplicitFrameworkReferences>
</PropertyGroup>
<ItemGroup>
<Compile Include="**\*.cs" />
<EmbeddedResource Include="**\*.resx" />
<EmbeddedResource Include="compiler\resources\**\*" />
</ItemGroup>
<ItemGroup />
<ItemGroup>
<ProjectReference Include="..\Discord.Net.Core\Discord.Net.Core.csproj" />
</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' ">
<DefineConstants>$(DefineConstants);RELEASE</DefineConstants>
<NoWarn>$(NoWarn);CS1573;CS1591</NoWarn>
<WarningsAsErrors>true</WarningsAsErrors>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</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 Discord.Commands.Builders;
using System.Diagnostics;

namespace Discord.Commands
{
[DebuggerDisplay("{Name,nq}")]
public class CommandInfo
{
private static readonly System.Reflection.MethodInfo _convertParamsMethod = typeof(CommandInfo).GetTypeInfo().GetDeclaredMethod(nameof(ConvertParamsList));
private static readonly ConcurrentDictionary<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 string Name { get; }
@@ -37,13 +39,21 @@ namespace Discord.Commands
Summary = builder.Summary;
Remarks = builder.Remarks;

RunMode = builder.RunMode;
RunMode = (builder.RunMode == RunMode.Default ? service._defaultRunMode : builder.RunMode);
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();

@@ -53,7 +63,7 @@ namespace Discord.Commands
_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)
map = DependencyMap.Empty;
@@ -74,30 +84,19 @@ namespace Discord.Commands

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)
return ParseResult.FromError(searchResult);
if (preconditionResult != null && !preconditionResult.Value.IsSuccess)
return ParseResult.FromError(preconditionResult.Value);
string input = searchResult.Text;
var matchingAliases = Aliases.Where(alias => input.StartsWith(alias));
string matchingAlias = "";
foreach (string alias in matchingAliases)
{
if (alias.Length > matchingAlias.Length)
matchingAlias = alias;
}
input = input.Substring(matchingAlias.Length);

string input = searchResult.Text.Substring(startIndex);
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)
return Task.FromResult(ExecuteResult.FromError(parseResult));
@@ -120,14 +119,22 @@ namespace Discord.Commands

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)
map = DependencyMap.Empty;

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)
{
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 IReadOnlyList<string> Aliases { get; }
public IEnumerable<CommandInfo> Commands { get; }
public IReadOnlyList<CommandInfo> Commands { get; }
public IReadOnlyList<PreconditionAttribute> Preconditions { 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;

Name = builder.Name;
Summary = builder.Summary;
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();

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;
}

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

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

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

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

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

using Discord.Commands.Builders;
@@ -10,6 +11,17 @@ namespace Discord.Commands
{
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)
{
Command = command;
@@ -23,19 +35,32 @@ namespace Discord.Commands
Type = builder.ParameterType;
DefaultValue = builder.DefaultValue;

Preconditions = builder.Preconditions.ToImmutableArray();

_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);
}


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

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

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

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)
{
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
{
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 string _name;
@@ -23,9 +23,9 @@ namespace Discord.Commands
_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;

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

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

CommandMapNode 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)
_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;
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);
else
name = text.Substring(index, nextSpace - index);

CommandMapNode nextNode;
name = text.Substring(index, nextSegment - index);
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;
}

//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;
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;
}


+ 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
{
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);
}

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
{
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>();
parserBuilder[typeof(bool)] = (TryParseDelegate<bool>)bool.TryParse;
@@ -27,16 +29,17 @@ namespace Discord.Commands
parserBuilder[typeof(decimal)] = (TryParseDelegate<decimal>)decimal.TryParse;
parserBuilder[typeof(DateTime)] = (TryParseDelegate<DateTime>)DateTime.TryParse;
parserBuilder[typeof(DateTimeOffset)] = (TryParseDelegate<DateTimeOffset>)DateTimeOffset.TryParse;
parserBuilder[typeof(TimeSpan)] = (TryParseDelegate<TimeSpan>)TimeSpan.TryParse;
parserBuilder[typeof(char)] = (TryParseDelegate<char>)char.TryParse;
parserBuilder[typeof(string)] = (TryParseDelegate<string>)delegate (string str, out string value)
{
value = str;
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
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)
{


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

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

public override Task<TypeReaderResult> Read(CommandContext context, string input)
public override Task<TypeReaderResult> Read(ICommandContext context, string input)
{
T baseValue;
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
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;



+ 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
where T : class, IRole
{
public override Task<TypeReaderResult> Read(CommandContext context, string input)
public override Task<TypeReaderResult> Read(ICommandContext context, string input)
{
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 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
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>();
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;
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;

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;
ParamValues = paramValue;
ParamValues = paramValues;
Error = error;
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 string Text { get; }
public IReadOnlyList<CommandInfo> Commands { get; }
public IReadOnlyList<CommandMatch> Commands { get; }

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

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;
Commands = commands;
@@ -22,7 +22,7 @@ namespace Discord.Commands
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);
public static SearchResult FromError(CommandError error, string reason)
=> new SearchResult(null, null, error, reason);


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

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


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

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

var constructor = constructors[0];
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) =>
{
@@ -27,28 +30,40 @@ namespace Discord.Commands
for (int i = 0; i < parameters.Length; 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
{
return (T)constructor.Invoke(args);
obj = (T)constructor.Invoke(args);
}
catch (Exception 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;

[assembly: InternalsVisibleTo("Discord.Net.Relay")]
[assembly: InternalsVisibleTo("Discord.Net.Rest")]
[assembly: InternalsVisibleTo("Discord.Net.Rpc")]
[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.IO;
using System.Threading.Tasks;

namespace Discord.Audio
{
public interface IAudioClient
public interface IAudioClient : IDisposable
{
event Func<Task> Connected;
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>
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 string GetApplicationIconUrl(ulong appId, string iconId)
=> 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)
=> iconId != null ? $"{DiscordConfig.CDNUrl}icons/{guildId}/{iconId}.jpg" : null;
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>
<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>
<Authors>RogueException</Authors>
<Description>A .Net API wrapper and bot framework for Discord.</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>
<PackageTargetFallback Condition=" '$(TargetFramework)' == 'netstandard1.3' ">$(PackageTargetFallback);dotnet5.4;dnxcore50;portable-net45+win8</PackageTargetFallback>
<RootNamespace>Discord</RootNamespace>
<DisableImplicitFrameworkReferences>true</DisableImplicitFrameworkReferences>
</PropertyGroup>
<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>
<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' ">
<DefineConstants>$(DefineConstants);RELEASE</DefineConstants>
<NoWarn>$(NoWarn);CS1573;CS1591</NoWarn>
<WarningsAsErrors>true</WarningsAsErrors>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</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 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>
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
{
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.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="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>
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>
Task<IReadOnlyCollection<IInviteMetadata>> GetInvitesAsync(RequestOptions options = null);
/// <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>
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
{
/// <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>
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>
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;

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

/// <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.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>
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>
Task ModifyAsync(Action<ModifyVoiceChannelParams> func, RequestOptions options = null);
Task ModifyAsync(Action<VoiceChannelProperties> func, RequestOptions options = null);
/// <summary> Connects to this voice channel. </summary>
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.Immutable;
using System.Diagnostics;
using Model = Discord.API.Emoji;

namespace Discord
{
@@ -14,7 +12,7 @@ namespace Discord
public bool RequireColons { 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;
Name = name;
@@ -22,10 +20,6 @@ namespace Discord
RequireColons = requireColons;
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;
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