Browse Source

Merged with recent changes

pull/745/head
ObsidianMinor 8 years ago
parent
commit
f7079dde48
100 changed files with 1603 additions and 763 deletions
  1. +4
    -1
      .gitignore
  2. +47
    -77
      Discord.Net.sln
  3. +31
    -0
      Discord.Net.targets
  4. +9
    -8
      README.md
  5. +61
    -0
      appveyor.yml
  6. +0
    -4
      build.ps1
  7. +36
    -5
      docs/CONTRIBUTING.md
  8. +0
    -1
      docs/api/.manifest
  9. +4
    -3
      docs/docfx.json
  10. +3
    -1
      docs/filterConfig.yml
  11. +14
    -11
      docs/guides/commands/commands.md
  12. +17
    -13
      docs/guides/commands/samples/command_handler.cs
  13. +4
    -5
      docs/guides/commands/samples/dependency_map_setup.cs
  14. +0
    -0
      docs/guides/commands/samples/dependency_module.cs
  15. +0
    -0
      docs/guides/commands/samples/empty-module.cs
  16. +0
    -0
      docs/guides/commands/samples/groups.cs
  17. +0
    -0
      docs/guides/commands/samples/module.cs
  18. +8
    -2
      docs/guides/commands/samples/require_owner.cs
  19. +0
    -0
      docs/guides/commands/samples/typereader.cs
  20. +58
    -0
      docs/guides/concepts/connections.md
  21. +68
    -0
      docs/guides/concepts/entities.md
  22. +84
    -0
      docs/guides/concepts/events.md
  23. +42
    -0
      docs/guides/concepts/logging.md
  24. +23
    -0
      docs/guides/concepts/samples/connections.cs
  25. +13
    -0
      docs/guides/concepts/samples/entities.cs
  26. +36
    -0
      docs/guides/concepts/samples/events.cs
  27. +29
    -0
      docs/guides/concepts/samples/logging.cs
  28. +0
    -28
      docs/guides/events.md
  29. BIN
      docs/guides/getting_started/images/install-rider-add.png
  30. BIN
      docs/guides/getting_started/images/install-rider-nuget-manager.png
  31. BIN
      docs/guides/getting_started/images/install-rider-search.png
  32. BIN
      docs/guides/getting_started/images/install-vs-deps.png
  33. BIN
      docs/guides/getting_started/images/install-vs-nuget.png
  34. BIN
      docs/guides/getting_started/images/intro-add-bot.png
  35. BIN
      docs/guides/getting_started/images/intro-client-id.png
  36. BIN
      docs/guides/getting_started/images/intro-create-app.png
  37. BIN
      docs/guides/getting_started/images/intro-create-bot.png
  38. BIN
      docs/guides/getting_started/images/intro-token.png
  39. +141
    -0
      docs/guides/getting_started/installing.md
  40. +227
    -0
      docs/guides/getting_started/intro.md
  41. +15
    -0
      docs/guides/getting_started/samples/intro/async-context.cs
  42. +16
    -0
      docs/guides/getting_started/samples/intro/client.cs
  43. +42
    -0
      docs/guides/getting_started/samples/intro/complete.cs
  44. +22
    -0
      docs/guides/getting_started/samples/intro/logging.cs
  45. +14
    -0
      docs/guides/getting_started/samples/intro/message.cs
  46. +32
    -16
      docs/guides/getting_started/samples/intro/structure.cs
  47. +9
    -0
      docs/guides/getting_started/samples/netstd11.cs
  48. +6
    -0
      docs/guides/getting_started/samples/nuget.config
  49. +13
    -0
      docs/guides/getting_started/samples/project.csproj
  50. +0
    -0
      docs/guides/getting_started/terminology.md
  51. +0
    -50
      docs/guides/intro.md
  52. +0
    -11
      docs/guides/logging.md
  53. +2
    -2
      docs/guides/migrating/migrating.md
  54. +0
    -0
      docs/guides/migrating/samples/event.cs
  55. +0
    -0
      docs/guides/migrating/samples/sync_event.cs
  56. +0
    -20
      docs/guides/samples.md
  57. +0
    -5
      docs/guides/samples/faq/avatar.cs
  58. +0
    -6
      docs/guides/samples/faq/send_message.cs
  59. +0
    -5
      docs/guides/samples/faq/status.cs
  60. +0
    -28
      docs/guides/samples/logging.cs
  61. +25
    -13
      docs/guides/toc.yml
  62. +0
    -0
      docs/guides/voice/samples/audio_create_ffmpeg.cs
  63. +2
    -2
      docs/guides/voice/samples/audio_ffmpeg.cs
  64. +0
    -0
      docs/guides/voice/samples/joining_audio.cs
  65. +6
    -16
      docs/guides/voice/sending-voice.md
  66. +1
    -1
      docs/index.md
  67. +0
    -2
      docs/toc.yml
  68. +0
    -0
      experiment/Discord.Net.Analyzers/AssemblyInfo.cs
  69. +0
    -0
      experiment/Discord.Net.Analyzers/ConfigureAwaitAnalyzer.cs
  70. +18
    -0
      experiment/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj
  71. +0
    -0
      experiment/Discord.Net.Relay/ApplicationBuilderExtensions.cs
  72. +0
    -0
      experiment/Discord.Net.Relay/AssemblyInfo.cs
  73. +18
    -0
      experiment/Discord.Net.Relay/Discord.Net.Relay.csproj
  74. +0
    -0
      experiment/Discord.Net.Relay/RelayConnection.cs
  75. +0
    -0
      experiment/Discord.Net.Relay/RelayServer.cs
  76. +0
    -17
      pack.ps1
  77. +0
    -32
      src/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj
  78. +2
    -1
      src/Discord.Net.Commands/Attributes/ParameterPreconditionAttribute.cs
  79. +8
    -1
      src/Discord.Net.Commands/Attributes/PreconditionAttribute.cs
  80. +7
    -4
      src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs
  81. +2
    -1
      src/Discord.Net.Commands/Attributes/Preconditions/RequireContextAttribute.cs
  82. +20
    -0
      src/Discord.Net.Commands/Attributes/Preconditions/RequireNsfwAttribute.cs
  83. +16
    -4
      src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs
  84. +4
    -3
      src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs
  85. +18
    -7
      src/Discord.Net.Commands/Builders/CommandBuilder.cs
  86. +11
    -2
      src/Discord.Net.Commands/Builders/ModuleBuilder.cs
  87. +125
    -69
      src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs
  88. +11
    -3
      src/Discord.Net.Commands/Builders/ParameterBuilder.cs
  89. +4
    -1
      src/Discord.Net.Commands/CommandError.cs
  90. +17
    -0
      src/Discord.Net.Commands/CommandException.cs
  91. +11
    -9
      src/Discord.Net.Commands/CommandMatch.cs
  92. +5
    -4
      src/Discord.Net.Commands/CommandParser.cs
  93. +110
    -58
      src/Discord.Net.Commands/CommandService.cs
  94. +7
    -1
      src/Discord.Net.Commands/CommandServiceConfig.cs
  95. +0
    -98
      src/Discord.Net.Commands/Dependencies/DependencyMap.cs
  96. +0
    -89
      src/Discord.Net.Commands/Dependencies/IDependencyMap.cs
  97. +7
    -18
      src/Discord.Net.Commands/Discord.Net.Commands.csproj
  98. +11
    -0
      src/Discord.Net.Commands/EmptyServiceProvider.cs
  99. +5
    -3
      src/Discord.Net.Commands/Extensions/MessageExtensions.cs
  100. +2
    -2
      src/Discord.Net.Commands/IModuleBase.cs

+ 4
- 1
.gitignore View File

@@ -202,4 +202,7 @@ project.lock.json
/docs/_build
*.pyc
/.editorconfig
.vscode/
.vscode/
docs/api/\.manifest

\.idea/

+ 47
- 77
Discord.Net.sln View File

@@ -1,7 +1,6 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26014.0
VisualStudioVersion = 15.0.26228.4
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Core", "src\Discord.Net.Core\Discord.Net.Core.csproj", "{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}"
EndProject
@@ -19,15 +18,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Providers", "Providers", "{
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}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Webhook", "src\Discord.Net.Webhook\Discord.Net.Webhook.csproj", "{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -41,16 +36,16 @@ Global
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{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}.Debug|x64.ActiveCfg = Debug|Any CPU
{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|x64.Build.0 = Debug|Any CPU
{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|x86.ActiveCfg = Debug|Any CPU
{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Debug|x86.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
{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|x86.Build.0 = Release|x86
{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|x64.ActiveCfg = Release|Any CPU
{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|x64.Build.0 = Release|Any CPU
{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|x86.ActiveCfg = Release|Any CPU
{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}.Release|x86.Build.0 = Release|Any CPU
{BFC6DC28-0351-4573-926A-D4124244C04F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BFC6DC28-0351-4573-926A-D4124244C04F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BFC6DC28-0351-4573-926A-D4124244C04F}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -89,76 +84,52 @@ Global
{078DD7E6-943D-4D09-AFC2-D2BA58B76C9C}.Release|x86.Build.0 = Debug|Any CPU
{688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Debug|x64.ActiveCfg = Debug|x64
{688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Debug|x64.Build.0 = Debug|x64
{688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Debug|x86.ActiveCfg = Debug|x86
{688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Debug|x86.Build.0 = Debug|x86
{688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Debug|x64.ActiveCfg = Debug|Any CPU
{688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Debug|x64.Build.0 = Debug|Any CPU
{688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Debug|x86.ActiveCfg = Debug|Any CPU
{688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Debug|x86.Build.0 = Debug|Any CPU
{688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Release|Any CPU.Build.0 = Release|Any CPU
{688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Release|x64.ActiveCfg = Release|x64
{688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Release|x64.Build.0 = Release|x64
{688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Release|x86.ActiveCfg = Release|x86
{688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Release|x86.Build.0 = Release|x86
{688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Release|x64.ActiveCfg = Release|Any CPU
{688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Release|x64.Build.0 = Release|Any CPU
{688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Release|x86.ActiveCfg = Release|Any CPU
{688FD1D8-7F01-4539-B2E9-F473C5D699C7}.Release|x86.Build.0 = Release|Any CPU
{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}.Debug|x64.ActiveCfg = Debug|Any CPU
{6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Debug|x64.Build.0 = Debug|Any CPU
{6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Debug|x86.ActiveCfg = Debug|Any CPU
{6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Debug|x86.Build.0 = Debug|Any CPU
{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
{6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Release|x64.ActiveCfg = Release|Any CPU
{6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Release|x64.Build.0 = Release|Any CPU
{6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Release|x86.ActiveCfg = Release|Any CPU
{6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7}.Release|x86.Build.0 = Release|Any CPU
{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}.Debug|x64.ActiveCfg = Debug|Any CPU
{C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Debug|x64.Build.0 = Debug|Any CPU
{C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Debug|x86.ActiveCfg = Debug|Any CPU
{C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Debug|x86.Build.0 = Debug|Any CPU
{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
{C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|x64.ActiveCfg = Release|Any CPU
{C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|x64.Build.0 = Release|Any CPU
{C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|x86.ActiveCfg = Release|Any CPU
{C38E5BC1-11CB-4101-8A38-5B40A1BC6433}.Release|x86.Build.0 = Release|Any CPU
{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Debug|x64.ActiveCfg = Debug|Any CPU
{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Debug|x64.Build.0 = Debug|Any CPU
{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Debug|x86.ActiveCfg = Debug|Any CPU
{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Debug|x86.Build.0 = Debug|Any CPU
{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|Any CPU.Build.0 = Release|Any CPU
{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|x64.ActiveCfg = Release|Any CPU
{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|x64.Build.0 = Release|Any CPU
{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|x86.ActiveCfg = Release|Any CPU
{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -169,7 +140,6 @@ Global
{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}
{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A}
EndGlobalSection
EndGlobal

+ 31
- 0
Discord.Net.targets View File

@@ -0,0 +1,31 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VersionPrefix>1.0.1</VersionPrefix>
<VersionSuffix></VersionSuffix>
<Authors>RogueException</Authors>
<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>
</PropertyGroup>
<PropertyGroup Condition=" '$(BuildNumber)' == '' ">
<VersionSuffix Condition=" '$(VersionSuffix)' != ''">$(VersionSuffix)-dev</VersionSuffix>
<VersionSuffix Condition=" '$(VersionSuffix)' == ''">dev</VersionSuffix>
</PropertyGroup>
<PropertyGroup Condition=" '$(BuildNumber)' != '' And $(IsTagBuild) != 'true' ">
<VersionSuffix Condition=" '$(VersionSuffix)' != '' ">$(VersionSuffix)-$(BuildNumber)</VersionSuffix>
<VersionSuffix Condition=" '$(VersionSuffix)' == '' ">build-$(BuildNumber)</VersionSuffix>
</PropertyGroup>
<PropertyGroup Condition=" '$(TargetFramework)' == 'netstandard1.3' Or '$(TargetFramework)' == 'net45' ">
<DefineConstants>$(DefineConstants);FILESYSTEM;DEFAULTUDPCLIENT;DEFAULTWEBSOCKET</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(TargetFramework)' == 'netstandard1.3' ">
<DefineConstants>$(DefineConstants);FORMATSTR;UNIXTIME;MSTRYBUFFER;UDPDISPOSE</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<NoWarn>$(NoWarn);CS1573;CS1591</NoWarn>
<WarningsAsErrors>true</WarningsAsErrors>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
</Project>

+ 9
- 8
README.md View File

@@ -1,4 +1,5 @@
# Discord.Net v1.0.0-rc
# Discord.Net
[![NuGet](https://img.shields.io/nuget/vpre/Discord.Net.svg?maxAge=2592000?style=plastic)](https://www.nuget.org/packages/Discord.Net)
[![MyGet](https://img.shields.io/myget/discord-net/vpre/Discord.Net.svg)](https://www.myget.org/feed/Packages/discord-net)
[![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)
@@ -13,13 +14,13 @@ 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.Commands](https://www.nuget.org/packages/Discord.Net.Commands/)
- [Discord.Net.Rest](https://www.nuget.org/packages/Discord.Net.Rest/)
- [Discord.Net.Rpc](https://www.nuget.org/packages/Discord.Net.Rpc/)
- [Discord.Net.WebSocket](https://www.nuget.org/packages/Discord.Net.WebSocket/)
- [Discord.Net.Commands](https://www.nuget.org/packages/Discord.Net.Commands/)
- [Discord.Net.Webhook](https://www.nuget.org/packages/Discord.Net.Webhook/)

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/)
The following provider is available for platforms not supporting .NET Standard 1.3:
- [Discord.Net.Providers.WS4Net](https://www.nuget.org/packages/Discord.Net.Providers.WS4Net/)

### Unstable (MyGet)
@@ -29,13 +30,13 @@ Nightly builds are available through our MyGet feed (`https://www.myget.org/F/di
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)
- [Visual Studio 2017](https://www.microsoft.com/net/core#windowsvs2017)
- [.NET Core SDK](https://www.microsoft.com/net/download/core)

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

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

## Known Issues



+ 61
- 0
appveyor.yml View File

@@ -0,0 +1,61 @@
version: build-{build}
branches:
only:
- dev
image: Visual Studio 2017

nuget:
disable_publish_on_pr: true
pull_requests:
do_not_increment_build_number: true
clone_folder: C:\Projects\Discord.Net
cache: test/Discord.Net.Tests/cache.db

environment:
DOTNET_CLI_TELEMETRY_OPTOUT: 1
DNET_TEST_TOKEN:
secure: l7h5e7UE7yRd70hAB97kjPiQpPOShwqoBbOzEAYQ+XBd/Pre5OA33IXa3uisdUeQJP/nPFhcOsI+yn7WpuFaoQ==
DNET_TEST_GUILDID: 273160668180381696
init:
- ps: $Env:BUILD = "$($Env:APPVEYOR_BUILD_NUMBER.PadLeft(5, "0"))"

build_script:
- ps: appveyor-retry dotnet restore Discord.Net.sln -v Minimal /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG"
- ps: dotnet build Discord.Net.sln -c "Release" /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG"
after_build:
- ps: dotnet pack "src\Discord.Net.Core\Discord.Net.Core.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG"
- ps: dotnet pack "src\Discord.Net.Rest\Discord.Net.Rest.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG"
- ps: dotnet pack "src\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG"
- ps: dotnet pack "src\Discord.Net.Rpc\Discord.Net.Rpc.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG"
- ps: dotnet pack "src\Discord.Net.Commands\Discord.Net.Commands.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG"
- ps: dotnet pack "src\Discord.Net.Webhook\Discord.Net.Webhook.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG"
- ps: dotnet pack "src\Discord.Net.Providers.WS4Net\Discord.Net.Providers.WS4Net.csproj" -c "Release" -o "../../artifacts" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG"
- ps: >-
if ($Env:APPVEYOR_REPO_TAG -eq "true") {
nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix=""
} else {
nuget pack src\Discord.Net\Discord.Net.nuspec -OutputDirectory "artifacts" -properties suffix="-build-$Env:BUILD"
}
- ps: Get-ChildItem artifacts\*.nupkg | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name }

test_script:
- ps: >-
if ($APPVEYOR_PULL_REQUEST_NUMBER -eq "") {
dotnet test test/Discord.Net.Tests/Discord.Net.Tests.csproj -c "Release" --no-build /p:BuildNumber="$Env:BUILD" /p:IsTagBuild="$Env:APPVEYOR_REPO_TAG"
}

deploy:
- provider: NuGet
server: https://www.myget.org/F/discord-net/api/v2/package
api_key:
secure: Jl7BXeUjRnkVHDMBuUWSXcEOkrli1PBleW2IiLyUs5j63UNUNp1hcjaUJRujx9lz
symbol_server: https://www.myget.org/F/discord-net/symbols/api/v2/package
on:
branch: dev
- provider: NuGet
server: https://www.myget.org/F/rogueexception/api/v2/package
api_key:
secure: D+vW2O2LBf/iJb4f+q8fkyIW2VdIYIGxSYLWNrOD4BHlDBZQlJipDbNarWjUr2Kn
symbol_server: https://www.myget.org/F/rogueexception/symbols/api/v2/package
on:
branch: dev

+ 0
- 4
build.ps1 View File

@@ -1,4 +0,0 @@
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) }

+ 36
- 5
docs/CONTRIBUTING.md View File

@@ -1,15 +1,46 @@
# Contributing to Docs

I don't really have any strict conditions for writing documentation, but just keep these few guidelines in mind:
I don't really have any strict conditions for writing documentation,
but just keep these few guidelines in mind:

* Keep code samples in the `guides/samples` folder
* When referencing an object in the API, link to it's page in the API documentation.
* When referencing an object in the API, link to it's page in the
API documentation.
* Documentation should be written in clear and proper English*

\* If anyone is interested in translating documentation into other languages, please open an issue or contact me on Discord (`foxbot#0282`).
\* If anyone is interested in translating documentation into other
languages, please open an issue or contact me on
Discord (`foxbot#0282`).

### Layout

Documentation should be written in a FAQ/Wiki style format.

Recommended reads:

* http://docs.microsoft.com
* http://flask.pocoo.org/docs/0.12/

Style consistencies:

* Use a ruler set at 70 characters
* Links should use long syntax
* Pages should be short and concise, not broad and long

Example of long link syntax:

```
Please consult the [API Documentation] for more information.

[API Documentation]: xref:System.String
```

### Compiling

Documentation is compiled into a static site using [DocFx](https://dotnet.github.io/docfx/). We currently use version 2.8
Documentation is compiled into a static site using [DocFx].
We currently use the most recent build off the dev branch.

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

After making changes, compile your changes into the static site with `docfx`. You can also view your changes live with `docfx --serve`.
[DocFx]: https://dotnet.github.io/docfx/

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


+ 4
- 3
docs/docfx.json View File

@@ -5,7 +5,7 @@
{
"src": "..",
"files": [
"src/**project.json"
"src/**/*.cs"
],
"exclude": [
"**/obj/**",
@@ -42,7 +42,8 @@
"resource": [
{
"files": [
"images/**"
"**/images/**",
"**/samples/**"
],
"exclude": [
"obj/**",
@@ -66,7 +67,7 @@
"default"
],
"globalMetadata": {
"_appFooter": "Discord.Net (c) 2015-2016"
"_appFooter": "Discord.Net (c) 2015-2017"
},
"noLangKeyword": false
}

+ 3
- 1
docs/filterConfig.yml View File

@@ -6,4 +6,6 @@ apiRules:
- exclude:
uidRegex: ^Discord\.Net\.Converters$
- exclude:
uidRegex: ^Discord\.Net.*$
uidRegex: ^Discord\.Net.*$
- exclude:
uidRegex: ^RegexAnalyzer$

docs/guides/commands.md → docs/guides/commands/commands.md View File

@@ -1,5 +1,9 @@
# The Command Service

>[!WARNING]
>This article is out of date, and has not been rewritten yet.
Information is not guaranteed to be accurate.

[Discord.Commands](xref:Discord.Commands) provides an Attribute-based
Command Parser.

@@ -41,7 +45,7 @@ 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 invoked.

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

@@ -163,8 +167,8 @@ a dependency map.

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.
@System.IServiceProvider. Alternatively, you may accept an
IServiceProvider as an argument and extract services yourself.

### Module Properties

@@ -201,21 +205,20 @@ 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 @System.IServiceProvider
You may create your own IServiceProvider if you wish.

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

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)]
[!code-csharp[IServiceProvider Setup](samples/dependency_map_setup.cs)]

### Usage in Modules

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

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

@@ -224,12 +227,12 @@ Any publicly settable properties will also be filled in the same manner.
being injected.

>[!NOTE]
>If you accept `CommandService` or `IDependencyMap` as a parameter in
>If you accept `CommandService` or `IServiceProvider` 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
by the CommandService the module was loaded from, and the ServiceProvider passed
into it, respectively.

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

# Preconditions


docs/guides/samples/command_handler.cs → docs/guides/commands/samples/command_handler.cs View File

@@ -1,14 +1,16 @@
using System;
using System.Threading.Tasks;
using System.Reflection;
using Discord;
using Discord.WebSocket;
using Discord.Commands;
using Microsoft.Extensions.DependencyInjection;

public class Program
{
private CommandService commands;
private DiscordSocketClient client;
private DependencyMap map;
private IServiceProvider services;

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

@@ -19,38 +21,40 @@ public class Program

string token = "bot token here";

map = new DependencyMap();
services = new ServiceCollection()
.BuildServiceProvider();

await InstallCommands();

await client.LoginAsync(TokenType.Bot, token);
await client.ConnectAsync();
await client.StartAsync();

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.
// Discover all of the commands in this assembly and load them.
await commands.AddModulesAsync(Assembly.GetEntryAssembly());
}

public async Task HandleCommand(SocketMessage messageParam)
{
{
// Don't process the command if it was a System Message
var message = messageParam as SocketUserMessage;
if (message == null) return;
// 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))) return;
// 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))) 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);
// rather an object stating if the command executed successfully)
var result = await commands.ExecuteAsync(context, argPos, service);
if (!result.IsSuccess)
await context.Channel.SendMessageAsync(result.ErrorReason);
}

}
}

docs/guides/samples/dependency_map_setup.cs → docs/guides/commands/samples/dependency_map_setup.cs View File

@@ -7,12 +7,11 @@ public class Commands
{
public async Task Install(DiscordSocketClient client)
{
// Here, we will inject the Dependency Map with
// Here, we will inject the ServiceProvider 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));
_serviceCollection.AddSingleton(client)
_serviceCollection.AddSingleton(new NotificationService())
_serviceCollection.AddSingleton(new DatabaseService())
// ...
await _commands.AddModulesAsync(Assembly.GetEntryAssembly());
}

docs/guides/samples/dependency_module.cs → docs/guides/commands/samples/dependency_module.cs View File


docs/guides/samples/empty-module.cs → docs/guides/commands/samples/empty-module.cs View File


docs/guides/samples/groups.cs → docs/guides/commands/samples/groups.cs View File


docs/guides/samples/module.cs → docs/guides/commands/samples/module.cs View File


docs/guides/samples/require_owner.cs → docs/guides/commands/samples/require_owner.cs View File

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

using Discord.Commands;
using Discord.WebSocket;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Threading.Tasks;

// Inherit from PreconditionAttribute
public class RequireOwnerAttribute : PreconditionAttribute
{
// Override the CheckPermissions method
public async override Task<PreconditionResult> CheckPermissions(CommandContext context, CommandInfo command, IDependencyMap map)
public async override Task<PreconditionResult> CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services)
{
// Get the ID of the bot's owner
var ownerId = (await map.Get<DiscordSocketClient>().GetApplicationInfoAsync()).Owner.Id;
var ownerId = (await services.GetService<DiscordSocketClient>().GetApplicationInfoAsync()).Owner.Id;
// If this command was executed by that user, return a success
if (context.User.Id == ownerId)
return PreconditionResult.FromSuccess();

docs/guides/samples/typereader.cs → docs/guides/commands/samples/typereader.cs View File


+ 58
- 0
docs/guides/concepts/connections.md View File

@@ -0,0 +1,58 @@
---
title: Managing Connections
---

In Discord.Net, once a client has been started, it will automatically
maintain a connection to Discord's gateway, until it is manually
stopped.

### Usage

To start a connection, invoke the `StartAsync` method on a client that
supports a WebSocket connection.

These clients include the [DiscordSocketClient] and
[DiscordRpcClient], as well as Audio clients.

To end a connection, invoke the `StopAsync` method. This will
gracefully close any open WebSocket or UdpSocket connections.

Since the Start/Stop methods only signal to an underlying connection
manager that a connection needs to be started, **they return before a
connection is actually made.**

As a result, you will need to hook into one of the connection-state
based events to have an accurate representation of when a client is
ready for use.

All clients provide a `Connected` and `Disconnected` event, which is
raised respectively when a connection opens or closes. In the case of
the DiscordSocketClient, this does **not** mean that the client is
ready to be used.

A separate event, `Ready`, is provided on DiscordSocketClient, which
is raised only when the client has finished guild stream or guild
sync, and has a complete guild cache.

[DiscordSocketClient]: xref:Discord.WebSocket.DiscordSocketClient
[DiscordRpcClient]: xref:Discord.Rpc.DiscordRpcClient

### Samples

[!code-csharp[Connection Sample](samples/events.cs)]

### Tips

Avoid running long-running code on the gateway! If you deadlock the
gateway (as explained in [events]), the connection manager will be
unable to recover and reconnect.

Assuming the client disconnected because of a fault on Discord's end,
and not a deadlock on your end, we will always attempt to reconnect
and resume a connection.

Don't worry about trying to maintain your own connections, the
connection manager is designed to be bulletproof and never fail - if
your client doesn't manage to reconnect, you've found a bug!

[events]: events.md

+ 68
- 0
docs/guides/concepts/entities.md View File

@@ -0,0 +1,68 @@
---
title: Entities
---

>[!NOTE]
This article is written with the Socket variants of entities in mind,
not the general interfaces or Rest/Rpc entities.

Discord.Net provides a versatile entity system for navigating the
Discord API.

### Inheritance

Due to the nature of the Discord API, some entities are designed with
multiple variants, for example, `SocketUser` and `SocketGuildUser`.

All models will contain the most detailed version of an entity
possible, even if the type is less detailed.

For example, in the case of the `MessageReceived` event, a
`SocketMessage` is passed in with a channel property of type
`SocketMessageChannel`. All messages come from channels capable of
messaging, so this is the only variant of a channel that can cover
every single case.

But that doesn't mean a message _can't_ come from a
`SocketTextChannel`, which is a message channel in a guild. To
retrieve information about a guild from a message entity, you will
need to cast its channel object to a `SocketTextChannel`.

### Navigation

All socket entities have navigation properties on them, which allow
you to easily navigate to an entity's parent or children. As explained
above, you will sometimes need to cast to a more detailed version of
an entity to navigate to its parent.

### Accessing Entities

The most basic forms of entities, `SocketGuild`, `SocketUser`, and
`SocketChannel` can be pulled from the DiscordSocketClient's global
cache, and can be retrieved using the respective `GetXXX` method on
DiscordSocketClient.

>[!TIP]
It is **vital** that you use the proper IDs for an entity when using
a GetXXX method. It is recommended that you enable Discord's
_developer mode_ to allow easy access to entity IDs, found in
Settings > Appearance > Advanced

More detailed versions of entities can be pulled from the basic
entities, e.g. `SocketGuild.GetUser`, which returns a
`SocketGuildUser`, or `SocketGuild.GetChannel`, which returns a
`SocketGuildChannel`. Again, you may need to cast these objects to get
a variant of the type that you need.

### Samples

[!code-csharp[Entity Sample](samples/entities.cs)]

### Tips

Avoid using boxing-casts to coerce entities into a variant, use the
`as` keyword, and a null-conditional operator.

This allows you to write safer code, and avoid InvalidCastExceptions.

For example, `(message.Author as SocketGuildUser)?.Nickname`.

+ 84
- 0
docs/guides/concepts/events.md View File

@@ -0,0 +1,84 @@
---
title: Working with Events
---

Events in Discord.Net are consumed in a similar manner to the standard
convention, with the exception that every event must be of the type
`System.Threading.Tasks.Task`, and instead of using EventArgs, the
event's parameters are passed directly into the handler.

This allows for events to be handled in an async context directly,
instead of relying on async void.

### Usage

To receive data from an event, hook into it using C#'s delegate
event pattern.

You may opt either to hook an event to an anonymous function (lambda)
or a named function.

### Safety

All events are designed to be thread-safe, in that events are executed
synchronously off the gateway task, in the same context as the gateway
task.

As a side effect, this makes it possible to deadlock the gateway task,
and kill a connection. As a general rule of thumb, any task that takes
longer than three seconds should **not** be awaited directly in the
context of an event, but should be wrapped in a `Task.Run` or
offloaded to another task.

This also means that you should not await a task that requests data
from Discord's gateway in the same context of an event. Since the
gateway will wait on all invoked event handlers to finish before
processing any additional data from the gateway, this will create
a deadlock that will be impossible to recover from.

Exceptions in commands will be swallowed by the gateway and logged out
through the client's log method.

### Common Patterns

As you may know, events in Discord.Net are only given a signature of
`Func<T1, ..., Task>`. There is no room for predefined argument names,
so you must either consult IntelliSense, or view the API documentation
directly.

That being said, there are a variety of common patterns that allow you
to infer what the parameters in an event mean.

#### Entity, Entity

An event handler with a signature of `Func<Entity, Entity, Task>`
typically means that the first object will be a clone of the entity
_before_ a change was made, and the latter object will be an attached
model of the entity _after_ the change was made.

This pattern is typically only found on `EntityUpdated` events.

#### Cacheable

An event handler with a signature of `Func<Cacheable, Entity, Task>`
means that the `before` state of the entity was not provided by the
API, so it can either be pulled from the client's cache, or
downloaded from the API.

See the documentation for [Cacheable] for more information on this
object.

[Cacheable]: xref:Discord.Cacheable`2

### Samples

[!code-csharp[Event Sample](samples/events.cs)]

### Tips

Many events relating to a Message entity, e.g. `MessageUpdated`
and `ReactionAdded` rely on the client's message cache, which is
**not** enabled by default. Set the `MessageCacheSize` flag in
[DiscordSocketConfig] to enable it.

[DiscordSocketConfig]: xref:Discord.WebSocket.DiscordSocketConfig

+ 42
- 0
docs/guides/concepts/logging.md View File

@@ -0,0 +1,42 @@
---
title: Logging
---

Discord.Net's clients provide a [Log] event that all messages will be
disbatched over.

For more information about events in Discord.Net, see the [Events]
section.

[Log]: xref:Discord.Rest.BaseDiscordClient#Discord_Rest_BaseDiscordClient_Log
[Events]: events.md

### Usage

To receive log events, simply hook the discord client's log method
to a Task with a single parameter of type [LogMessage]

It is recommended that you use an established function instead of a
lambda for handling logs, because most [addons] accept a reference
to a logging function to write their own messages.

### Usage in Commands

Discord.Net's [CommandService] also provides a log event, identical
in signature to other log events.

Data logged through this event is typically coupled with a
[CommandException], where information about the command's context
and error can be found and handled.

#### Samples

[!code-csharp[Logging Sample](samples/logging.cs)]

#### Tips

Due to the nature of Discord.Net's event system, all log event
handlers will be executed synchronously on the gateway thread. If your
log output will be dumped to a Web API (e.g. Sentry), you are advised
to wrap your output in a `Task.Run` so the gateway thread does not
become blocked while waiting for logging data to be written.

+ 23
- 0
docs/guides/concepts/samples/connections.cs View File

@@ -0,0 +1,23 @@
using Discord;
using Discord.WebSocket;

public class Program
{
private DiscordSocketClient _client;
static void Main(string[] args) => new Program().MainAsync().GetAwaiter().GetResult();
public async Task MainAsync()
{
_client = new DiscordSocketClient();

await _client.LoginAsync(TokenType.Bot, "bot token");
await _client.StartAsync();

Console.WriteLine("Press any key to exit...");
Console.ReadKey();

await _client.StopAsync();
// Wait a little for the client to finish disconnecting before allowing the program to return
await Task.Delay(500);
}
}

+ 13
- 0
docs/guides/concepts/samples/entities.cs View File

@@ -0,0 +1,13 @@
public string GetChannelTopic(ulong id)
{
var channel = client.GetChannel(81384956881809408) as SocketTextChannel;
if (channel == null) return "";
return channel.Topic;
}

public string GuildOwner(SocketChannel channel)
{
var guild = (channel as SocketGuildChannel)?.Guild;
if (guild == null) return "";
return Context.Guild.Owner.Username;
}

+ 36
- 0
docs/guides/concepts/samples/events.cs View File

@@ -0,0 +1,36 @@
using Discord;
using Discord.WebSocket;

public class Program
{
private DiscordSocketClient _client;
static void Main(string[] args) => new Program().MainAsync().GetAwaiter().GetResult();
public async Task MainAsync()
{
// When working with events that have Cacheable<IMessage, ulong> parameters,
// you must enable the message cache in your config settings if you plan to
// use the cached message entity.
var _config = new DiscordSocketConfig { MessageCacheSize = 100 };
_client = new DiscordSocketClient(_config);

await _client.LoginAsync(TokenType.Bot, "bot token");
await _client.StartAsync();

_client.MessageUpdated += MessageUpdated;
_client.Ready += () =>
{
Console.WriteLine("Bot is connected!");
return Task.CompletedTask;
}
await Task.Delay(-1);
}

private async Task MessageUpdated(Cacheable<IMessage, ulong> before, SocketMessage after, ISocketMessageChannel channel)
{
// If the message was not in the cache, downloading it will result in getting a copy of `after`.
var message = await before.GetOrDownloadAsync();
Console.WriteLine($"{message} -> {after}");
}
}

+ 29
- 0
docs/guides/concepts/samples/logging.cs View File

@@ -0,0 +1,29 @@
using Discord;
using Discord.WebSocket;

public class Program
{
private DiscordSocketClient _client;
static void Main(string[] args) => new Program().MainAsync().GetAwaiter().GetResult();
public async Task MainAsync()
{
_client = new DiscordSocketClient(new DiscordSocketConfig
{
LogLevel = LogSeverity.Info
});

_client.Log += Log;

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

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

+ 0
- 28
docs/guides/events.md View File

@@ -1,28 +0,0 @@
---
title: Events
---

# Events

Messages from Discord are exposed via events, and follow a pattern of `Func<[event params], Task>`, which allows you to easily create either async or sync event handlers.

To hook into events, you must be using the @Discord.WebSocket.DiscordSocketClient, which provides WebSocket capabilities, necessary for receiving events.

>[!NOTE]
>The gateway will wait for all registered handlers of an event to finish before raising the next event. As a result of this, it is reccomended that if you need to perform any heavy work in an event handler, it is done on its own thread or Task.

**For further documentation of all events**, it is reccomended to look at the [Events Section](xref:Discord.WebSocket.DiscordSocketClient#events) on the API documentation of @Discord.WebSocket.DiscordSocketClient

## Connection State

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](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.

[DiscordSocketClient.Ready](xref:Discord.WebSocket.DiscordSocketClient#Discord_WebSocket_DiscordSocketClient_Ready) is raised when the `READY` packet is parsed and received from Discord.

>[!NOTE]
>The [DiscordSocketClient.ConnectAsync](xref:Discord.WebSocket.DiscordSocketClient#Discord_WebSocket_DiscordSocketClient_ConnectAsync_System_Boolean_) method will not return until the READY packet has been processed. By default, it also will not return until the guild stream has finished. This means it is safe to run bot code directly after awaiting the ConnectAsync method.

BIN
docs/guides/getting_started/images/install-rider-add.png View File

Before After
Width: 910  |  Height: 225  |  Size: 24 kB

BIN
docs/guides/getting_started/images/install-rider-nuget-manager.png View File

Before After
Width: 522  |  Height: 326  |  Size: 18 kB

BIN
docs/guides/getting_started/images/install-rider-search.png View File

Before After
Width: 462  |  Height: 222  |  Size: 12 kB

BIN
docs/guides/getting_started/images/install-vs-deps.png View File

Before After
Width: 495  |  Height: 272  |  Size: 13 kB

BIN
docs/guides/getting_started/images/install-vs-nuget.png View File

Before After
Width: 1563  |  Height: 936  |  Size: 120 kB

BIN
docs/guides/getting_started/images/intro-add-bot.png View File

Before After
Width: 500  |  Height: 512  |  Size: 26 kB

BIN
docs/guides/getting_started/images/intro-client-id.png View File

Before After
Width: 340  |  Height: 130  |  Size: 5.1 kB

BIN
docs/guides/getting_started/images/intro-create-app.png View File

Before After
Width: 962  |  Height: 716  |  Size: 52 kB

BIN
docs/guides/getting_started/images/intro-create-bot.png View File

Before After
Width: 763  |  Height: 769  |  Size: 46 kB

BIN
docs/guides/getting_started/images/intro-token.png View File

Before After
Width: 700  |  Height: 339  |  Size: 27 kB

+ 141
- 0
docs/guides/getting_started/installing.md View File

@@ -0,0 +1,141 @@
---
title: Installing Discord.Net
---

Discord.Net is distributed through the NuGet package manager, and it is
recommended to use NuGet to get started.

Optionally, you may compile from source and install yourself.

# Supported Platforms

Currently, Discord.Net targets [.NET Standard] 1.3, and offers support for
.NET Standard 1.1. If your application will be targeting .NET Standard 1.1,
please see the [additional steps](#installing-on-net-standard-11).

Since Discord.Net is built on the .NET Standard, it is also recommended to
create applications using [.NET Core], though you are not required to. When
using .NET Framework, it is suggested to target `.NET 4.6.1` or higher.

[.NET Standard]: https://docs.microsoft.com/en-us/dotnet/articles/standard/library
[.NET Core]: https://docs.microsoft.com/en-us/dotnet/articles/core/

# Installing with NuGet

Release builds of Discord.Net 1.0 will be published to the
[official NuGet feed].

Development builds of Discord.Net 1.0, as well as [addons](TODO) are published
to our development [MyGet feed].

Direct feed link: `https://www.myget.org/F/discord-net/api/v3/index.json`

Not sure how to add a direct feed? See how [with Visual Studio]
or [without Visual Studio](#configuring-nuget-without-visual-studio)

[official NuGet feed]: https://nuget.org
[MyGet feed]: https://www.myget.org/feed/Packages/discord-net
[with Visual Studio]: https://docs.microsoft.com/en-us/nuget/tools/package-manager-ui#package-sources


## Using Visual Studio

1. Create a solution for your bot
2. In Solution Explorer, find the 'Dependencies' element under your bot's
project
3. Right click on 'Dependencies', and select 'Manage NuGet packages'
![Step 3](images/install-vs-deps.png)
4. In the 'browse' tab, search for 'Discord.Net'

> [!TIP]
Don't forget to change your package source if you're installing from the
developer feed.
Also make sure to check 'Enable Prereleases' if installing a dev build!

5. Install the 'Discord.Net' package

![Step 5](images/install-vs-nuget.png)

## Using JetBrains Rider

1. Create a new solution for your bot
2. Open the NuGet window (Tools > NuGet > Manage NuGet packages for Solution)
![Step 2](images/install-rider-nuget-manager.png)
3. In the 'Packages' tab, search for 'Discord.Net'
![Step 3](images/install-rider-search.png)

> [!TIP]
Make sure to check the 'Prerelease' box if installing a dev build!

4. Install by adding the package to your project
![Step 4](images/install-rider-add.png)

## Using Visual Studio Code

1. Create a new project for your bot
2. Add Discord.Net to your .csproj

[!code-xml[Sample .csproj](samples/project.csproj)]

> [!TIP]
Don't forget to add the package source to a [NuGet.Config file](#configuring-nuget-without-visual-studio) if you're installing from the
developer feed.

# Compiling from Source

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

### Using Visual Studio

- [Visual Studio 2017](https://www.visualstudio.com/)
- [.NET Core SDK 1.0](https://www.microsoft.com/net/download/core#/sdk)

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

### Using Command Line

- [.NET Core SDK 1.0](https://www.microsoft.com/net/download/core#/sdk)

# Additional Information

## Installing on .NET Standard 1.1

For applications targeting a runtime corresponding with .NET Standard 1.1 or 1.2,
the builtin WebSocket and UDP provider will not work. For applications which
utilize a WebSocket connection to Discord (WebSocket or RPC), third-party
provider packages will need to be installed and configured.

First, install the following packages through NuGet, or compile yourself, if
you prefer:

- Discord.Net.Providers.WS4Net
- Discord.Net.Providers.UDPClient

Note that `Discord.Net.Providers.UDPClient` is _only_ required if your bot will
be utilizing voice chat.

Next, you will need to configure your [DiscordSocketClient] to use these custom
providers over the default ones.

To do this, set the `WebSocketProvider` and optionally `UdpSocketProvider`
properties on the [DiscordSocketConfig] that you are passing into your
client.

[!code-csharp[NET Standard 1.1 Example](samples/netstd11.cs)]

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

## Configuring NuGet without Visual Studio

If you plan on deploying your bot or developing outside of Visual Studio, you
will need to create a local NuGet configuration file for your project.

To do this, create a file named `nuget.config` alongside the root of your
application, where the project solution is located.

Paste the following snippets into this configuration file, adding any additional
feeds as necessary.

[!code-xml[NuGet Configuration](samples/nuget.config)]

+ 227
- 0
docs/guides/getting_started/intro.md View File

@@ -0,0 +1,227 @@
---
title: Getting Started
---

# Making a Ping-Pong bot

One of the first steps to getting started with the Discord API is to
write a basic ping-pong bot. We will expand on this to create more
diverse commands later, but for now, it is a good starting point.

## Creating a Discord Bot

Before you can begin writing your bot, it is necessary to create a bot
account on Discord.

1. Visit the [Discord Applications Portal]
2. Create a New Application
3. Give the application a name (this will be the bot's initial
username).
4. Create the Application
![Step 4](images/intro-create-app.png)
5. In the application review page, click **Create a Bot User**
![Step 5](images/intro-create-bot.png)
6. Confirm the popup
7. If this bot will be public, check 'Public Bot'.
**Do not tick any other options!**

[Discord Applications Portal]: https://discordapp.com/developers/applications/me

## Adding your bot to a server

Bots **can not** use invite links, they must be explicitly invited
through the OAuth2 flow.

1. Open your bot's application on the [Discord Applications Portal]
2. Retrieve the app's **Client ID**.

![Step 2](images/intro-client-id.png)

3. Create an OAuth2 authorization URL
`https://discordapp.com/oauth2/authorize?client_id=<CLIENT ID>&scope=bot`
4. Open the authorization URL in your browser
5. Select a server

>[!NOTE]
Only servers where you have the `MANAGE_SERVER` permission will be
present in this list.

6. Click authorize

![Step 6](images/intro-add-bot.png)

## Connecting to Discord

If you have not already created a project and installed Discord.Net,
do that now. (see the [Installing](installing.md) section)

### Async

Discord.Net uses .NET's Task-based Asynchronous Pattern ([TAP])
extensively - nearly every operation is asynchronous.

It is highly recommended that these operations be awaited in a
properly established async context whenever possible. Establishing an
async context can be problematic, but not hard.

To do so, we will be creating an async main in your console
application, and rewriting the static main method to invoke the new
async main.

[!code-csharp[Async Context](samples/intro/async-context.cs)]

As a result of this, your program will now start, and immidiately
jump into an async context. This will allow us later on to create a
connection to Discord, without needing to worry about setting up the
correct async implementation.

>[!TIP]
If your application throws any exceptions within an async context,
they will be thrown all the way back up to the first non-async method.
Since our first non-async method is the program's Main method, this
means that **all** unhandled exceptions will be thrown up there, which
will crash your application. Discord.Net will prevent exceptions in
event handlers from crashing your program, but any exceptions in your
async main **will** cause the application to crash.

### Creating a logging method

Before we create and configure a Discord client, we will add a method
to handle Discord.Net's log events.

To allow agnostic support of as many log providers as possible, we
log information through a Log event, with a proprietary LogMessage
parameter. See the [API Documentation] for this event.

If you are using your own logging framework, this is where you would
invoke it. For the sake of simplicity, we will only be logging to
the Console.

[!code-csharp[Async Context](samples/intro/logging.cs)]

### Creating a Discord Client

Finally, we can create a connection to Discord. Since we are writing
a bot, we will be using a [DiscordSocketClient], along with socket
entities. See the [terminology](terminology.md) if you're unsure of
the differences.

To do so, create an instance of [DiscordSocketClient] in your async
main, passing in a configuration object only if necessary. For most
users, the default will work fine.

Before connecting, we should hook the client's log event to the
log handler that was just created. Events in Discord.Net work
similarly to other events in C#, so hook this event the way that
you typically would.

Next, you will need to 'login to Discord' with the `LoginAsync` method.

You may create a variable to hold your bot's token (this can be found
on your bot's application page on the [Discord Applications Portal]).
![Token](images/intro-token.png)

>[!IMPORTANT]
Your bot's token can be used to gain total access to your bot, so
**do __NOT__ share this token with anyone!** It may behoove you to
store this token in an external file if you plan on distributing the
source code for your bot.

We may now invoke the client's `StartAsync` method, which will
start connection/reconnection logic. It is important to note that
**this method returns as soon as connection logic has been started!**

Any methods that rely on the client's state should go in an event
handler.

>[!NOTE]
Connection logic is incomplete as of the current build. Events will
soon be added to indicate when the client's state is ready for use;
(rewrite this section when possible)

Finally, we will want to block the async main method from returning
until after the application is exited. To do this, we can await an
infinite delay, or any other blocking method, such as reading from
the console.

The following lines can now be added:

[!code-csharp[Create client](samples/intro/client.cs)]

At this point, feel free to start your program and see your bot come
online in Discord.

>[!TIP]
Encountering a `PlatformNotSupportedException` when starting your bot?
This means that you are targeting a platform where .NET's default
WebSocket client is not supported. Refer to the [installing guide]
for how to fix this.

[TAP]: https://docs.microsoft.com/en-us/dotnet/articles/csharp/async
[API Documentation]: xref:Discord.Rest.BaseDiscordClient#Discord_Rest_BaseDiscordClient_Log
[DiscordSocketClient]: xref:Discord.WebSocket.DiscordSocketClient
[installing guide]: installing.md#installing-on-net-standard-11

### Handling a 'ping'

Now that we have learned how to open a connection to Discord, we can
begin handling messages that users are sending.

To start out, our bot will listen for any message where the content
is equal to `!ping`, and respond back with `Pong!`.

Since we want to listen for new messages, the event to hook in to
is [MessageReceived].

In your program, add a method that matches the signature of the
MessageReceived event - it must be a method (`Func`) that returns the
type `Task`, and takes a single parameter, a [SocketMessage]. Also,
since we will be sending data to Discord in this method, we will flag
it as `async`.

In this method, we will add an `if` block, to determine if the message
content fits the rules of our scenario - recall that it must be equal
to `!ping`.

Inside the branch of this condition, we will want to send a message
back to the channel from which the message came - `Pong!`. To find the
channel, look for the `Channel` property on the message parameter.

Next, we will want to send a message to this channel. Since the
channel object is of type [SocketMessageChannel], we can invoke the
`SendMessageAsync` instance method. For the message content, send back
a string containing 'Pong!'.

You should have now added the following lines:

[!code-csharp[Message](samples/intro/message.cs)]

Now, your first bot is complete. You may continue to add on to this
if you desire, but for any bot that will be carrying out multiple
commands, it is strongly encouraged to use the command framework, as
shown below.

For your reference, you may view the [completed program].

[MessageReceived]: xref:Discord.WebSocket.DiscordSocketClient#Discord_WebSocket_DiscordSocketClient_MessageReceived
[SocketMessage]: xref:Discord.WebSocket.SocketMessage
[SocketMessageChannel]: xref:Discord.WebSocket.ISocketMessageChannel
[completed program]: samples/intro/complete.cs

# Building a bot with commands

This section will show you how to write a program that is ready for
[commands](commands/commands.md). Note that this will not be explaining _how_
to write commands or services, it will only be covering the general
structure.

For reference, view an [annotated example] of this structure.

[annotated example]: samples/intro/structure.cs

It is important to know that the recommended design pattern of bots
should be to separate the program (initialization and command handler),
the modules (handle commands), and the services (persistent storage,
pure functions, data manipulation).

**todo:** diagram of bot structure

+ 15
- 0
docs/guides/getting_started/samples/intro/async-context.cs View File

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

namespace MyBot
{
public class Program
{
public static void Main(string[] args)
=> new Program().MainAsync().GetAwaiter().GetResult();

public async Task MainAsync()
{
}
}
}

+ 16
- 0
docs/guides/getting_started/samples/intro/client.cs View File

@@ -0,0 +1,16 @@
// Program.cs
using Discord.WebSocket;
// ...
public async Task MainAsync()
{
var client = new DiscordSocketClient();

client.Log += Log;

string token = "abcdefg..."; // Remember to keep this private!
await client.LoginAsync(TokenType.Bot, token);
await client.StartAsync();

// Block this task until the program is closed.
await Task.Delay(-1);
}

+ 42
- 0
docs/guides/getting_started/samples/intro/complete.cs View File

@@ -0,0 +1,42 @@
using Discord;
using Discord.WebSocket;
using System;
using System.Threading.Tasks;

namespace MyBot
{
public class Program
{
public static void Main(string[] args)
=> new Program().MainAsync().GetAwaiter().GetResult();

public async Task MainAsync()
{
var client = new DiscordSocketClient();

client.Log += Log;
client.MessageReceived += MessageReceived;

string token = "abcdefg..."; // Remember to keep this private!
await client.LoginAsync(TokenType.Bot, token);
await client.StartAsync();

// Block this task until the program is closed.
await Task.Delay(-1);
}

private async Task MessageReceived(SocketMessage message)
{
if (message.Content == "!ping")
{
await message.Channel.SendMessageAsync("Pong!");
}
}

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

+ 22
- 0
docs/guides/getting_started/samples/intro/logging.cs View File

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

namespace MyBot
{
public class Program
{
public static void Main(string[] args)
=> new Program().MainAsync().GetAwaiter().GetResult();

public async Task MainAsync()
{
}

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

+ 14
- 0
docs/guides/getting_started/samples/intro/message.cs View File

@@ -0,0 +1,14 @@
public async Task MainAsync()
{
// client.Log ...
client.MessageReceived += MessageReceived;
// ...
}

private async Task MessageReceived(SocketMessage message)
{
if (message.Content == "!ping")
{
await message.Channel.SendMessageAsync("Pong!");
}
}

docs/guides/samples/first-steps.cs → docs/guides/getting_started/samples/intro/structure.cs View File

@@ -1,6 +1,7 @@
using System;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Discord;
using Discord.Commands;
using Discord.WebSocket;
@@ -9,8 +10,8 @@ class Program
{
private readonly DiscordSocketClient _client;
// Keep the CommandService and IDependencyMap around for use with commands.
private readonly IDependencyMap _map = new DependencyMap();
// Keep the CommandService and IServiceCollection around for use with commands.
private readonly IServiceCollection _map = new ServiceCollection();
private readonly CommandService _commands = new CommandService();

// Program entry point
@@ -29,8 +30,8 @@ class Program
LogLevel = LogSeverity.Info,
// If you or another service needs to do anything with messages
// (eg. checking Reactions), you should probably
// set the MessageCacheSize here.
// (eg. checking Reactions, checking the content of edited/deleted messages),
// you must set the MessageCacheSize. You may adjust the number as needed.
//MessageCacheSize = 50,

// If your platform doesn't have native websockets,
@@ -40,7 +41,7 @@ class Program
});
}

// Create a named logging handler, so it can be re-used by addons
// Example of a logging handler. This can be re-used by addons
// that ask for a Func<LogMessage, Task>.
private static Task Logger(LogMessage message)
{
@@ -64,6 +65,13 @@ class Program
}
Console.WriteLine($"{DateTime.Now,-19} [{message.Severity,8}] {message.Source}: {message.Message}");
Console.ForegroundColor = cc;
// If you get an error saying 'CompletedTask' doesn't exist,
// your project is targeting .NET 4.5.2 or lower. You'll need
// to adjust your project's target framework to 4.6 or higher
// (instructions for this are easily Googled).
// If you *need* to run on .NET 4.5 for compat/other reasons,
// the alternative is to 'return Task.Delay(0);' instead.
return Task.CompletedTask;
}

@@ -77,28 +85,36 @@ class Program

// Login and connect.
await _client.LoginAsync(TokenType.Bot, /* <DON'T HARDCODE YOUR TOKEN> */);
await _client.ConnectAsync();
await _client.StartAsync();
// Wait infinitely so your bot actually stays connected.
await Task.Delay(-1);
}

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

// When all your required services are in the collection, build the container.
// Tip: There's an overload taking in a 'validateScopes' bool to make sure
// you haven't made any mistakes in your dependency graph.
_services = _map.BuildServiceProvider();

// Either search the program and add all Module classes that can be found:
// Either search the program and add all Module classes that can be found.
// Module classes *must* be marked 'public' or they will be ignored.
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;
_client.MessageReceived += HandleCommandAsync;
}

private async Task CmdHandler(SocketMessage arg)
private async Task HandleCommandAsync(SocketMessage arg)
{
// Bail out if it's a System Message.
var msg = arg as SocketUserMessage;
@@ -110,14 +126,14 @@ class Program
// 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) */)
if (msg.HasCharPrefix('!', ref pos) /* || msg.HasMentionPrefix(_client.CurrentUser, ref pos) */)
{
// Create a Command Context
var context = new SocketCommandContext(msg.Discord, msg);
// Create a Command Context.
var context = new SocketCommandContext(_client, 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);
var result = await _commands.ExecuteAsync(context, pos, _services);

// Uncomment the following lines if you want the bot
// to send a message if it failed (not advised for most situations).
@@ -125,4 +141,4 @@ class Program
// await msg.Channel.SendMessageAsync(result.ErrorReason);
}
}
}
}

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

@@ -0,0 +1,9 @@
using Discord.Providers.WS4Net;
using Discord.Providers.UDPClient;
using Discord.WebSocket;
// ...
var client = new DiscordSocketClient(new DiscordSocketConfig
{
WebSocketProvider = WS4NetProvider.Instance,
UdpSocketProvider = UDPClientProvider.Instance,
});

+ 6
- 0
docs/guides/getting_started/samples/nuget.config View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="discord.net ci feed" value="https://www.myget.org/F/discord-net/api/v3/index.json" />
</packageSources>
</configuration>

+ 13
- 0
docs/guides/getting_started/samples/project.csproj View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp1.1</TargetFramework>
<NoWin32Manifest>true</NoWin32Manifest>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Discord.Net" Version="1.0.0-rc-00617" />
</ItemGroup>

</Project>

docs/guides/terminology.md → docs/guides/getting_started/terminology.md View File


+ 0
- 50
docs/guides/intro.md View File

@@ -1,50 +0,0 @@
---
title: Getting Started
---

# Getting Started

## Requirements

Discord.Net supports logging in with all variations of Discord Accounts, however the Discord API reccomends using a `Bot Account`.

You may [register a bot account here](https://discordapp.com/developers/applications/me).

Bot accounts must be added to a server, you must use the [OAuth 2 Flow](https://discordapp.com/developers/docs/topics/oauth2#adding-bots-to-guilds) to add them to servers.

## Installation

You can install Discord.Net 1.0 from our [MyGet Feed](https://www.myget.org/feed/Packages/discord-net).

**For most users writing bots, install only `Discord.Net.WebSocket`.**

You may add the MyGet feed to Visual Studio directly from `https://www.myget.org/F/discord-net/api/v3/index.json`.

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

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

## First Steps

[!code-csharp[Main](samples/first-steps.cs)]

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

+ 0
- 11
docs/guides/logging.md View File

@@ -1,11 +0,0 @@
# Using the Logger

Discord.Net will automatically output log messages through the [Log](xref:Discord.DiscordClient#Discord_DiscordClient_Log) event.

## Usage

To handle Log Messages through Discord.Net's Logger, hook into the [Log](xref:Discord.DiscordClient#Discord_DiscordClient_Log) event.

The @Discord.LogMessage object has a custom `ToString` method attached to it, when outputting log messages, it is reccomended you use this, instead of building your own output message.

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

docs/migrating.md → docs/guides/migrating/migrating.md View File

@@ -42,7 +42,7 @@ events are delegates, but are still registered the same.
For example, let's look at [DiscordSocketClient.MessageReceived](xref:Discord.WebSocket.DiscordSocketClient#Discord_WebSocket_DiscordSocketClient_MessageReceived)

To hook an event into MessageReceived, we now use the following code:
[!code-csharp[Event Registration](guides/samples/migrating/event.cs)]
[!code-csharp[Event Registration](samples/event.cs)]

> **All Event Handlers in 1.0 MUST return Task!**

@@ -50,7 +50,7 @@ If your event handler is marked as `async`, it will automatically return `Task`.
if you do not need to execute asynchronus code, do _not_ mark your handler as `async`, and instead,
stick a `return Task.CompletedTask` at the bottom.

[!code-csharp[Sync Event Registration](guides/samples/migrating/sync_event.cs)]
[!code-csharp[Sync Event Registration](samples/sync_event.cs)]

**Event handlers no longer require a sender.** The only arguments your event handler needs to accept
are the parameters used by the event. It is recommended to look at the event in IntelliSense or on the

docs/guides/samples/migrating/event.cs → docs/guides/migrating/samples/event.cs View File


docs/guides/samples/migrating/sync_event.cs → docs/guides/migrating/samples/sync_event.cs View File


+ 0
- 20
docs/guides/samples.md View File

@@ -1,20 +0,0 @@
---
title: Samples
---

# Samples

>[!NOTE]
>All of these samples assume you have `_client` defined as a `DiscordSocketClient`.

#### Changing the bot's avatar

[!code-csharp[Bot Avatar](samples/faq/avatar.cs)]

#### Changing the bot's status

[!code-csharp[Bot Status](samples/faq/status.cs)]

#### Sending a message to a channel

[!code-csharp[Message to Channel](samples/faq/send_message.cs)]

+ 0
- 5
docs/guides/samples/faq/avatar.cs View File

@@ -1,5 +0,0 @@
public async Task ChangeAvatar()
{
var fileStream = new FileStream("./newAvatar.png", FileMode.Open);
await _client.CurrentUser.ModifyAsync(x => x.Avatar = fileStream);
}

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

@@ -1,6 +0,0 @@
public async Task SendMessageToChannel(ulong ChannelId)
{
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. */
}

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

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

+ 0
- 28
docs/guides/samples/logging.cs View File

@@ -1,28 +0,0 @@
using Discord;
using Discord.Rest;

public class Program
{
private DiscordSocketClient _client;
static void Main(string[] args) => new Program().Start().GetAwaiter().GetResult();
public async Task Start()
{
_client = new DiscordSocketClient(new DiscordSocketConfig() {
LogLevel = LogSeverity.Info
});

_client.Log += Log;

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

+ 25
- 13
docs/guides/toc.yml View File

@@ -1,15 +1,27 @@

- name: Getting Started
href: intro.md
- name: Terminology
href: terminology.md
- name: Logging
href: logging.md
- name: Commands
href: commands.md
items:
- name: Installation
href: getting_started/installing.md
- name: Your First Bot
href: getting_started/intro.md
- name: Terminology
href: getting_started/terminology.md
- name: Basic Concepts
items:
- name: Logging Data
href: concepts/logging.md
- name: Working with Events
href: concepts/events.md
- name: Managing Connections
href: concepts/connections.md
- name: Entities
href: concepts/entities.md
- name: The Command Service
items:
- name: Command Guide
href: commands/commands.md
- name: Voice
href: voice.md
- name: Events
href: events.md
- name: Code Samples
href: samples.md
items:
- name: Voice Guide
href: voice/sending-voice.md
- name: Migrating from 0.9

docs/guides/samples/audio_create_ffmpeg.cs → docs/guides/voice/samples/audio_create_ffmpeg.cs View File


docs/guides/samples/audio_ffmpeg.cs → docs/guides/voice/samples/audio_ffmpeg.cs View File

@@ -3,7 +3,7 @@ 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);
var discord = client.CreatePCMStream(AudioApplication.Mixed);
await output.CopyToAsync(discord);
await discord.FlushAsync();
}
}

docs/guides/samples/joining_audio.cs → docs/guides/voice/samples/joining_audio.cs View File


docs/guides/voice.md → docs/guides/voice/sending-voice.md View File

@@ -1,24 +1,14 @@
# Voice
---
title: Sending Voice
---

**Information on this page is subject to change!**

>[!WARNING]
>Audio in 1.0 is incomplete. Most of the below documentation is untested.
>This article is out of date, and has not been rewritten yet.
Information is not guaranteed to be accurate.

## Installation

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

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

Audio requires two native libraries, `libsodium` and `opus`.
Both of these libraries must be placed in the runtime directory of your

+ 1
- 1
docs/index.md View File

@@ -3,7 +3,7 @@

Discord.Net is an asynchronous, multiplatform .NET Library used to interface with the [Discord API](https://discordapp.com/).

If this is your first time using Discord.Net, you should refer to the [Intro](guides/intro.md) for tutorials.
If this is your first time using Discord.Net, you should refer to the [Intro](guides/getting_started/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:


+ 0
- 2
docs/toc.yml View File

@@ -1,8 +1,6 @@

- name: Guides
href: guides/
- name: Migrating
href: migrating.md
- name: API Documentation
href: api/
homepage: api/index.md

src/Discord.Net.Analyzers/AssemblyInfo.cs → experiment/Discord.Net.Analyzers/AssemblyInfo.cs View File


src/Discord.Net.Analyzers/ConfigureAwaitAnalyzer.cs → experiment/Discord.Net.Analyzers/ConfigureAwaitAnalyzer.cs View File


+ 18
- 0
experiment/Discord.Net.Analyzers/Discord.Net.Analyzers.csproj View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../../Discord.Net.targets" />
<PropertyGroup>
<AssemblyName>Discord.Net.Analyzers</AssemblyName>
<RootNamespace>Discord.Analyzers</RootNamespace>
<Description>A Discord.Net extension adding compile-time analysis.</Description>
<TargetFrameworks>netstandard1.3</TargetFrameworks>
<PackageTargetFallback>$(PackageTargetFallback);portable-net45+win81</PackageTargetFallback>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Discord.Net.Core\Discord.Net.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis" Version="2.1.0">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
</Project>

src/Discord.Net.Relay/ApplicationBuilderExtensions.cs → experiment/Discord.Net.Relay/ApplicationBuilderExtensions.cs View File


src/Discord.Net.Relay/AssemblyInfo.cs → experiment/Discord.Net.Relay/AssemblyInfo.cs View File


+ 18
- 0
experiment/Discord.Net.Relay/Discord.Net.Relay.csproj View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../../Discord.Net.targets" />
<PropertyGroup>
<AssemblyName>Discord.Net.Relay</AssemblyName>
<RootNamespace>Discord.Relay</RootNamespace>
<Description>A core Discord.Net library containing the Relay server.</Description>
<TargetFrameworks>netstandard1.3</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Discord.Net.Core\Discord.Net.Core.csproj" />
<ProjectReference Include="..\Discord.Net.Rest\Discord.Net.Rest.csproj" />
<ProjectReference Include="..\Discord.Net.WebSocket\Discord.Net.WebSocket.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore" Version="1.1.2" />
<PackageReference Include="Microsoft.AspNetCore.WebSockets" Version="1.0.2" />
</ItemGroup>
</Project>

src/Discord.Net.Relay/RelayConnection.cs → experiment/Discord.Net.Relay/RelayConnection.cs View File


src/Discord.Net.Relay/RelayServer.cs → experiment/Discord.Net.Relay/RelayServer.cs View File


+ 0
- 17
pack.ps1 View File

@@ -1,17 +0,0 @@
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) }

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

@@ -1,32 +0,0 @@
<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>

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

@@ -1,11 +1,12 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;

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);
public abstract Task<PreconditionResult> CheckPermissions(ICommandContext context, ParameterInfo parameter, object value, IServiceProvider services);
}
}

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

@@ -6,6 +6,13 @@ namespace Discord.Commands
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public abstract class PreconditionAttribute : Attribute
{
public abstract Task<PreconditionResult> CheckPermissions(ICommandContext context, CommandInfo command, IDependencyMap map);
/// <summary>
/// Specify a group that this precondition belongs to. Preconditions of the same group require only one
/// of the preconditions to pass in order to be successful (A || B). Specifying <see cref="Group"/> = <see cref="null"/>
/// or not at all will require *all* preconditions to pass, just like normal (A &amp;&amp; B).
/// </summary>
public string Group { get; set; } = null;

public abstract Task<PreconditionResult> CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services);
}
}

+ 7
- 4
src/Discord.Net.Commands/Attributes/Preconditions/RequireBotPermissionAttribute.cs View File

@@ -1,5 +1,6 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;

namespace Discord.Commands
{
@@ -41,16 +42,18 @@ namespace Discord.Commands
GuildPermission = null;
}

public override async Task<PreconditionResult> CheckPermissions(ICommandContext context, CommandInfo command, IDependencyMap map)
public override async Task<PreconditionResult> CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services)
{
var guildUser = await context.Guild.GetCurrentUserAsync();
IGuildUser guildUser = null;
if (context.Guild != null)
guildUser = await context.Guild.GetCurrentUserAsync().ConfigureAwait(false);

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}");
return PreconditionResult.FromError($"Bot requires guild permission {GuildPermission.Value}");
}

if (ChannelPermission.HasValue)
@@ -64,7 +67,7 @@ namespace Discord.Commands
perms = ChannelPermissions.All(guildChannel);

if (!perms.Has(ChannelPermission.Value))
return PreconditionResult.FromError($"Command requires channel permission {ChannelPermission.Value}");
return PreconditionResult.FromError($"Bot requires channel permission {ChannelPermission.Value}");
}

return PreconditionResult.FromSuccess();


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

@@ -1,5 +1,6 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;

namespace Discord.Commands
{
@@ -37,7 +38,7 @@ namespace Discord.Commands
Contexts = contexts;
}

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



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

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

namespace Discord.Commands
{
/// <summary>
/// Require that the command is invoked in a channel marked NSFW
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class RequireNsfwAttribute : PreconditionAttribute
{
public override Task<PreconditionResult> CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services)
{
if (context.Channel.IsNsfw)
return Task.FromResult(PreconditionResult.FromSuccess());
else
return Task.FromResult(PreconditionResult.FromError("This command may only be invoked in an NSFW channel."));
}
}
}

+ 16
- 4
src/Discord.Net.Commands/Attributes/Preconditions/RequireOwnerAttribute.cs View File

@@ -1,5 +1,6 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;

namespace Discord.Commands
{
@@ -10,11 +11,22 @@ namespace Discord.Commands
[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)
public override async Task<PreconditionResult> CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services)
{
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");
switch (context.Client.TokenType)
{
case TokenType.Bot:
var application = await context.Client.GetApplicationInfoAsync();
if (context.User.Id != application.Owner.Id)
return PreconditionResult.FromError("Command can only be run by the owner of the bot");
return PreconditionResult.FromSuccess();
case TokenType.User:
if (context.User.Id != context.Client.CurrentUser.Id)
return PreconditionResult.FromError("Command can only be run by the owner of the bot");
return PreconditionResult.FromSuccess();
default:
return PreconditionResult.FromError($"{nameof(RequireOwnerAttribute)} is not supported by this {nameof(TokenType)}.");
}
}
}
}

+ 4
- 3
src/Discord.Net.Commands/Attributes/Preconditions/RequireUserPermissionAttribute.cs View File

@@ -1,5 +1,6 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;

namespace Discord.Commands
{
@@ -42,7 +43,7 @@ namespace Discord.Commands
GuildPermission = null;
}
public override Task<PreconditionResult> CheckPermissions(ICommandContext context, CommandInfo command, IDependencyMap map)
public override Task<PreconditionResult> CheckPermissions(ICommandContext context, CommandInfo command, IServiceProvider services)
{
var guildUser = context.User as IGuildUser;

@@ -51,7 +52,7 @@ namespace Discord.Commands
if (guildUser == null)
return Task.FromResult(PreconditionResult.FromError("Command must be used in a guild channel"));
if (!guildUser.GuildPermissions.Has(GuildPermission.Value))
return Task.FromResult(PreconditionResult.FromError($"Command requires guild permission {GuildPermission.Value}"));
return Task.FromResult(PreconditionResult.FromError($"User requires guild permission {GuildPermission.Value}"));
}

if (ChannelPermission.HasValue)
@@ -65,7 +66,7 @@ namespace Discord.Commands
perms = ChannelPermissions.All(guildChannel);

if (!perms.Has(ChannelPermission.Value))
return Task.FromResult(PreconditionResult.FromError($"Command requires channel permission {ChannelPermission.Value}"));
return Task.FromResult(PreconditionResult.FromError($"User requires channel permission {ChannelPermission.Value}"));
}

return Task.FromResult(PreconditionResult.FromSuccess());


+ 18
- 7
src/Discord.Net.Commands/Builders/CommandBuilder.cs View File

@@ -2,6 +2,7 @@
using System.Linq;
using System.Threading.Tasks;
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;

namespace Discord.Commands.Builders
{
@@ -9,19 +10,22 @@ namespace Discord.Commands.Builders
{
private readonly List<PreconditionAttribute> _preconditions;
private readonly List<ParameterBuilder> _parameters;
private readonly List<Attribute> _attributes;
private readonly List<string> _aliases;

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

public string Name { get; set; }
public string Summary { get; set; }
public string Remarks { get; set; }
public string PrimaryAlias { get; set; }
public RunMode RunMode { get; set; }
public int Priority { get; set; }

public IReadOnlyList<PreconditionAttribute> Preconditions => _preconditions;
public IReadOnlyList<ParameterBuilder> Parameters => _parameters;
public IReadOnlyList<Attribute> Attributes => _attributes;
public IReadOnlyList<string> Aliases => _aliases;

//Automatic
@@ -31,16 +35,18 @@ namespace Discord.Commands.Builders

_preconditions = new List<PreconditionAttribute>();
_parameters = new List<ParameterBuilder>();
_attributes = new List<Attribute>();
_aliases = new List<string>();
}
//User-defined
internal CommandBuilder(ModuleBuilder module, string primaryAlias, Func<ICommandContext, object[], IDependencyMap, Task> callback)
internal CommandBuilder(ModuleBuilder module, string primaryAlias, Func<ICommandContext, object[], IServiceProvider, CommandInfo, Task> callback)
: this(module)
{
Discord.Preconditions.NotNull(primaryAlias, nameof(primaryAlias));
Discord.Preconditions.NotNull(callback, nameof(callback));

Callback = callback;
PrimaryAlias = primaryAlias;
_aliases.Add(primaryAlias);
}

@@ -74,12 +80,17 @@ namespace Discord.Commands.Builders
{
for (int i = 0; i < aliases.Length; i++)
{
var alias = aliases[i] ?? "";
string alias = aliases[i] ?? "";
if (!_aliases.Contains(alias))
_aliases.Add(alias);
}
return this;
}
public CommandBuilder AddAttributes(params Attribute[] attributes)
{
_attributes.AddRange(attributes);
return this;
}
public CommandBuilder AddPrecondition(PreconditionAttribute precondition)
{
_preconditions.Add(precondition);
@@ -109,9 +120,9 @@ namespace Discord.Commands.Builders

internal CommandInfo Build(ModuleInfo info, CommandService service)
{
//Default name to first alias
//Default name to primary alias
if (Name == null)
Name = _aliases[0];
Name = PrimaryAlias;

if (_parameters.Count > 0)
{
@@ -119,11 +130,11 @@ namespace Discord.Commands.Builders

var firstMultipleParam = _parameters.FirstOrDefault(x => x.IsMultiple);
if ((firstMultipleParam != null) && (firstMultipleParam != lastParam))
throw new InvalidOperationException("Only the last parameter in a command may have the Multiple flag.");
throw new InvalidOperationException($"Only the last parameter in a command may have the Multiple flag. Parameter: {firstMultipleParam.Name} in {PrimaryAlias}");
var firstRemainderParam = _parameters.FirstOrDefault(x => x.IsRemainder);
if ((firstRemainderParam != null) && (firstRemainderParam != lastParam))
throw new InvalidOperationException("Only the last parameter in a command may have the Remainder flag.");
throw new InvalidOperationException($"Only the last parameter in a command may have the Remainder flag. Parameter: {firstRemainderParam.Name} in {PrimaryAlias}");
}

return new CommandInfo(this, info, service);


+ 11
- 2
src/Discord.Net.Commands/Builders/ModuleBuilder.cs View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;

namespace Discord.Commands.Builders
{
@@ -9,6 +10,7 @@ namespace Discord.Commands.Builders
private readonly List<CommandBuilder> _commands;
private readonly List<ModuleBuilder> _submodules;
private readonly List<PreconditionAttribute> _preconditions;
private readonly List<Attribute> _attributes;
private readonly List<string> _aliases;

public CommandService Service { get; }
@@ -20,6 +22,7 @@ namespace Discord.Commands.Builders
public IReadOnlyList<CommandBuilder> Commands => _commands;
public IReadOnlyList<ModuleBuilder> Modules => _submodules;
public IReadOnlyList<PreconditionAttribute> Preconditions => _preconditions;
public IReadOnlyList<Attribute> Attributes => _attributes;
public IReadOnlyList<string> Aliases => _aliases;

//Automatic
@@ -31,6 +34,7 @@ namespace Discord.Commands.Builders
_commands = new List<CommandBuilder>();
_submodules = new List<ModuleBuilder>();
_preconditions = new List<PreconditionAttribute>();
_attributes = new List<Attribute>();
_aliases = new List<string>();
}
//User-defined
@@ -62,18 +66,23 @@ namespace Discord.Commands.Builders
{
for (int i = 0; i < aliases.Length; i++)
{
var alias = aliases[i] ?? "";
string alias = aliases[i] ?? "";
if (!_aliases.Contains(alias))
_aliases.Add(alias);
}
return this;
}
public ModuleBuilder AddAttributes(params Attribute[] attributes)
{
_attributes.AddRange(attributes);
return this;
}
public ModuleBuilder AddPrecondition(PreconditionAttribute precondition)
{
_preconditions.Add(precondition);
return this;
}
public ModuleBuilder AddCommand(string primaryAlias, Func<ICommandContext, object[], IDependencyMap, Task> callback, Action<CommandBuilder> createFunc)
public ModuleBuilder AddCommand(string primaryAlias, Func<ICommandContext, object[], IServiceProvider, CommandInfo, Task> callback, Action<CommandBuilder> createFunc)
{
var builder = new CommandBuilder(this, primaryAlias, callback);
createFunc(builder);


+ 125
- 69
src/Discord.Net.Commands/Builders/ModuleClassBuilder.cs View File

@@ -12,25 +12,42 @@ namespace Discord.Commands
{
private static readonly TypeInfo _moduleTypeInfo = typeof(IModuleBase).GetTypeInfo();

public static IEnumerable<TypeInfo> Search(Assembly assembly)
public static async Task<IReadOnlyList<TypeInfo>> SearchAsync(Assembly assembly, CommandService service)
{
foreach (var type in assembly.ExportedTypes)
bool IsLoadableModule(TypeInfo info)
{
var typeInfo = type.GetTypeInfo();
if (IsValidModuleDefinition(typeInfo) &&
!typeInfo.IsDefined(typeof(DontAutoLoadAttribute)))
return info.DeclaredMethods.Any(x => x.GetCustomAttribute<CommandAttribute>() != null) &&
info.GetCustomAttribute<DontAutoLoadAttribute>() == null;
}

var result = new List<TypeInfo>();

foreach (var typeInfo in assembly.DefinedTypes)
{
if (typeInfo.IsPublic || typeInfo.IsNestedPublic)
{
yield return typeInfo;
if (IsValidModuleDefinition(typeInfo) &&
!typeInfo.IsDefined(typeof(DontAutoLoadAttribute)))
{
result.Add(typeInfo);
}
}
else if (IsLoadableModule(typeInfo))
{
await service._cmdLogger.WarningAsync($"Class {typeInfo.FullName} is not public and cannot be loaded. To suppress this message, mark the class with {nameof(DontAutoLoadAttribute)}.");
}
}

return result;
}

public static Dictionary<Type, ModuleInfo> Build(CommandService service, params TypeInfo[] validTypes) => Build(validTypes, service);
public static Dictionary<Type, ModuleInfo> Build(IEnumerable<TypeInfo> validTypes, CommandService service)

public static Task<Dictionary<Type, ModuleInfo>> BuildAsync(CommandService service, params TypeInfo[] validTypes) => BuildAsync(validTypes, service);
public static async Task<Dictionary<Type, ModuleInfo>> BuildAsync(IEnumerable<TypeInfo> validTypes, CommandService service)
{
/*if (!validTypes.Any())
throw new InvalidOperationException("Could not find any valid modules from the given selection");*/
var topLevelGroups = validTypes.Where(x => x.DeclaringType == null);
var subGroups = validTypes.Intersect(topLevelGroups);

@@ -48,10 +65,13 @@ namespace Discord.Commands

BuildModule(module, typeInfo, service);
BuildSubTypes(module, typeInfo.DeclaredNestedTypes, builtTypes, service);
builtTypes.Add(typeInfo);

result[typeInfo.AsType()] = module.Build(service);
}

await service._cmdLogger.DebugAsync($"Successfully built {builtTypes.Count} modules.").ConfigureAwait(false);

return result;
}

@@ -81,23 +101,31 @@ namespace Discord.Commands

foreach (var attribute in attributes)
{
// TODO: C#7 type switch
if (attribute is NameAttribute)
builder.Name = (attribute as NameAttribute).Text;
else if (attribute is SummaryAttribute)
builder.Summary = (attribute as SummaryAttribute).Text;
else if (attribute is RemarksAttribute)
builder.Remarks = (attribute as RemarksAttribute).Text;
else if (attribute is AliasAttribute)
builder.AddAliases((attribute as AliasAttribute).Aliases);
else if (attribute is GroupAttribute)
switch (attribute)
{
var groupAttr = attribute as GroupAttribute;
builder.Name = builder.Name ?? groupAttr.Prefix;
builder.AddAliases(groupAttr.Prefix);
case NameAttribute name:
builder.Name = name.Text;
break;
case SummaryAttribute summary:
builder.Summary = summary.Text;
break;
case RemarksAttribute remarks:
builder.Remarks = remarks.Text;
break;
case AliasAttribute alias:
builder.AddAliases(alias.Aliases);
break;
case GroupAttribute group:
builder.Name = builder.Name ?? group.Prefix;
builder.AddAliases(group.Prefix);
break;
case PreconditionAttribute precondition:
builder.AddPrecondition(precondition);
break;
default:
builder.AddAttributes(attribute);
break;
}
else if (attribute is PreconditionAttribute)
builder.AddPrecondition(attribute as PreconditionAttribute);
}

//Check for unspecified info
@@ -123,26 +151,35 @@ namespace Discord.Commands
foreach (var attribute in attributes)
{
// TODO: C#7 type switch
if (attribute is CommandAttribute)
switch (attribute)
{
var cmdAttr = attribute as CommandAttribute;
builder.AddAliases(cmdAttr.Text);
builder.RunMode = cmdAttr.RunMode;
builder.Name = builder.Name ?? cmdAttr.Text;
case CommandAttribute command:
builder.AddAliases(command.Text);
builder.RunMode = command.RunMode;
builder.Name = builder.Name ?? command.Text;
break;
case NameAttribute name:
builder.Name = name.Text;
break;
case PriorityAttribute priority:
builder.Priority = priority.Priority;
break;
case SummaryAttribute summary:
builder.Summary = summary.Text;
break;
case RemarksAttribute remarks:
builder.Remarks = remarks.Text;
break;
case AliasAttribute alias:
builder.AddAliases(alias.Aliases);
break;
case PreconditionAttribute precondition:
builder.AddPrecondition(precondition);
break;
default:
builder.AddAttributes(attribute);
break;
}
else if (attribute is NameAttribute)
builder.Name = (attribute as NameAttribute).Text;
else if (attribute is PriorityAttribute)
builder.Priority = (attribute as PriorityAttribute).Priority;
else if (attribute is SummaryAttribute)
builder.Summary = (attribute as SummaryAttribute).Text;
else if (attribute is RemarksAttribute)
builder.Remarks = (attribute as RemarksAttribute).Text;
else if (attribute is AliasAttribute)
builder.AddAliases((attribute as AliasAttribute).Aliases);
else if (attribute is PreconditionAttribute)
builder.AddPrecondition(attribute as PreconditionAttribute);
}

if (builder.Name == null)
@@ -160,21 +197,34 @@ namespace Discord.Commands

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

builder.Callback = (ctx, args, map) =>
async Task<IResult> ExecuteCallback(ICommandContext context, object[] args, IServiceProvider services, CommandInfo cmd)
{
var instance = createInstance(map);
instance.SetContext(ctx);
var instance = createInstance(services);
instance.SetContext(context);

try
{
instance.BeforeExecute();
return method.Invoke(instance, args) as Task ?? Task.Delay(0);
instance.BeforeExecute(cmd);

var task = method.Invoke(instance, args) as Task ?? Task.Delay(0);
if (task is Task<RuntimeResult> resultTask)
{
return await resultTask.ConfigureAwait(false);
}
else
{
await task.ConfigureAwait(false);
return ExecuteResult.FromSuccess();
}
}
finally
{
instance.AfterExecute();
instance.AfterExecute(cmd);
(instance as IDisposable)?.Dispose();
}
};
}

builder.Callback = ExecuteCallback;
}

private static void BuildParameter(ParameterBuilder builder, System.Reflection.ParameterInfo paramInfo, int position, int count, CommandService service)
@@ -189,24 +239,30 @@ namespace Discord.Commands

foreach (var attribute in attributes)
{
// 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;
paramType = paramType.GetElementType();
}
else if (attribute is RemainderAttribute)
switch (attribute)
{
if (position != count-1)
throw new InvalidOperationException("Remainder parameters must be the last parameter in a command.");
builder.IsRemainder = true;
case SummaryAttribute summary:
builder.Summary = summary.Text;
break;
case OverrideTypeReaderAttribute typeReader:
builder.TypeReader = GetTypeReader(service, paramType, typeReader.TypeReader);
break;
case ParamArrayAttribute _:
builder.IsMultiple = true;
paramType = paramType.GetElementType();
break;
case ParameterPreconditionAttribute precon:
builder.AddPrecondition(precon);
break;
case RemainderAttribute _:
if (position != count - 1)
throw new InvalidOperationException($"Remainder parameters must be the last parameter in a command. Parameter: {paramInfo.Name} in {paramInfo.Member.DeclaringType.Name}.{paramInfo.Member.Name}");

builder.IsRemainder = true;
break;
default:
builder.AddAttributes(attribute);
break;
}
}

@@ -237,7 +293,7 @@ namespace Discord.Commands
}

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

return reader;
@@ -252,9 +308,9 @@ namespace Discord.Commands
private static bool IsValidCommandDefinition(MethodInfo methodInfo)
{
return methodInfo.IsDefined(typeof(CommandAttribute)) &&
methodInfo.ReturnType == typeof(Task) &&
(methodInfo.ReturnType == typeof(Task) || methodInfo.ReturnType == typeof(Task<RuntimeResult>)) &&
!methodInfo.IsStatic &&
!methodInfo.IsGenericMethod;
}
}
}
}

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

@@ -8,7 +8,8 @@ namespace Discord.Commands.Builders
{
public class ParameterBuilder
{
private readonly List<ParameterPreconditionAttribute> _preconditions;
private readonly List<ParameterPreconditionAttribute> _preconditions;
private readonly List<Attribute> _attributes;

public CommandBuilder Command { get; }
public string Name { get; internal set; }
@@ -22,11 +23,13 @@ namespace Discord.Commands.Builders
public string Summary { get; set; }

public IReadOnlyList<ParameterPreconditionAttribute> Preconditions => _preconditions;
public IReadOnlyList<Attribute> Attributes => _attributes;

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

Command = command;
}
@@ -49,7 +52,7 @@ namespace Discord.Commands.Builders
TypeReader = Command.Module.Service.GetDefaultTypeReader(type);

if (TypeReader == null)
throw new InvalidOperationException($"{type} does not have a TypeReader registered for it");
throw new InvalidOperationException($"{type} does not have a TypeReader registered for it. Parameter: {Name} in {Command.PrimaryAlias}");

if (type.GetTypeInfo().IsValueType)
DefaultValue = Activator.CreateInstance(type);
@@ -84,6 +87,11 @@ namespace Discord.Commands.Builders
return this;
}

public ParameterBuilder AddAttributes(params Attribute[] attributes)
{
_attributes.AddRange(attributes);
return this;
}
public ParameterBuilder AddPrecondition(ParameterPreconditionAttribute precondition)
{
_preconditions.Add(precondition);
@@ -93,7 +101,7 @@ namespace Discord.Commands.Builders
internal ParameterInfo Build(CommandInfo info)
{
if (TypeReader == null)
throw new InvalidOperationException($"No default TypeReader found, one must be specified");
throw new InvalidOperationException($"No type reader found for type {ParameterType.Name}, one must be specified");

return new ParameterInfo(this, info, Command.Module.Service);
}


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

@@ -18,6 +18,9 @@
UnmetPrecondition,

//Execute
Exception
Exception,

//Runtime
Unsuccessful
}
}

+ 17
- 0
src/Discord.Net.Commands/CommandException.cs View File

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

namespace Discord.Commands
{
public class CommandException : Exception
{
public CommandInfo Command { get; }
public ICommandContext Context { get; }

public CommandException(CommandInfo command, ICommandContext context, Exception ex)
: base($"Error occurred executing {command.GetLogText(context)}.", ex)
{
Command = command;
Context = context;
}
}
}

+ 11
- 9
src/Discord.Net.Commands/CommandMatch.cs View File

@@ -1,5 +1,7 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;

namespace Discord.Commands
{
@@ -14,13 +16,13 @@ namespace Discord.Commands
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);
public Task<PreconditionResult> CheckPreconditionsAsync(ICommandContext context, IServiceProvider services = null)
=> Command.CheckPreconditionsAsync(context, services);
public Task<ParseResult> ParseAsync(ICommandContext context, SearchResult searchResult, PreconditionResult preconditionResult = null, IServiceProvider services = null)
=> Command.ParseAsync(context, Alias.Length, searchResult, preconditionResult, services);
public Task<IResult> ExecuteAsync(ICommandContext context, IEnumerable<object> argList, IEnumerable<object> paramList, IServiceProvider services)
=> Command.ExecuteAsync(context, argList, paramList, services);
public Task<IResult> ExecuteAsync(ICommandContext context, ParseResult parseResult, IServiceProvider services)
=> Command.ExecuteAsync(context, parseResult, services);
}
}

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

@@ -1,4 +1,5 @@
using System.Collections.Immutable;
using System;
using System.Collections.Immutable;
using System.Text;
using System.Threading.Tasks;

@@ -13,7 +14,7 @@ namespace Discord.Commands
QuotedParameter
}
public static async Task<ParseResult> ParseArgs(CommandInfo command, ICommandContext context, string input, int startPos)
public static async Task<ParseResult> ParseArgs(CommandInfo command, ICommandContext context, IServiceProvider services, string input, int startPos)
{
ParameterInfo curParam = null;
StringBuilder argBuilder = new StringBuilder(input.Length);
@@ -110,7 +111,7 @@ namespace Discord.Commands
if (curParam == null)
return ParseResult.FromError(CommandError.BadArgCount, "The input text has too many parameters.");

var typeReaderResult = await curParam.Parse(context, argString).ConfigureAwait(false);
var typeReaderResult = await curParam.Parse(context, argString, services).ConfigureAwait(false);
if (!typeReaderResult.IsSuccess && typeReaderResult.Error != CommandError.MultipleMatches)
return ParseResult.FromError(typeReaderResult);

@@ -133,7 +134,7 @@ namespace Discord.Commands

if (curParam != null && curParam.IsRemainder)
{
var typeReaderResult = await curParam.Parse(context, argBuilder.ToString()).ConfigureAwait(false);
var typeReaderResult = await curParam.Parse(context, argBuilder.ToString(), services).ConfigureAwait(false);
if (!typeReaderResult.IsSuccess)
return ParseResult.FromError(typeReaderResult);
argList.Add(typeReaderResult);


+ 110
- 58
src/Discord.Net.Commands/CommandService.cs View File

@@ -1,4 +1,6 @@
using System;
using Discord.Commands.Builders;
using Discord.Logging;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
@@ -6,13 +8,15 @@ using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;

using Discord.Commands.Builders;
using Microsoft.Extensions.DependencyInjection;

namespace Discord.Commands
{
public class CommandService
{
public event Func<LogMessage, Task> Log { add { _logEvent.Add(value); } remove { _logEvent.Remove(value); } }
internal readonly AsyncEvent<Func<LogMessage, Task>> _logEvent = new AsyncEvent<Func<LogMessage, Task>>();

private readonly SemaphoreSlim _moduleLock;
private readonly ConcurrentDictionary<Type, ModuleInfo> _typedModuleDefs;
private readonly ConcurrentDictionary<Type, ConcurrentDictionary<Type, TypeReader>> _typeReaders;
@@ -21,22 +25,29 @@ namespace Discord.Commands
private readonly HashSet<ModuleInfo> _moduleDefs;
private readonly CommandMap _map;

internal readonly bool _caseSensitive;
internal readonly bool _caseSensitive, _throwOnError;
internal readonly char _separatorChar;
internal readonly RunMode _defaultRunMode;
internal readonly Logger _cmdLogger;
internal readonly LogManager _logManager;

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 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;
_throwOnError = config.ThrowOnError;
_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");
throw new InvalidOperationException("The default run mode cannot be set to Default.");

_logManager = new LogManager(config.LogLevel);
_logManager.Message += async msg => await _logEvent.InvokeAsync(msg).ConfigureAwait(false);
_cmdLogger = _logManager.CreateLogger("Command");

_moduleLock = new SemaphoreSlim(1, 1);
_typedModuleDefs = new ConcurrentDictionary<Type, ModuleInfo>();
@@ -48,6 +59,9 @@ namespace Discord.Commands
foreach (var type in PrimitiveParsers.SupportedTypes)
_defaultTypeReaders[type] = PrimitiveTypeReader.Create(type);

_defaultTypeReaders[typeof(string)] =
new PrimitiveTypeReader<string>((string x, out string y) => { y = x; return true; }, 0);

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<>)));
@@ -73,20 +87,21 @@ namespace Discord.Commands
_moduleLock.Release();
}
}
public async Task<ModuleInfo> AddModuleAsync<T>()
public Task<ModuleInfo> AddModuleAsync<T>() => AddModuleAsync(typeof(T));
public async Task<ModuleInfo> AddModuleAsync(Type type)
{
await _moduleLock.WaitAsync().ConfigureAwait(false);
try
{
var typeInfo = typeof(T).GetTypeInfo();
var typeInfo = type.GetTypeInfo();

if (_typedModuleDefs.ContainsKey(typeof(T)))
if (_typedModuleDefs.ContainsKey(type))
throw new ArgumentException($"This module has already been added.");

var module = ModuleClassBuilder.Build(this, typeInfo).FirstOrDefault();
var module = (await ModuleClassBuilder.BuildAsync(this, typeInfo).ConfigureAwait(false)).FirstOrDefault();

if (module.Value == default(ModuleInfo))
throw new InvalidOperationException($"Could not build the module {typeof(T).FullName}, did you pass an invalid type?");
throw new InvalidOperationException($"Could not build the module {type.FullName}, did you pass an invalid type?");

_typedModuleDefs[module.Key] = module.Value;

@@ -102,8 +117,8 @@ namespace Discord.Commands
await _moduleLock.WaitAsync().ConfigureAwait(false);
try
{
var types = ModuleClassBuilder.Search(assembly).ToArray();
var moduleDefs = ModuleClassBuilder.Build(types, this);
var types = await ModuleClassBuilder.SearchAsync(assembly, this).ConfigureAwait(false);
var moduleDefs = await ModuleClassBuilder.BuildAsync(types, this).ConfigureAwait(false);

foreach (var info in moduleDefs)
{
@@ -143,14 +158,13 @@ namespace Discord.Commands
_moduleLock.Release();
}
}
public async Task<bool> RemoveModuleAsync<T>()
public Task<bool> RemoveModuleAsync<T>() => RemoveModuleAsync(typeof(T));
public async Task<bool> RemoveModuleAsync(Type type)
{
await _moduleLock.WaitAsync().ConfigureAwait(false);
try
{
ModuleInfo module;
_typedModuleDefs.TryGetValue(typeof(T), out module);
if (module == default(ModuleInfo))
if (!_typedModuleDefs.TryRemove(type, out var module))
return false;

return RemoveModuleInternal(module);
@@ -184,20 +198,18 @@ namespace Discord.Commands
}
public void AddTypeReader(Type type, TypeReader reader)
{
var readers = _typeReaders.GetOrAdd(type, x=> new ConcurrentDictionary<Type, TypeReader>());
var readers = _typeReaders.GetOrAdd(type, x => new ConcurrentDictionary<Type, TypeReader>());
readers[reader.GetType()] = reader;
}
internal IDictionary<Type, TypeReader> GetTypeReaders(Type type)
{
ConcurrentDictionary<Type, TypeReader> definedTypeReaders;
if (_typeReaders.TryGetValue(type, out definedTypeReaders))
if (_typeReaders.TryGetValue(type, out var definedTypeReaders))
return definedTypeReaders;
return null;
}
internal TypeReader GetDefaultTypeReader(Type type)
{
TypeReader reader;
if (_defaultTypeReaders.TryGetValue(type, out reader))
if (_defaultTypeReaders.TryGetValue(type, out var reader))
return reader;
var typeInfo = type.GetTypeInfo();

@@ -223,70 +235,110 @@ namespace Discord.Commands
}

//Execution
public SearchResult Search(ICommandContext context, int argPos)
public SearchResult Search(ICommandContext context, int argPos)
=> Search(context, context.Message.Content.Substring(argPos));
public SearchResult Search(ICommandContext context, string input)
{
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);
else
return SearchResult.FromError(CommandError.UnknownCommand, "Unknown command.");
}

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(ICommandContext context, string input, IDependencyMap dependencyMap = null, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception)
public Task<IResult> ExecuteAsync(ICommandContext context, int argPos, IServiceProvider services = null, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception)
=> ExecuteAsync(context, context.Message.Content.Substring(argPos), services, multiMatchHandling);
public async Task<IResult> ExecuteAsync(ICommandContext context, string input, IServiceProvider services = null, MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception)
{
dependencyMap = dependencyMap ?? DependencyMap.Empty;
services = services ?? EmptyServiceProvider.Instance;

var searchResult = Search(context, input);
if (!searchResult.IsSuccess)
return searchResult;

var commands = searchResult.Commands;
for (int i = commands.Count - 1; i >= 0; i--)
var preconditionResults = new Dictionary<CommandMatch, PreconditionResult>();

foreach (var match in commands)
{
var preconditionResult = await commands[i].CheckPreconditionsAsync(context, dependencyMap).ConfigureAwait(false);
if (!preconditionResult.IsSuccess)
{
if (commands.Count == 1)
return preconditionResult;
else
continue;
}
preconditionResults[match] = await match.Command.CheckPreconditionsAsync(context, services).ConfigureAwait(false);
}

var successfulPreconditions = preconditionResults
.Where(x => x.Value.IsSuccess)
.ToArray();

var parseResult = await commands[i].ParseAsync(context, searchResult, preconditionResult).ConfigureAwait(false);
if (!parseResult.IsSuccess)
if (successfulPreconditions.Length == 0)
{
//All preconditions failed, return the one from the highest priority command
var bestCandidate = preconditionResults
.OrderByDescending(x => x.Key.Command.Priority)
.FirstOrDefault(x => !x.Value.IsSuccess);
return bestCandidate.Value;
}

//If we get this far, at least one precondition was successful.

var parseResultsDict = new Dictionary<CommandMatch, ParseResult>();
foreach (var pair in successfulPreconditions)
{
var parseResult = await pair.Key.ParseAsync(context, searchResult, pair.Value, services).ConfigureAwait(false);

if (parseResult.Error == CommandError.MultipleMatches)
{
if (parseResult.Error == CommandError.MultipleMatches)
IReadOnlyList<TypeReaderValue> argList, paramList;
switch (multiMatchHandling)
{
IReadOnlyList<TypeReaderValue> argList, paramList;
switch (multiMatchHandling)
{
case MultiMatchHandling.Best:
argList = parseResult.ArgValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray();
paramList = parseResult.ParamValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray();
parseResult = ParseResult.FromSuccess(argList, paramList);
break;
}
case MultiMatchHandling.Best:
argList = parseResult.ArgValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray();
paramList = parseResult.ParamValues.Select(x => x.Values.OrderByDescending(y => y.Score).First()).ToImmutableArray();
parseResult = ParseResult.FromSuccess(argList, paramList);
break;
}
}

if (!parseResult.IsSuccess)
{
if (commands.Count == 1)
return parseResult;
else
continue;
}
parseResultsDict[pair.Key] = parseResult;
}

// Calculates the 'score' of a command given a parse result
float CalculateScore(CommandMatch match, ParseResult parseResult)
{
float argValuesScore = 0, paramValuesScore = 0;
if (match.Command.Parameters.Count > 0)
{
var argValuesSum = parseResult.ArgValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0;
var paramValuesSum = parseResult.ParamValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score) ?? 0;

argValuesScore = argValuesSum / match.Command.Parameters.Count;
paramValuesScore = paramValuesSum / match.Command.Parameters.Count;
}

return await commands[i].ExecuteAsync(context, parseResult, dependencyMap).ConfigureAwait(false);
var totalArgsScore = (argValuesScore + paramValuesScore) / 2;
return match.Command.Priority + totalArgsScore * 0.99f;
}

//Order the parse results by their score so that we choose the most likely result to execute
var parseResults = parseResultsDict
.OrderByDescending(x => CalculateScore(x.Key, x.Value));

var successfulParses = parseResults
.Where(x => x.Value.IsSuccess)
.ToArray();

if (successfulParses.Length == 0)
{
//All parses failed, return the one from the highest priority command, using score as a tie breaker
var bestMatch = parseResults
.FirstOrDefault(x => !x.Value.IsSuccess);
return bestMatch.Value;
}

return SearchResult.FromError(CommandError.UnknownCommand, "This input does not match any overload.");
//If we get this far, at least one parse was successful. Execute the most likely overload.
var chosenOverload = successfulParses[0];
return await chosenOverload.Key.ExecuteAsync(context, chosenOverload.Value, services).ConfigureAwait(false);
}
}
}

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

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

/// <summary> Gets or sets the minimum log level severity that will be sent to the Log event. </summary>
public LogSeverity LogLevel { get; set; } = LogSeverity.Info;

/// <summary> Gets or sets whether RunMode.Sync commands should push exceptions up to the caller. </summary>
public bool ThrowOnError { get; set; } = true;
}
}
}

+ 0
- 98
src/Discord.Net.Commands/Dependencies/DependencyMap.cs View File

@@ -1,98 +0,0 @@
using System;
using System.Collections.Generic;

namespace Discord.Commands
{
public class DependencyMap : IDependencyMap
{
private Dictionary<Type, Func<object>> map;

public static DependencyMap Empty => new DependencyMap();

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

/// <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, 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;
if (!TryGet(t, out result))
throw new KeyNotFoundException($"The dependency map does not contain \"{t.FullName}\"");
else
return result;
}

/// <inheritdoc />
public bool TryGet<T>(out T result)
{
object untypedResult;
if (TryGet(typeof(T), out untypedResult))
{
result = (T)untypedResult;
return true;
}
else
{
result = default(T);
return false;
}
}
/// <inheritdoc />
public bool TryGet(Type t, out object result)
{
Func<object> func;
if (map.TryGetValue(t, out func))
{
result = func();
return true;
}
result = null;
return false;
}
}
}

+ 0
- 89
src/Discord.Net.Commands/Dependencies/IDependencyMap.cs View File

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

namespace Discord.Commands
{
public interface IDependencyMap
{
/// <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);
}
}

+ 7
- 18
src/Discord.Net.Commands/Discord.Net.Commands.csproj View File

@@ -1,26 +1,15 @@
<Project ToolsVersion="15.0" Sdk="Microsoft.NET.Sdk" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../../Discord.Net.targets" />
<PropertyGroup>
<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>
<RootNamespace>Discord.Commands</RootNamespace>
<DisableImplicitFrameworkReferences>true</DisableImplicitFrameworkReferences>
<Description>A Discord.Net extension adding support for bot commands.</Description>
<TargetFrameworks>netstandard1.1</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Discord.Net.Core\Discord.Net.Core.csproj" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<NoWarn>$(NoWarn);CS1573;CS1591</NoWarn>
<WarningsAsErrors>true</WarningsAsErrors>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="1.1.1" />
</ItemGroup>
</Project>

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

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

namespace Discord.Commands
{
internal class EmptyServiceProvider : IServiceProvider
{
public static readonly EmptyServiceProvider Instance = new EmptyServiceProvider();
public object GetService(Type serviceType) => null;
}
}

+ 5
- 3
src/Discord.Net.Commands/Extensions/MessageExtensions.cs View File

@@ -1,4 +1,6 @@
namespace Discord.Commands
using System;

namespace Discord.Commands
{
public static class MessageExtensions
{
@@ -12,10 +14,10 @@
}
return false;
}
public static bool HasStringPrefix(this IUserMessage msg, string str, ref int argPos)
public static bool HasStringPrefix(this IUserMessage msg, string str, ref int argPos, StringComparison comparisonType = StringComparison.Ordinal)
{
var text = msg.Content;
if (text.StartsWith(str))
if (text.StartsWith(str, comparisonType))
{
argPos = str.Length;
return true;


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

@@ -4,8 +4,8 @@
{
void SetContext(ICommandContext context);

void BeforeExecute();
void BeforeExecute(CommandInfo command);
void AfterExecute();
void AfterExecute(CommandInfo command);
}
}

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

Loading…
Cancel
Save