├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── nuget-publish.yml ├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── DSharpPlus.SlashCommands.sln ├── DSharpPlus.SlashCommands ├── Attributes │ ├── DefaultParameterAttribute.cs │ ├── SlashCommandAttribute.cs │ ├── SlashSubcommandAttribute.cs │ └── SlashSubcommandGroupAttribute.cs ├── DSharpPlus.SlashCommands.csproj ├── DiscordSlashClient.cs ├── DiscordSlashConfiguration.cs ├── Entities │ ├── ApplicationCommandOptionChoice.cs │ ├── BaseSlashCommandModule.cs │ ├── Builders │ │ ├── ApplicationCommandBuilder.cs │ │ ├── ApplicationCommandOptionBuilder.cs │ │ ├── ApplicationCommandOptionChoiceBuilder.cs │ │ ├── IBuilder.cs │ │ ├── InteractionApplicationCommandCallbackDataBuilder.cs │ │ └── InteractionResponseBuilder.cs │ ├── InteractionApplicationCommandCallbackData.cs │ ├── InteractionContext.cs │ ├── InteractionResponse.cs │ ├── SlashCommand.cs │ ├── SlashCommandConfiguration.cs │ ├── SlashSubcommand.cs │ └── SlashSubcommandGroup.cs ├── Enums │ └── ApplicationCommandOptionType.cs ├── LICENSE └── Services │ └── SlashCommandHandlingService.cs ├── ExampleGatewayBot ├── Commands │ └── SlashCommandTesting.cs ├── Configuration Examples │ └── bot_config.json ├── ExampleGatewayBot.csproj └── Program.cs ├── ExampleHTTPBot ├── Api │ └── DiscordSlashCommandController.cs ├── Commands │ ├── Discord │ │ └── PingDiscordCommand.cs │ └── Slash │ │ ├── ArgumentExampleCommand.cs │ │ ├── ArgumentSubcommandCommand.cs │ │ ├── HelloWorldSlashCommand.cs │ │ └── SubcommandExampleSlashCommand.cs ├── Configuration Examples │ └── bot_config.json ├── ExampleHTTPBot.csproj ├── Program.cs ├── Properties │ └── launchSettings.json ├── Startup.cs ├── Utils.cs ├── appsettings.Development.json ├── appsettings.json └── sccfg_774671144719482881.json ├── LICENSE ├── NuGet.config └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | ko_fi: soyvolon 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve the Lib 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | # Bug 11 | ## Description 12 | A clear and concise description of what the bug is. Please include the Lib version. 13 | 14 | ## To Reproduce 15 | Steps to reproduce the behavior: 16 | 1. Go to '...' 17 | 2. Click on '....' 18 | 3. Scroll down to '....' 19 | 4. See error 20 | 21 | ## Expected behavior 22 | A clear and concise description of what you expected to happen. 23 | 24 | ## Notes 25 | Add any other context about the problem here. 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | # Feature 11 | A clear and concise description of what the feature is 12 | 13 | ## Potential Solution 14 | A concise idea about how this could be implemented. 15 | 16 | ## Notes 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/workflows/nuget-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to Nuget 2 | 3 | on: 4 | release: 5 | types: [released] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Setup .NET 15 | uses: actions/setup-dotnet@v1 16 | with: 17 | dotnet-version: 5.0.100 18 | - name: Restore dependencies 19 | run: dotnet restore 20 | - name: Build 21 | run: dotnet build --no-restore 22 | - name: Test 23 | run: dotnet test --no-build --verbosity normal 24 | - name: Update Nuget Package 25 | uses: brandedoutcast/publish-nuget@v2.5.5 26 | with: 27 | PROJECT_FILE_PATH: DSharpPlus.SlashCommands/DSharpPlus.SlashCommands.csproj 28 | NUGET_KEY: ${{ secrets.NUGET_API_KEY }} 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # Custom-Ignore 7 | *[Cc]onfig/ 8 | 9 | # User-specific files 10 | *.rsuser 11 | *.suo 12 | *.user 13 | *.userosscache 14 | *.sln.docstates 15 | 16 | # User-specific files (MonoDevelop/Xamarin Studio) 17 | *.userprefs 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | 33 | # Visual Studio 2015/2017 cache/options directory 34 | .vs/ 35 | # Uncomment if you have tasks that create the project's static files in wwwroot 36 | #wwwroot/ 37 | 38 | # Visual Studio 2017 auto generated files 39 | Generated\ Files/ 40 | 41 | # MSTest test Results 42 | [Tt]est[Rr]esult*/ 43 | [Bb]uild[Ll]og.* 44 | 45 | # NUNIT 46 | *.VisualState.xml 47 | TestResult.xml 48 | 49 | # Build Results of an ATL Project 50 | [Dd]ebugPS/ 51 | [Rr]eleasePS/ 52 | dlldata.c 53 | 54 | # Benchmark Results 55 | BenchmarkDotNet.Artifacts/ 56 | 57 | # .NET Core 58 | project.lock.json 59 | project.fragment.lock.json 60 | artifacts/ 61 | 62 | # StyleCop 63 | StyleCopReport.xml 64 | 65 | # Files built by Visual Studio 66 | *_i.c 67 | *_p.c 68 | *_h.h 69 | *.ilk 70 | *.meta 71 | *.obj 72 | *.iobj 73 | *.pch 74 | *.pdb 75 | *.ipdb 76 | *.pgc 77 | *.pgd 78 | *.rsp 79 | *.sbr 80 | *.tlb 81 | *.tli 82 | *.tlh 83 | *.tmp 84 | *.tmp_proj 85 | *_wpftmp.csproj 86 | *.log 87 | *.vspscc 88 | *.vssscc 89 | .builds 90 | *.pidb 91 | *.svclog 92 | *.scc 93 | 94 | # Chutzpah Test files 95 | _Chutzpah* 96 | 97 | # Visual C++ cache files 98 | ipch/ 99 | *.aps 100 | *.ncb 101 | *.opendb 102 | *.opensdf 103 | *.sdf 104 | *.cachefile 105 | *.VC.db 106 | *.VC.VC.opendb 107 | 108 | # Visual Studio profiler 109 | *.psess 110 | *.vsp 111 | *.vspx 112 | *.sap 113 | 114 | # Visual Studio Trace Files 115 | *.e2e 116 | 117 | # TFS 2012 Local Workspace 118 | $tf/ 119 | 120 | # Guidance Automation Toolkit 121 | *.gpState 122 | 123 | # ReSharper is a .NET coding add-in 124 | _ReSharper*/ 125 | *.[Rr]e[Ss]harper 126 | *.DotSettings.user 127 | 128 | # JustCode is a .NET coding add-in 129 | .JustCode 130 | 131 | # TeamCity is a build add-in 132 | _TeamCity* 133 | 134 | # DotCover is a Code Coverage Tool 135 | *.dotCover 136 | 137 | # AxoCover is a Code Coverage Tool 138 | .axoCover/* 139 | !.axoCover/settings.json 140 | 141 | # Visual Studio code coverage results 142 | *.coverage 143 | *.coveragexml 144 | 145 | # NCrunch 146 | _NCrunch_* 147 | .*crunch*.local.xml 148 | nCrunchTemp_* 149 | 150 | # MightyMoose 151 | *.mm.* 152 | AutoTest.Net/ 153 | 154 | # Web workbench (sass) 155 | .sass-cache/ 156 | 157 | # Installshield output folder 158 | [Ee]xpress/ 159 | 160 | # DocProject is a documentation generator add-in 161 | DocProject/buildhelp/ 162 | DocProject/Help/*.HxT 163 | DocProject/Help/*.HxC 164 | DocProject/Help/*.hhc 165 | DocProject/Help/*.hhk 166 | DocProject/Help/*.hhp 167 | DocProject/Help/Html2 168 | DocProject/Help/html 169 | 170 | # Click-Once directory 171 | publish/ 172 | 173 | # Publish Web Output 174 | *.[Pp]ublish.xml 175 | *.azurePubxml 176 | # Note: Comment the next line if you want to checkin your web deploy settings, 177 | # but database connection strings (with potential passwords) will be unencrypted 178 | *.pubxml 179 | *.publishproj 180 | 181 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 182 | # checkin your Azure Web App publish settings, but sensitive information contained 183 | # in these scripts will be unencrypted 184 | PublishScripts/ 185 | 186 | # NuGet Packages 187 | *.nupkg 188 | # The packages folder can be ignored because of Package Restore 189 | **/[Pp]ackages/* 190 | # except build/, which is used as an MSBuild target. 191 | !**/[Pp]ackages/build/ 192 | # Uncomment if necessary however generally it will be regenerated when needed 193 | #!**/[Pp]ackages/repositories.config 194 | # NuGet v3's project.json files produces more ignorable files 195 | *.nuget.props 196 | *.nuget.targets 197 | 198 | # Microsoft Azure Build Output 199 | csx/ 200 | *.build.csdef 201 | 202 | # Microsoft Azure Emulator 203 | ecf/ 204 | rcf/ 205 | 206 | # Windows Store app package directories and files 207 | AppPackages/ 208 | BundleArtifacts/ 209 | Package.StoreAssociation.xml 210 | _pkginfo.txt 211 | *.appx 212 | 213 | # Visual Studio cache files 214 | # files ending in .cache can be ignored 215 | *.[Cc]ache 216 | # but keep track of directories ending in .cache 217 | !?*.[Cc]ache/ 218 | 219 | # Others 220 | ClientBin/ 221 | ~$* 222 | *~ 223 | *.dbmdl 224 | *.dbproj.schemaview 225 | *.jfm 226 | *.pfx 227 | *.publishsettings 228 | orleans.codegen.cs 229 | 230 | # Including strong name files can present a security risk 231 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 232 | #*.snk 233 | 234 | # Since there are multiple workflows, uncomment next line to ignore bower_components 235 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 236 | #bower_components/ 237 | 238 | # RIA/Silverlight projects 239 | Generated_Code/ 240 | 241 | # Backup & report files from converting an old project file 242 | # to a newer Visual Studio version. Backup files are not needed, 243 | # because we have git ;-) 244 | _UpgradeReport_Files/ 245 | Backup*/ 246 | UpgradeLog*.XML 247 | UpgradeLog*.htm 248 | ServiceFabricBackup/ 249 | *.rptproj.bak 250 | 251 | # SQL Server files 252 | *.mdf 253 | *.ldf 254 | *.ndf 255 | 256 | # Business Intelligence projects 257 | *.rdl.data 258 | *.bim.layout 259 | *.bim_*.settings 260 | *.rptproj.rsuser 261 | *- Backup*.rdl 262 | 263 | # Microsoft Fakes 264 | FakesAssemblies/ 265 | 266 | # GhostDoc plugin setting file 267 | *.GhostDoc.xml 268 | 269 | # Node.js Tools for Visual Studio 270 | .ntvs_analysis.dat 271 | node_modules/ 272 | 273 | # Visual Studio 6 build log 274 | *.plg 275 | 276 | # Visual Studio 6 workspace options file 277 | *.opt 278 | 279 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 280 | *.vbw 281 | 282 | # Visual Studio LightSwitch build output 283 | **/*.HTMLClient/GeneratedArtifacts 284 | **/*.DesktopClient/GeneratedArtifacts 285 | **/*.DesktopClient/ModelManifest.xml 286 | **/*.Server/GeneratedArtifacts 287 | **/*.Server/ModelManifest.xml 288 | _Pvt_Extensions 289 | 290 | # Paket dependency manager 291 | .paket/paket.exe 292 | paket-files/ 293 | 294 | # FAKE - F# Make 295 | .fake/ 296 | 297 | # JetBrains Rider 298 | .idea/ 299 | *.sln.iml 300 | 301 | # CodeRush personal settings 302 | .cr/personal 303 | 304 | # Python Tools for Visual Studio (PTVS) 305 | __pycache__/ 306 | *.pyc 307 | 308 | # Cake - Uncomment if you are using it 309 | # tools/** 310 | # !tools/packages.config 311 | 312 | # Tabs Studio 313 | *.tss 314 | 315 | # Telerik's JustMock configuration file 316 | *.jmconfig 317 | 318 | # BizTalk build output 319 | *.btp.cs 320 | *.btm.cs 321 | *.odx.cs 322 | *.xsd.cs 323 | 324 | # OpenCover UI analysis results 325 | OpenCover/ 326 | 327 | # Azure Stream Analytics local run output 328 | ASALocalRun/ 329 | 330 | # MSBuild Binary and Structured Log 331 | *.binlog 332 | 333 | # NVidia Nsight GPU debugger configuration file 334 | *.nvuser 335 | 336 | # MFractors (Xamarin productivity tool) working folder 337 | .mfractor/ 338 | 339 | # Local History for Visual Studio 340 | .localhistory/ 341 | 342 | # BeatPulse healthcheck temp database 343 | healthchecksdb -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (web)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/ExampleBot/bin/Debug/net5.0/ExampleBot.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}/ExampleBot", 16 | "stopAtEntry": false, 17 | // Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser 18 | "serverReadyAction": { 19 | "action": "openExternally", 20 | "pattern": "\\bNow listening on:\\s+(https?://\\S+)" 21 | }, 22 | "env": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | }, 25 | "sourceFileMap": { 26 | "/Views": "${workspaceFolder}/Views" 27 | } 28 | }, 29 | { 30 | "name": ".NET Core Attach", 31 | "type": "coreclr", 32 | "request": "attach", 33 | "processId": "${command:pickProcess}" 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "Linq", 4 | "Moinvaziri", 5 | "Newtonsoft", 6 | "Subcommand", 7 | "discordslash", 8 | "sccfg", 9 | "subcommands" 10 | ] 11 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/ExampleBot/ExampleBot.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/ExampleBot/ExampleBot.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "${workspaceFolder}/ExampleBot/ExampleBot.csproj", 36 | "/property:GenerateFullPaths=true", 37 | "/consoleloggerparameters:NoSummary" 38 | ], 39 | "problemMatcher": "$msCompile" 40 | } 41 | ] 42 | } -------------------------------------------------------------------------------- /DSharpPlus.SlashCommands.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30711.63 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DSharpPlus.SlashCommands", "DSharpPlus.SlashCommands\DSharpPlus.SlashCommands.csproj", "{30FF64B3-45E2-4776-A822-94073D4F92D9}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "1. sln", "1. sln", "{BB481774-975C-4AE6-B39D-CEF4030848B1}" 9 | ProjectSection(SolutionItems) = preProject 10 | .gitignore = .gitignore 11 | LICENSE = LICENSE 12 | NuGet.config = NuGet.config 13 | README.md = README.md 14 | EndProjectSection 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExampleGatewayBot", "ExampleGatewayBot\ExampleGatewayBot.csproj", "{D11DEA0A-E20E-4CA9-B7CD-98AE6E46D58A}" 17 | EndProject 18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExampleHTTPBot", "ExampleHTTPBot\ExampleHTTPBot.csproj", "{7D2C3FE2-CCE6-47F7-BE02-F1FE0D61F1A5}" 19 | EndProject 20 | Global 21 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 22 | Debug|Any CPU = Debug|Any CPU 23 | Release|Any CPU = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 26 | {30FF64B3-45E2-4776-A822-94073D4F92D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {30FF64B3-45E2-4776-A822-94073D4F92D9}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {30FF64B3-45E2-4776-A822-94073D4F92D9}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {30FF64B3-45E2-4776-A822-94073D4F92D9}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {D11DEA0A-E20E-4CA9-B7CD-98AE6E46D58A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {D11DEA0A-E20E-4CA9-B7CD-98AE6E46D58A}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {D11DEA0A-E20E-4CA9-B7CD-98AE6E46D58A}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {D11DEA0A-E20E-4CA9-B7CD-98AE6E46D58A}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {7D2C3FE2-CCE6-47F7-BE02-F1FE0D61F1A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {7D2C3FE2-CCE6-47F7-BE02-F1FE0D61F1A5}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {7D2C3FE2-CCE6-47F7-BE02-F1FE0D61F1A5}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {7D2C3FE2-CCE6-47F7-BE02-F1FE0D61F1A5}.Release|Any CPU.Build.0 = Release|Any CPU 38 | EndGlobalSection 39 | GlobalSection(SolutionProperties) = preSolution 40 | HideSolutionNode = FALSE 41 | EndGlobalSection 42 | GlobalSection(ExtensibilityGlobals) = postSolution 43 | SolutionGuid = {E978CC3F-A9D6-4EDF-AEBA-5F96A68F30BE} 44 | EndGlobalSection 45 | EndGlobal 46 | -------------------------------------------------------------------------------- /DSharpPlus.SlashCommands/Attributes/DefaultParameterAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DSharpPlus.SlashCommands.Attributes 4 | { 5 | [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] 6 | public class DefaultParameterAttribute : Attribute 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /DSharpPlus.SlashCommands/Attributes/SlashCommandAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DSharpPlus.SlashCommands.Attributes 4 | { 5 | /// 6 | /// Used to designate a class as a slash command. 7 | /// 8 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] 9 | public class SlashCommandAttribute : Attribute 10 | { 11 | public string Name { get; init; } 12 | public ulong? GuildId { get; init; } 13 | 14 | public SlashCommandAttribute(string name, ulong guildId = 0) 15 | { 16 | Name = name; 17 | GuildId = guildId == 0 ? null : guildId; 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /DSharpPlus.SlashCommands/Attributes/SlashSubcommandAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DSharpPlus.SlashCommands.Attributes 4 | { 5 | /// 6 | /// Defines a method as the default command for a command grouping. 7 | /// 8 | [AttributeUsage(AttributeTargets.Method)] 9 | public class SlashSubcommandAttribute : Attribute 10 | { 11 | public string Name { get; init; } 12 | 13 | public SlashSubcommandAttribute(string name) 14 | { 15 | Name = name; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /DSharpPlus.SlashCommands/Attributes/SlashSubcommandGroupAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DSharpPlus.SlashCommands.Attributes 4 | { 5 | [AttributeUsage(AttributeTargets.Class)] 6 | public class SlashSubcommandGroupAttribute : Attribute 7 | { 8 | public string Name { get; set; } 9 | public SlashSubcommandGroupAttribute(string name) 10 | { 11 | Name = name; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /DSharpPlus.SlashCommands/DSharpPlus.SlashCommands.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net5.0 5 | enable 6 | Soyvolon.DSharpPlus.SlashCommands 7 | 0.5.0 8 | Soyvolon 9 | LICENSE 10 | 11 | DSharpPlus based SlashCommnads 12 | >A SlashCommnad library that uses DSharpPlus to encoperate SlashCommands for Discord 13 | https://github.com/Soyvolon/DSharpPlus.SlashCommands 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /DSharpPlus.SlashCommands/DiscordSlashClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Reflection; 4 | using System.Threading.Tasks; 5 | 6 | using DSharpPlus.Entities; 7 | using DSharpPlus.EventArgs; 8 | using DSharpPlus.SlashCommands.Entities; 9 | using DSharpPlus.SlashCommands.Entities.Builders; 10 | using DSharpPlus.SlashCommands.Services; 11 | 12 | using Microsoft.AspNetCore.Http; 13 | using Microsoft.Extensions.DependencyInjection; 14 | using Microsoft.Extensions.Logging; 15 | 16 | using Newtonsoft.Json; 17 | using Newtonsoft.Json.Linq; 18 | 19 | namespace DSharpPlus.SlashCommands 20 | { 21 | public class DiscordSlashClient 22 | { 23 | private const string api = "https://discord.com/api/v8"; 24 | 25 | private ulong ApplicationId 26 | { 27 | get 28 | { 29 | if (this._discord is not null) 30 | return this._discord.CurrentApplication.Id; 31 | else if (this._sharded is not null) 32 | return this._sharded.CurrentApplication.Id; 33 | else return 0; 34 | } 35 | } 36 | 37 | private readonly IServiceProvider _services; 38 | private readonly IServiceProvider _internalServices; 39 | private readonly SlashCommandHandlingService _slash; 40 | private readonly DiscordSlashConfiguration _config; 41 | private readonly BaseDiscordClient? _discord; 42 | private readonly DiscordShardedClient? _sharded; 43 | private readonly HttpClient _http; 44 | private readonly ILogger _logger; 45 | private readonly JsonSerializerSettings _jsonSettings = new JsonSerializerSettings 46 | { 47 | NullValueHandling = NullValueHandling.Ignore, 48 | DefaultValueHandling = DefaultValueHandling.Ignore 49 | }; 50 | private const string _contentType = "application/json"; 51 | 52 | public DiscordSlashClient(DiscordSlashConfiguration config) 53 | { 54 | IServiceCollection internalServices = new ServiceCollection(); 55 | internalServices.AddSingleton() 56 | .AddSingleton(); 57 | 58 | this._config = config; 59 | this._discord = this._config.Client; 60 | this._sharded = this._config.ShardedClient; 61 | 62 | if (config.Logger is not null) 63 | { 64 | internalServices.AddSingleton(config.Logger); 65 | } 66 | else 67 | { 68 | if (_discord is not null) 69 | { 70 | internalServices.AddSingleton(_discord.Logger); 71 | } 72 | else if (_sharded is not null) 73 | { 74 | internalServices.AddSingleton(_sharded!.Logger); 75 | } 76 | } 77 | 78 | this._services = config.Services ?? new ServiceCollection().BuildServiceProvider(); 79 | this._internalServices = internalServices.BuildServiceProvider(); 80 | this._logger = this._internalServices.GetRequiredService(); 81 | this._http = this._internalServices.GetRequiredService(); 82 | this._slash = new SlashCommandHandlingService(this._services, this._http, this._logger); 83 | this._http.DefaultRequestHeaders.Authorization = new("Bot", this._config.Token); 84 | 85 | if (this._discord is null && this._sharded is null) 86 | throw new Exception("A Discord Client or Sharded Client is required."); 87 | } 88 | 89 | /// 90 | /// Add an assembly to register commands from. 91 | /// 92 | /// Assembly to register 93 | public void RegisterCommands(Assembly assembly) 94 | { 95 | _slash.WithCommandAssembly(assembly); 96 | } 97 | 98 | /// 99 | /// Starts the slash command client. 100 | /// 101 | /// Start operation 102 | public async Task StartAsync() 103 | { 104 | // Set this restriction to ensure proper response for async command handling. 105 | if ((_config.DefaultResponseType == InteractionResponseType.ChannelMessageWithSource) 106 | && _config.DefaultResponseData is null) 107 | throw new Exception("DeafultResponseData must not be null if not using ResponseType of ChannelMessageWithSource."); 108 | 109 | 110 | // Initialize the command handling service (and therefor updating command on discord). 111 | await _slash.StartAsync(_config.Token, ApplicationId); 112 | } 113 | 114 | public async Task HandleGatewayEvent(DiscordClient client, InteractionCreateEventArgs args) 115 | { 116 | await _slash.HandleInteraction(client, args.Interaction, this); 117 | 118 | var data = GetDeafultResponse().Build(); 119 | 120 | var msg = new HttpRequestMessage() 121 | { 122 | Method = HttpMethod.Post, 123 | RequestUri = GetGatewayFollowupUri(args.Interaction.Id.ToString(), args.Interaction.Token), 124 | Content = new StringContent(JsonConvert.SerializeObject(data, _jsonSettings)) 125 | }; 126 | 127 | msg.Content.Headers.ContentType = new(_contentType); 128 | 129 | var res = await _http.SendAsync(msg); 130 | 131 | return res.IsSuccessStatusCode; 132 | } 133 | 134 | /// 135 | /// Handle an incoming webhook request and return the default data to send back to Discord. 136 | /// 137 | /// HttpRequest for the interaction POST 138 | /// Handle Webhook operation 139 | public async Task HandleWebhookPost(string requestBody) 140 | { 141 | try 142 | {// Attempt to get the Interact object from the JSON ... 143 | var i = JsonConvert.DeserializeObject(requestBody); 144 | // ... and tell the handler to run the command ... 145 | 146 | //var jobj = JObject.Parse(requestBody); 147 | //DiscordUser? user = jobj["member"]?["user"]?.ToObject(); 148 | //// ... because we cant serialize direct to a DiscordMember, we are working around this 149 | //// and using a DiscordUser instead. I would have to set the Lib as upstream to this before I 150 | //// would be able to change this. 151 | //i.User = user; 152 | 153 | var client = GetBaseClientForRequest(i.GuildId); 154 | 155 | await _slash.HandleInteraction(client, i, this); 156 | } 157 | catch (Exception ex) 158 | { // ... if it errors, log and return null. 159 | _logger.LogError(ex, "Webhook Handler failed."); 160 | return null; 161 | } 162 | 163 | return GetDeafultResponse().Build(); 164 | } 165 | 166 | private BaseDiscordClient GetBaseClientForRequest(ulong? guildId = null) 167 | { 168 | BaseDiscordClient? client = null; 169 | if (_discord is not null) 170 | client = _discord; 171 | 172 | if (client is null) 173 | { 174 | if (guildId is null) 175 | { 176 | if(_sharded is not null && _sharded.ShardClients.Count > 0) 177 | { 178 | client = _sharded.ShardClients[0]; 179 | } 180 | } 181 | else 182 | { 183 | if (_sharded is not null) 184 | { 185 | foreach (var shard in _sharded.ShardClients) 186 | if (shard.Value.Guilds.ContainsKey(guildId.Value)) 187 | client = shard.Value; 188 | } 189 | } 190 | } 191 | 192 | if (client is null) 193 | throw new Exception("Failed to get a proper cleint for this request."); 194 | 195 | return client; 196 | } 197 | 198 | private InteractionResponseBuilder GetDeafultResponse() 199 | { 200 | // createa new response object .... 201 | var response = new InteractionResponseBuilder() 202 | .WithType(_config.DefaultResponseType); 203 | // ... add the optional configs ... 204 | if (_config.DefaultResponseType == InteractionResponseType.ChannelMessageWithSource) 205 | { 206 | response.Data = _config.DefaultResponseData; 207 | } 208 | // ... and return the builder object. 209 | return response; 210 | } 211 | 212 | /// 213 | /// Updates the original interaction response. 214 | /// 215 | /// New version of the response 216 | /// Update task 217 | internal async Task UpdateAsync(InteractionResponse edit, string token) 218 | { 219 | if(_config.DefaultResponseType == InteractionResponseType.ChannelMessageWithSource) 220 | throw new Exception("Can't edit default response when using Acknowledge or ACKWithSource."); 221 | 222 | var request = new HttpRequestMessage() 223 | { 224 | Method = HttpMethod.Patch, 225 | RequestUri = GetEditOrDeleteInitialUri(token), 226 | Content = new StringContent(edit.BuildWebhookEditBody(_jsonSettings)), 227 | }; 228 | request.Content.Headers.ContentType = new(_contentType); 229 | 230 | var res = await _http.SendAsync(request); 231 | 232 | if (res.IsSuccessStatusCode) 233 | { 234 | return await GetResponseBody(res); 235 | } 236 | else return null; 237 | } 238 | 239 | /// 240 | /// Deletes the original response 241 | /// 242 | /// Token for the default interaction to be delete. 243 | /// Delete task 244 | internal async Task DeleteAsync(string token) 245 | { 246 | if(_config.DefaultResponseType == InteractionResponseType.ChannelMessageWithSource) 247 | throw new Exception("Can't delete default response when using Acknowledge or ACKWithSource."); 248 | 249 | var request = new HttpRequestMessage() 250 | { 251 | Method = HttpMethod.Delete, 252 | RequestUri = GetEditOrDeleteInitialUri(token), 253 | }; 254 | 255 | var res = await _http.SendAsync(request); 256 | 257 | if (res.IsSuccessStatusCode) 258 | { 259 | return await GetResponseBody(res); 260 | } 261 | else return null; 262 | } 263 | 264 | /// 265 | /// Follow up the interaction response with a new response. 266 | /// 267 | /// New response to send. 268 | /// Original response token. 269 | /// The DiscordMessage that was created. 270 | internal async Task FollowupWithAsync(InteractionResponse followup, string token) 271 | { 272 | var request = new HttpRequestMessage 273 | { 274 | Method = HttpMethod.Post, 275 | RequestUri = GetPostFollowupUri(token), 276 | Content = new StringContent(followup.BuildWebhookBody(_jsonSettings)) 277 | }; 278 | request.Content.Headers.ContentType = new(_contentType); 279 | 280 | var res = await _http.SendAsync(request); 281 | 282 | if (res.IsSuccessStatusCode) 283 | { 284 | return await GetResponseBody(res); 285 | } 286 | else return null; 287 | } 288 | 289 | /// 290 | /// Edits a followup message from a response. 291 | /// 292 | /// New message to replace the old one with. 293 | /// Original response token. 294 | /// Id of the followup message that you want to edit. 295 | /// Edit task 296 | internal async Task EditAsync(InteractionResponse edit, string token, ulong id) 297 | { 298 | var request = new HttpRequestMessage() 299 | { 300 | Method = HttpMethod.Patch, 301 | RequestUri = GetEditFollowupUri(token, id), 302 | Content = new StringContent(edit.BuildWebhookEditBody(_jsonSettings)), 303 | }; 304 | request.Content.Headers.ContentType = new(_contentType); 305 | 306 | var res = await _http.SendAsync(request); 307 | 308 | if (res.IsSuccessStatusCode) 309 | { 310 | return await GetResponseBody(res); 311 | } 312 | else return null; 313 | } 314 | 315 | private async Task GetResponseBody(HttpResponseMessage res) 316 | { 317 | try 318 | { 319 | var resJson = await res.Content.ReadAsStringAsync(); 320 | var msg = JsonConvert.DeserializeObject(resJson); 321 | return msg; 322 | } 323 | catch (Exception ex) 324 | { 325 | _logger.LogError(ex, "Update Original Async Failed"); 326 | return null; 327 | } 328 | } 329 | 330 | protected Uri GetEditOrDeleteInitialUri(string token) 331 | { 332 | return new Uri($"{api}/webhooks/{ApplicationId}/{token}/messages/@original"); 333 | } 334 | 335 | protected Uri GetPostFollowupUri(string token) 336 | { 337 | return new Uri($"{api}/webhooks/{ApplicationId}/{token}"); 338 | } 339 | 340 | protected Uri GetEditFollowupUri(string token, ulong messageId) 341 | { 342 | return new Uri($"{api}/webhooks/{ApplicationId}/{token}/messages/{messageId}"); 343 | } 344 | 345 | protected Uri GetGatewayFollowupUri(string interactId, string token) 346 | { 347 | return new Uri($"{api}/interactions/{interactId}/{token}/callback"); 348 | } 349 | } 350 | } 351 | -------------------------------------------------------------------------------- /DSharpPlus.SlashCommands/DiscordSlashConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using DSharpPlus.SlashCommands.Entities; 4 | using DSharpPlus.SlashCommands.Entities.Builders; 5 | using DSharpPlus.SlashCommands.Enums; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace DSharpPlus.SlashCommands 9 | { 10 | public class DiscordSlashConfiguration 11 | { 12 | /// 13 | /// Token used for the Discrod Bot user that your application has. 14 | /// 15 | public string Token { get; set; } 16 | /// 17 | /// Base client used for parsing DSharpPlus arguments. Can be a Rest or Regular client. This or a ShardedClient is required. 18 | /// 19 | public BaseDiscordClient? Client { get; set; } 20 | /// 21 | /// Base sharded client used for parsing DSharpPLus arguments. This or a Rest or Regular client is required. 22 | /// 23 | public DiscordShardedClient? ShardedClient { get; set; } 24 | /// 25 | /// The Default Response type that is sent to Discord upon receiving a request. 26 | /// 27 | public InteractionResponseType DefaultResponseType { get; set; } = InteractionResponseType.DeferredChannelMessageWithSource; 28 | /// 29 | /// The default data to be used when the DefaultResponseType is ChannelMessage or ChannelMessageWithSource. 30 | /// 31 | public InteractionApplicationCommandCallbackDataBuilder? DefaultResponseData { get; set; } = null; 32 | /// 33 | /// Supply a logger to override the DSharpPlus logger. 34 | /// 35 | public ILogger? Logger { get; set; } = null; 36 | /// 37 | /// Services for dependency injection for slash commands. 38 | /// 39 | public IServiceProvider? Services { get; set; } = null; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /DSharpPlus.SlashCommands/Entities/ApplicationCommandOptionChoice.cs: -------------------------------------------------------------------------------- 1 |  2 | using Newtonsoft.Json; 3 | 4 | namespace DSharpPlus.SlashCommands.Entities 5 | { 6 | public class ApplicationCommandOptionChoice 7 | { 8 | [JsonProperty("name")] 9 | public string Name { get; internal set; } 10 | 11 | /// 12 | /// Must be string or int 13 | /// 14 | [JsonProperty("value")] 15 | public object Value { get; internal set; } 16 | 17 | internal ApplicationCommandOptionChoice() : this("", 0) { } 18 | 19 | public ApplicationCommandOptionChoice(string n, int v) 20 | { 21 | Name = n; 22 | Value = v; 23 | } 24 | 25 | public ApplicationCommandOptionChoice(string n, string v) 26 | { 27 | Name = n; 28 | Value = v; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /DSharpPlus.SlashCommands/Entities/BaseSlashCommandModule.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DSharpPlus.SlashCommands.Entities 4 | { 5 | public class BaseSlashCommandModule 6 | { 7 | protected readonly IServiceProvider _services; 8 | 9 | public BaseSlashCommandModule(IServiceProvider services) 10 | { 11 | _services = services; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /DSharpPlus.SlashCommands/Entities/Builders/ApplicationCommandBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | using DSharpPlus.Entities; 5 | 6 | namespace DSharpPlus.SlashCommands.Entities.Builders 7 | { 8 | public class ApplicationCommandBuilder : IBuilder 9 | { 10 | public string Name { get; set; } 11 | public string Description { get; set; } 12 | 13 | /// 14 | /// Options/Subcommands - Max of 10. 15 | /// 16 | public List Options { get; set; } 17 | 18 | public ApplicationCommandBuilder() 19 | { 20 | Name = ""; 21 | Description = ""; 22 | Options = new(); 23 | } 24 | 25 | public ApplicationCommandBuilder WithName(string name) 26 | { 27 | if (name is null || name == "") 28 | throw new Exception("Name cannot be null"); 29 | 30 | if (name.Length < 3 || name.Length > 32) 31 | throw new Exception("Name must be between 3 and 32 characters."); 32 | 33 | Name = name; 34 | return this; 35 | } 36 | 37 | public ApplicationCommandBuilder WithDescription(string description) 38 | { 39 | if (description.Length < 1 || description.Length > 100) 40 | throw new Exception("Description must be between 1 and 100 characters."); 41 | 42 | Description = description; 43 | return this; 44 | } 45 | 46 | public ApplicationCommandBuilder AddOption(ApplicationCommandOptionBuilder options) 47 | { 48 | Options.Add(options); 49 | return this; 50 | } 51 | 52 | public DiscordApplicationCommand Build() 53 | { 54 | List options = new(); 55 | foreach (var op in Options) 56 | options.Add(op.Build()); 57 | 58 | if (Description is null || Description == "") 59 | Description = "none provided"; 60 | 61 | return new DiscordApplicationCommand(Name, Description, options); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /DSharpPlus.SlashCommands/Entities/Builders/ApplicationCommandOptionBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | using DSharpPlus.Entities; 5 | using DSharpPlus.SlashCommands.Enums; 6 | 7 | namespace DSharpPlus.SlashCommands.Entities.Builders 8 | { 9 | public class ApplicationCommandOptionBuilder : IBuilder 10 | { 11 | public ApplicationCommandOptionType Type { get; set; } 12 | public string Name { get; set; } 13 | public string Description { get; set; } 14 | public bool? Default { get; set; } 15 | public bool? Required { get; set; } 16 | public List Choices { get; set; } 17 | public List Options { get; set; } 18 | 19 | public ApplicationCommandOptionBuilder() 20 | { 21 | Name = ""; 22 | Description = ""; 23 | Choices = new(); 24 | Options = new(); 25 | } 26 | 27 | public ApplicationCommandOptionBuilder WithType(ApplicationCommandOptionType type) 28 | { 29 | Type = type; 30 | return this; 31 | } 32 | 33 | public ApplicationCommandOptionBuilder WithName(string name) 34 | { 35 | if (name is null || name == "") 36 | throw new Exception("Name can not be null"); 37 | 38 | if (name.Length < 1 || name.Length > 32) 39 | throw new Exception("Name must be between 1 and 32 characters."); 40 | 41 | Name = name; 42 | return this; 43 | } 44 | 45 | public ApplicationCommandOptionBuilder WithDescription(string description) 46 | { 47 | Description = description; 48 | return this; 49 | } 50 | 51 | public ApplicationCommandOptionBuilder IsDefault(bool? defaultCmd) 52 | { 53 | Default = defaultCmd; 54 | return this; 55 | } 56 | 57 | public ApplicationCommandOptionBuilder IsRequired(bool? required) 58 | { 59 | Required = required; 60 | return this; 61 | } 62 | 63 | public ApplicationCommandOptionBuilder AddChoice(ApplicationCommandOptionChoiceBuilder choices) 64 | { 65 | // I think. Not completely sure. 66 | if (Options.Count >= 10) 67 | throw new Exception("Cant have more than 10 choices."); 68 | 69 | Choices.Add(choices); 70 | return this; 71 | } 72 | /// 73 | /// Adds an Enum as the avalible choices. This overrides all other choices added with AddChoice. 74 | /// 75 | /// Enum to generate choices from. 76 | /// This builder 77 | public ApplicationCommandOptionBuilder WithChoices(Type enumType) 78 | { 79 | if (!enumType.IsEnum) 80 | throw new Exception("Type is not an enum"); 81 | 82 | var names = enumType.GetEnumNames(); 83 | var values = enumType.GetEnumValues(); 84 | 85 | List choices = new(); 86 | 87 | for(int i = 0; i < names.Length; i++) 88 | { 89 | var part = new ApplicationCommandOptionChoiceBuilder() 90 | .WithName(names[i]); 91 | var val = values.GetValue(i); 92 | 93 | part.WithValue((int)val!); 94 | 95 | choices.Add(part); 96 | } 97 | 98 | Choices = choices; 99 | 100 | return this; 101 | } 102 | 103 | public ApplicationCommandOptionBuilder AddOption(ApplicationCommandOptionBuilder options) 104 | { 105 | if (Options.Count >= 10) 106 | throw new Exception("Cant have more than 10 options."); 107 | 108 | Options.Add(options); 109 | return this; 110 | } 111 | 112 | public DiscordApplicationCommandOption Build() 113 | { 114 | List choices = new(); 115 | foreach (var ch in Choices) 116 | choices.Add(ch.Build()); 117 | 118 | List options = new(); 119 | foreach (var op in Options) 120 | options.Add(op.Build()); 121 | 122 | return new DiscordApplicationCommandOption(Name, Description, Type, Required, choices, options); 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /DSharpPlus.SlashCommands/Entities/Builders/ApplicationCommandOptionChoiceBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using DSharpPlus.Entities; 4 | 5 | namespace DSharpPlus.SlashCommands.Entities.Builders 6 | { 7 | public class ApplicationCommandOptionChoiceBuilder : IBuilder 8 | { 9 | public string Name { get; set; } 10 | 11 | /// 12 | /// Must be string or int 13 | /// 14 | public object Value { get; set; } 15 | 16 | public ApplicationCommandOptionChoiceBuilder() 17 | { 18 | Name = ""; 19 | Value = 0; 20 | } 21 | 22 | public ApplicationCommandOptionChoiceBuilder WithName(string name) 23 | { 24 | if (name is null || name == "") 25 | throw new Exception("Name can not be null"); 26 | 27 | if (name.Length < 1 || name.Length > 100) 28 | throw new Exception("Name must be between 1 and 100 characters."); 29 | 30 | Name = name; 31 | return this; 32 | } 33 | 34 | public ApplicationCommandOptionChoiceBuilder WithValue(int value) 35 | { 36 | Value = value; 37 | return this; 38 | } 39 | 40 | public ApplicationCommandOptionChoiceBuilder WithValue(string value) 41 | { 42 | Value = value; 43 | return this; 44 | } 45 | 46 | public DiscordApplicationCommandOptionChoice Build() 47 | { 48 | return new DiscordApplicationCommandOptionChoice(Name, Value); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /DSharpPlus.SlashCommands/Entities/Builders/IBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace DSharpPlus.SlashCommands.Entities.Builders 8 | { 9 | public interface IBuilder 10 | { 11 | public T Build(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /DSharpPlus.SlashCommands/Entities/Builders/InteractionApplicationCommandCallbackDataBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | using DSharpPlus.Entities; 8 | 9 | namespace DSharpPlus.SlashCommands.Entities.Builders 10 | { 11 | public class InteractionApplicationCommandCallbackDataBuilder : IBuilder 12 | { 13 | public bool? TextToSpeech { get; set; } 14 | public string? Content { get; set; } 15 | public List Embeds { get; set; } = new(); 16 | public List AllowedMentions { get; set; } = new(); 17 | 18 | public InteractionApplicationCommandCallbackDataBuilder() 19 | { 20 | 21 | } 22 | 23 | public InteractionApplicationCommandCallbackDataBuilder WithTTS() 24 | { 25 | TextToSpeech = true; 26 | return this; 27 | } 28 | 29 | public InteractionApplicationCommandCallbackDataBuilder WithContent(string content) 30 | { 31 | Content = content; 32 | return this; 33 | } 34 | 35 | public InteractionApplicationCommandCallbackDataBuilder WithEmbed(DiscordEmbed embed) 36 | { 37 | Embeds.Add(embed); 38 | return this; 39 | } 40 | 41 | public InteractionApplicationCommandCallbackDataBuilder WithAllowedMention(IMention mention) 42 | { 43 | AllowedMentions.Add(mention); 44 | return this; 45 | } 46 | 47 | public InteractionApplicationCommandCallbackData Build() 48 | { 49 | if (Embeds.Count <= 0 && (Content is null || Content == "")) 50 | throw new Exception("Either an embed or content is required."); 51 | 52 | return new InteractionApplicationCommandCallbackData() 53 | { 54 | AllowedMentions = AllowedMentions.Count > 0 ? AllowedMentions : null, 55 | Embeds = Embeds.Count > 0 ? Embeds.ToArray() : null, 56 | Content = Content, 57 | TextToSpeech = TextToSpeech 58 | }; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /DSharpPlus.SlashCommands/Entities/Builders/InteractionResponseBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | using DSharpPlus.SlashCommands.Enums; 8 | 9 | namespace DSharpPlus.SlashCommands.Entities.Builders 10 | { 11 | public class InteractionResponseBuilder : IBuilder 12 | { 13 | public InteractionResponseType Type { get; set; } 14 | public InteractionApplicationCommandCallbackDataBuilder? Data { get; set; } 15 | 16 | public InteractionResponseBuilder WithType(InteractionResponseType type) 17 | { 18 | Type = type; 19 | return this; 20 | } 21 | 22 | public InteractionResponseBuilder WithData(InteractionApplicationCommandCallbackDataBuilder data) 23 | { 24 | Data = data; 25 | return this; 26 | } 27 | 28 | public InteractionResponse Build() 29 | { 30 | return new InteractionResponse() 31 | { 32 | Type = Type, 33 | Data = Data?.Build() 34 | }; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /DSharpPlus.SlashCommands/Entities/InteractionApplicationCommandCallbackData.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | using DSharpPlus.Entities; 4 | 5 | using Newtonsoft.Json; 6 | 7 | namespace DSharpPlus.SlashCommands.Entities 8 | { 9 | public class InteractionApplicationCommandCallbackData 10 | { 11 | [JsonProperty("tts")] 12 | public bool? TextToSpeech { get; internal set; } 13 | [JsonProperty("content")] 14 | public string Content { get; internal set; } 15 | [JsonProperty("embeds")] 16 | public DiscordEmbed[]? Embeds { get; internal set; } 17 | [JsonProperty("allowed_mentions")] 18 | public IEnumerable? AllowedMentions { get; internal set; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /DSharpPlus.SlashCommands/Entities/InteractionContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | using DSharpPlus.Entities; 9 | using DSharpPlus.SlashCommands.Enums; 10 | 11 | using Microsoft.AspNetCore.Http; 12 | 13 | namespace DSharpPlus.SlashCommands.Entities 14 | { 15 | public class InteractionContext 16 | { 17 | private readonly DiscordSlashClient _client; 18 | 19 | public DiscordInteraction Interaction { get; internal set; } 20 | 21 | public InteractionContext(DiscordSlashClient c, DiscordInteraction i) 22 | { 23 | _client = c; 24 | Interaction = i; 25 | } 26 | #region Followup 27 | /// 28 | /// Reply to the interaction by sending a followup message. 29 | /// 30 | /// Text content to send back 31 | /// Embeds to send back 32 | /// Is this message a text to speech message? 33 | /// The allowed mentions of the message 34 | /// The response object form discord 35 | public async Task ReplyAsync(string message = "", DiscordEmbed[]? embeds = null, bool? tts = null, IMention[]? allowedMentions = null, bool showSource = false) 36 | { 37 | if (embeds is not null && embeds.Length > 10) 38 | throw new Exception("Too many embeds"); 39 | 40 | return await ReplyAsync(new InteractionResponse() 41 | { 42 | Type = showSource ? InteractionResponseType.ChannelMessageWithSource : InteractionResponseType.DeferredChannelMessageWithSource, 43 | Data = new InteractionApplicationCommandCallbackData() 44 | { 45 | Content = message, 46 | Embeds = embeds, 47 | TextToSpeech = tts, 48 | AllowedMentions = allowedMentions 49 | } 50 | }); 51 | } 52 | 53 | /// 54 | /// Reply to the interaction by sending a followup message. 55 | /// 56 | /// An InteractionResponse object to send to the user. 57 | /// The response object form discord 58 | public async Task ReplyAsync(InteractionResponse response) 59 | { 60 | var msg = await _client.FollowupWithAsync(response, Interaction.Token); 61 | 62 | if (msg is null) 63 | throw new Exception("Failed to reterive message object from Discord."); 64 | 65 | return msg; 66 | } 67 | #endregion 68 | #region Edit 69 | // Edit for both initial response and other responses. 70 | /// 71 | /// Edits an already sent message. 72 | /// 73 | /// Text content to send 74 | /// Message to edit by ID of the order it was sent. Defaults to the initial response 75 | /// Embeds to send. 76 | /// Is this Text To Speech? 77 | /// Allowed mentions list 78 | /// The edited response 79 | public async Task EditResponseAsync(string message = "", ulong toEdit = 0, DiscordEmbed[]? embeds = null, bool? tts = null, IMention[]? allowedMentions = null) 80 | { 81 | if (embeds is not null && embeds.Length > 10) 82 | throw new Exception("Too many embeds"); 83 | 84 | return await EditResponseAsync(new InteractionResponse() 85 | { 86 | Type = InteractionResponseType.ChannelMessageWithSource, 87 | Data = new InteractionApplicationCommandCallbackData() 88 | { 89 | Content = message, 90 | Embeds = embeds, 91 | TextToSpeech = tts, 92 | AllowedMentions = allowedMentions 93 | } 94 | }, 95 | toEdit); 96 | } 97 | 98 | /// 99 | /// Edits an already sent message 100 | /// 101 | /// InteractionResponse to send to the user 102 | /// Message to edit by ID of the order it was sent. Defaults to the initial response 103 | /// The edited response 104 | public async Task EditResponseAsync(InteractionResponse response, ulong toEdit = 0) 105 | { 106 | DiscordMessage? msg; 107 | if(toEdit == 0) 108 | { 109 | msg = await _client.UpdateAsync(response, Interaction.Token); 110 | } 111 | else 112 | { 113 | msg = await _client.EditAsync(response, Interaction.Token, toEdit); 114 | } 115 | 116 | if (msg is null) 117 | throw new Exception("Failed to edit the message"); 118 | 119 | return msg; 120 | } 121 | #endregion 122 | #region Delete 123 | /// 124 | /// Deletes the initial response for this interaction. 125 | /// 126 | /// The deleted interaction 127 | public async Task DeleteInitalAsync() 128 | { 129 | var msg = await _client.DeleteAsync(Interaction.Token); 130 | 131 | if (msg is null) 132 | throw new Exception("Failed to delete original message"); 133 | 134 | return msg; 135 | } 136 | #endregion 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /DSharpPlus.SlashCommands/Entities/InteractionResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using DSharpPlus.SlashCommands.Enums; 4 | 5 | using Newtonsoft.Json; 6 | using Newtonsoft.Json.Linq; 7 | 8 | namespace DSharpPlus.SlashCommands.Entities 9 | { 10 | public class InteractionResponse 11 | { 12 | [JsonProperty("type")] 13 | public InteractionResponseType Type { get; internal set; } 14 | [JsonProperty("data")] 15 | public InteractionApplicationCommandCallbackData? Data { get; internal set; } 16 | 17 | /// 18 | /// Builds the webhook body for sending a new message. 19 | /// 20 | /// Raw JSON body for a webhook POST operation. 21 | public string BuildWebhookBody(JsonSerializerSettings settings) 22 | { 23 | if (Data is null) 24 | throw new Exception("Data can not be null."); 25 | 26 | return JsonConvert.SerializeObject(Data, settings); 27 | } 28 | 29 | /// 30 | /// Builds the webhook edit body for editing a previous message. 31 | /// 32 | /// Raw JSON body for a webhook PATCH operation. 33 | public string BuildWebhookEditBody(JsonSerializerSettings settings) 34 | { 35 | if (Data is null) 36 | throw new Exception("Data can not be null."); 37 | 38 | var d = JObject.Parse(JsonConvert.SerializeObject(Data, settings)); 39 | 40 | if (d.ContainsKey("tts")) 41 | d.Remove("tts"); 42 | 43 | return d.ToString(); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /DSharpPlus.SlashCommands/Entities/SlashCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace DSharpPlus.SlashCommands.Entities 7 | { 8 | public class SlashCommand 9 | { 10 | public string Name { get; set; } 11 | public string Description { get; set; } 12 | public ulong? GuildId { get; set; } 13 | public ulong? CommandId { get; set; } 14 | public ulong? ApplicationId { get; set; } 15 | public SlashSubcommand? Command { get; init; } 16 | 17 | public Dictionary? Subcommands { get; init; } 18 | 19 | public SlashCommand(string name, SlashSubcommand command, ulong? gid) 20 | { 21 | Name = name; 22 | Description = command.Description; 23 | GuildId = gid; 24 | Command = command; 25 | Subcommands = null; 26 | } 27 | 28 | public SlashCommand(string name, SlashSubcommandGroup[] subcommands, ulong? gid, string desc = "n/a") 29 | { 30 | Name = name; 31 | Description = desc; 32 | Subcommands = subcommands.ToDictionary(x => x.Name); 33 | GuildId = gid; 34 | Command = null; 35 | } 36 | 37 | /// 38 | /// Attempts to execute a command from a command with no subcommands. 39 | /// 40 | /// Command arguments 41 | /// True if the command was attempted, false if there was no command to attempt. 42 | public bool ExecuteCommand(BaseDiscordClient c, InteractionContext ctx, params object[] args) 43 | { 44 | List combinedArgs = new List 45 | { 46 | ctx 47 | }; 48 | combinedArgs.AddRange(args); 49 | 50 | var cArgs = combinedArgs.ToArray(); 51 | 52 | if (Command is not null) 53 | { 54 | _ = Task.Run(async () => await Command.ExecuteCommand(c, ctx.Interaction.GuildId, cArgs)); 55 | return true; 56 | } 57 | else 58 | { 59 | if(Subcommands is null) return false; 60 | 61 | var group = ctx.Interaction.Data?.Options?.FirstOrDefault(); 62 | if(group is not null) 63 | { 64 | if(Subcommands.TryGetValue(group.Name, out var cmdGroup)) 65 | { 66 | var cmdData = group.Options?.FirstOrDefault(); 67 | if(cmdData is not null) 68 | { 69 | if(cmdGroup.Commands.TryGetValue(cmdData.Name, out var cmd)) 70 | { 71 | _ = Task.Run(async () => await cmd.ExecuteCommand(c, ctx.Interaction.GuildId, cArgs)); 72 | return true; 73 | } 74 | } 75 | } 76 | } 77 | } 78 | 79 | return false; 80 | } 81 | 82 | public SlashCommandConfiguration GetConfiguration() 83 | { 84 | return new SlashCommandConfiguration() 85 | { 86 | CommandId = CommandId ?? throw new Exception("Failed to get a valid command ID"), 87 | GuildId = GuildId, 88 | Name = Name 89 | }; 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /DSharpPlus.SlashCommands/Entities/SlashCommandConfiguration.cs: -------------------------------------------------------------------------------- 1 |  2 | using Newtonsoft.Json; 3 | 4 | namespace DSharpPlus.SlashCommands.Entities 5 | { 6 | public class SlashCommandConfiguration 7 | { 8 | [JsonProperty("name")] 9 | public string Name { get; set; } 10 | [JsonProperty("version")] 11 | public int Version { get; set; } 12 | [JsonProperty("guild_id")] 13 | public ulong? GuildId { get; set; } 14 | [JsonProperty("command_id")] 15 | public ulong CommandId { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /DSharpPlus.SlashCommands/Entities/SlashSubcommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Reflection; 4 | using System.Threading.Tasks; 5 | 6 | using DSharpPlus.Entities; 7 | 8 | namespace DSharpPlus.SlashCommands.Entities 9 | { 10 | public class SlashSubcommand 11 | { 12 | public string Name { get; init; } 13 | public string Description { get; init; } 14 | public MethodInfo ExecutionMethod { get; init; } 15 | public BaseSlashCommandModule BaseCommand { get; init; } 16 | 17 | public SlashSubcommand(string name, string desc, MethodInfo method, BaseSlashCommandModule commandInstance) 18 | { 19 | Name = name; 20 | Description = desc; 21 | ExecutionMethod = method; 22 | BaseCommand = commandInstance; 23 | } 24 | 25 | public async Task ExecuteCommand(BaseDiscordClient c, ulong? guildId, params object[] args) 26 | { 27 | try 28 | { 29 | var parsedArgs = await ParseArguments(c, guildId, args); 30 | ExecutionMethod.Invoke(BaseCommand, parsedArgs); 31 | } 32 | catch (Exception ex) 33 | { 34 | // TODO Loger here 35 | } 36 | } 37 | 38 | private async Task ParseArguments(BaseDiscordClient c, ulong? guildId, object[] args) 39 | { 40 | var parsedArgs = new object[args.Length]; 41 | var parameters = ExecutionMethod.GetParameters(); 42 | 43 | for(int i = 0; i < args.Length; i++) 44 | { 45 | var param = parameters[i]; 46 | 47 | if (param.ParameterType.IsEnum) 48 | { 49 | var e = ParseEnum(args[i], param); 50 | 51 | if (e is null) 52 | throw new ArgumentNullException(param.Name, "Failed to parse Discord result to enum value."); 53 | 54 | parsedArgs[i] = e; 55 | } 56 | else if (param.ParameterType == typeof(DiscordUser)) 57 | { 58 | var u = await ParseUser(args[i], c); 59 | 60 | if (u is null) 61 | throw new ArgumentNullException(param.Name, "Failed to parse Discord result to DiscordUser value"); 62 | 63 | parsedArgs[i] = u; 64 | } 65 | else if (param.ParameterType == typeof(DiscordChannel)) 66 | { 67 | var chan = await ParseChannel(args[i], c); 68 | 69 | if (chan is null) 70 | throw new ArgumentNullException(param.Name, "Failed to parse Discord result to DiscordChannel value"); 71 | 72 | parsedArgs[i] = chan; 73 | } 74 | else if (param.ParameterType == typeof(DiscordRole) && guildId is not null) 75 | { 76 | var r = await ParseRole(args[i], c, guildId.Value); 77 | 78 | if (r is null) 79 | throw new ArgumentNullException(param.Name, "Failed to parse Discord result to DisocrdRole value"); 80 | 81 | parsedArgs[i] = r; 82 | } 83 | else 84 | { 85 | try 86 | { 87 | object parsed = Convert.ChangeType(args[i], param.ParameterType); 88 | parsedArgs[i] = parsed; 89 | } 90 | catch(Exception ex) 91 | { 92 | // TODO: Log this 93 | // Failed basic conversion. 94 | } 95 | } 96 | } 97 | 98 | return parsedArgs; 99 | } 100 | 101 | private static object? ParseEnum(object arg, ParameterInfo info) 102 | { 103 | try 104 | { 105 | var e = Enum.ToObject(info.ParameterType, arg); 106 | return e; 107 | } 108 | catch 109 | { 110 | return null; 111 | } 112 | } 113 | 114 | private static async Task ParseUser(object arg, BaseDiscordClient client) 115 | { 116 | try 117 | { 118 | ulong argVal = Convert.ToUInt64(arg); 119 | 120 | return client switch 121 | { 122 | DiscordClient discord => await discord.GetUserAsync(argVal), 123 | DiscordRestClient rest => await rest.GetUserAsync(argVal), 124 | _ => null, 125 | }; 126 | } 127 | catch 128 | { 129 | // TODO: Logger here. 130 | 131 | return null; 132 | } 133 | } 134 | 135 | private static async Task ParseChannel(object arg, BaseDiscordClient client) 136 | { 137 | try 138 | { 139 | ulong argVal = Convert.ToUInt64(arg); 140 | return client switch 141 | { 142 | DiscordClient discord => await discord.GetChannelAsync(argVal), 143 | DiscordRestClient rest => await rest.GetChannelAsync(argVal), 144 | _ => null, 145 | }; 146 | } 147 | catch 148 | { 149 | // TODO: Logger here. 150 | 151 | return null; 152 | } 153 | } 154 | 155 | private static async Task ParseRole(object arg, BaseDiscordClient client, ulong guildId) 156 | { 157 | try 158 | { 159 | ulong argVal = Convert.ToUInt64(arg); 160 | 161 | switch (client) 162 | { 163 | case DiscordClient discord: 164 | var guild = await discord.GetGuildAsync(guildId); 165 | return guild.GetRole(argVal); 166 | case DiscordRestClient rest: 167 | var roles = await rest.GetGuildRolesAsync(guildId); 168 | return roles.FirstOrDefault(x => x.Id == argVal); 169 | } 170 | 171 | return null; 172 | } 173 | catch 174 | { 175 | // TODO: Logger here 176 | 177 | return null; 178 | } 179 | } 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /DSharpPlus.SlashCommands/Entities/SlashSubcommandGroup.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace DSharpPlus.SlashCommands.Entities 5 | { 6 | public class SlashSubcommandGroup 7 | { 8 | public string Name { get; init; } 9 | public string Description { get; set; } 10 | public bool? Required { get; set; } 11 | public bool? Default { get; set; } 12 | public Dictionary Commands { get; init; } 13 | public SlashSubcommandGroup(string name, string description, SlashSubcommand[] commands) 14 | { 15 | Name = name; 16 | Description = description; 17 | Commands = commands.ToDictionary(x => x.Name); 18 | } 19 | 20 | public SlashSubcommandGroup(string name, string description) 21 | { 22 | Name = name; 23 | Description = description; 24 | Commands = new(); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /DSharpPlus.SlashCommands/Enums/ApplicationCommandOptionType.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | using DSharpPlus.Entities; 4 | using DSharpPlus; 5 | 6 | namespace DSharpPlus.SlashCommands.Enums 7 | { 8 | public static class ApplicationCommandOptionTypeExtensions 9 | { 10 | public static ApplicationCommandOptionType? GetOptionType(ParameterInfo parameter) 11 | { 12 | if (parameter.ParameterType == typeof(string)) 13 | return ApplicationCommandOptionType.String; 14 | else if (parameter.ParameterType == typeof(int)) 15 | return ApplicationCommandOptionType.Integer; 16 | else if (parameter.ParameterType == typeof(bool)) 17 | return ApplicationCommandOptionType.Boolean; 18 | else if (parameter.ParameterType == typeof(DiscordUser)) 19 | return ApplicationCommandOptionType.User; 20 | else if (parameter.ParameterType == typeof(DiscordChannel)) 21 | return ApplicationCommandOptionType.Channel; 22 | else if (parameter.ParameterType == typeof(DiscordRole)) 23 | return ApplicationCommandOptionType.Role; 24 | else 25 | return null; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /DSharpPlus.SlashCommands/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Andrew B 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /DSharpPlus.SlashCommands/Services/SlashCommandHandlingService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Net.Http; 7 | using System.Reflection; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | using DSharpPlus.CommandsNext.Attributes; 12 | using DSharpPlus.Entities; 13 | using DSharpPlus.EventArgs; 14 | using DSharpPlus.SlashCommands.Attributes; 15 | using DSharpPlus.SlashCommands.Entities; 16 | using DSharpPlus.SlashCommands.Entities.Builders; 17 | using DSharpPlus.SlashCommands.Enums; 18 | using Microsoft.Extensions.Logging; 19 | using DSharpPlus; 20 | 21 | using Newtonsoft.Json; 22 | 23 | namespace DSharpPlus.SlashCommands.Services 24 | { 25 | public class SlashCommandHandlingService 26 | { 27 | public bool Started { get; private set; } 28 | 29 | private readonly ILogger _logger; 30 | private readonly IServiceProvider _services; 31 | private readonly HttpClient _client; 32 | private ulong BotId { get; set; } 33 | private string Token { get; set; } 34 | private string ConfigPath { get 35 | { 36 | return $"sccfg_{BotId}.json"; 37 | } 38 | } 39 | 40 | private ConcurrentDictionary Commands { get; set; } 41 | private List Assemblies { get; set; } 42 | 43 | private ConcurrentDictionary> RunningInteractions; 44 | 45 | 46 | /// 47 | /// Create a new Slash Command Service. Best used by adding it into a service collection, then pulling it once and running start. Or, 48 | /// when it is used, verify it is Started and run Start if it is not. 49 | /// 50 | /// Services for DI (which is kinda not really implemented) 51 | /// HTTP Client for making web requests to the Discord API 52 | /// A Logger for logging what this service does. 53 | public SlashCommandHandlingService(IServiceProvider services, HttpClient http, ILogger logger) 54 | { 55 | _logger = logger; 56 | _services = services; 57 | _client = http; 58 | 59 | Commands = new(); 60 | Assemblies = new(); 61 | RunningInteractions = new(); 62 | Started = false; 63 | } 64 | 65 | /// 66 | /// Add an assembly to register commands from. 67 | /// 68 | /// Assembly to get commands from. 69 | public void WithCommandAssembly(Assembly assembly) 70 | { 71 | Assemblies.Add(assembly); 72 | } 73 | 74 | /// 75 | /// Register the commands and allow the service to handle commands. 76 | /// 77 | /// Bot token for authentication 78 | /// Bot Client ID, used for storing command state locally. 79 | public async Task StartAsync(string botToken, ulong clientId) 80 | { 81 | Token = botToken; 82 | BotId = clientId; 83 | 84 | LoadCommandTree(); 85 | await BulkUpdateCommands(); 86 | } 87 | 88 | public Task HandleInteraction(BaseDiscordClient discord, DiscordInteraction interact, DiscordSlashClient c) 89 | { 90 | // This should not get here, but check just in case. 91 | if (interact.Type == InteractionType.Ping) return Task.CompletedTask; 92 | // Create a cancellation token for the event in which it is needed. 93 | var cancelSource = new CancellationTokenSource(); 94 | // Store the command task in a ConcurrentDictionary and continue with execution to not hodlup the webhook response. 95 | RunningInteractions[interact] = new( 96 | Task.Run(async () => await ExecuteInteraction(discord, interact, c, cancelSource.Token)), 97 | cancelSource); 98 | 99 | return Task.CompletedTask; 100 | } 101 | 102 | private async Task ExecuteInteraction(BaseDiscordClient discord, DiscordInteraction interact, DiscordSlashClient c, CancellationToken cancellationToken) 103 | { 104 | try 105 | { 106 | cancellationToken.ThrowIfCancellationRequested(); 107 | 108 | if (interact.Data is null) 109 | throw new Exception("Interact object has no command data."); 110 | 111 | if(Commands.TryGetValue(interact.Data.Name, out var cmd)) 112 | { // TODO: Check how subcommands are returned. 113 | // TODO: Do argument parsing. 114 | 115 | var context = new InteractionContext(c, interact); 116 | 117 | if(interact.Data.Options is not null) 118 | { 119 | var args = await GetRawArguments(interact.Data.Options); 120 | 121 | cmd.ExecuteCommand(discord, context, args); 122 | } 123 | else 124 | { 125 | cmd.ExecuteCommand(discord, context); 126 | } 127 | } 128 | } 129 | catch (Exception ex) 130 | { 131 | _logger.LogError(ex, "Interaction Handler failed"); 132 | } 133 | finally 134 | { 135 | RunningInteractions.TryRemove(interact, out _); 136 | } 137 | } 138 | 139 | private async Task GetRawArguments(IEnumerable options) 140 | { 141 | if(options.FirstOrDefault()?.Options is not null) 142 | { 143 | return await GetRawArguments(options.First().Options); 144 | } 145 | else 146 | { 147 | var args = new List(); 148 | 149 | foreach(var val in options) 150 | { 151 | if (val.Value is null) 152 | continue; 153 | 154 | args.Add(val.Value); 155 | } 156 | 157 | return args.ToArray(); 158 | } 159 | } 160 | 161 | #region Command Registration 162 | /// 163 | /// Loads the commands from the assembly. 164 | /// 165 | // TODO: Pass in Assembly values to specify where to look for commands. 166 | private void LoadCommandTree() 167 | { 168 | _logger.LogInformation("Building Slash Command Objects ..."); 169 | // Get the base command class type... 170 | var cmdType = typeof(BaseSlashCommandModule); 171 | // ... and all the methods in it... 172 | var commandMethods = cmdType.GetMethods().ToList(); 173 | 174 | // ... and then all the classes in the provided assemblies ... 175 | List types = new(); 176 | foreach(var a in Assemblies) 177 | { 178 | // ... and add the types from that aseembly that are subclasses of the command type. 179 | types.AddRange(a.GetTypes().Where(x => x.IsSubclassOf(cmdType))); 180 | } 181 | 182 | // ... then for each type that is a subclass of SlashCommandBase ... 183 | foreach (var t in types) 184 | { 185 | // ... add its methods as command methods. 186 | commandMethods.AddRange(t.GetMethods()); 187 | } 188 | 189 | //... and create a list for methods that are not subcommands... 190 | List nonSubcommandCommands = new(); 191 | //... and a dict for all registered commands ... 192 | Dictionary commands = new(); 193 | // ... and for every command ... 194 | foreach (var cmd in commandMethods) 195 | { 196 | // ... try and get the SlashSubommandAttribute for it... 197 | // (we will check for methods with just the SlashCommandAttribute later) 198 | SlashSubcommandAttribute? attr; 199 | if((attr = cmd.GetCustomAttribute(false)) is not null) 200 | { //... if it is a subcommand, get the class that the subcommand is in... 201 | var subGroupClass = cmd.DeclaringType; 202 | // ... and the SubcommandGroup attribute for that class ... 203 | SlashSubcommandGroupAttribute? subGroupAttr; 204 | if(subGroupClass is not null 205 | && (subGroupAttr = subGroupClass.GetCustomAttribute(false)) is not null) 206 | { //... if it is a subcommand group, get the class the subcommand group is in... 207 | var slashCmdClass = subGroupClass.BaseType; 208 | // ... and the SlashCommand attribute for that class... 209 | SlashCommandAttribute? slashAttr; 210 | if(slashCmdClass is not null 211 | && (slashAttr = slashCmdClass.GetCustomAttribute(false)) is not null) 212 | { //... if it is a slash command, get or add the SlashCommand for the command ... 213 | if (!commands.ContainsKey(slashAttr.Name)) 214 | commands.Add(slashAttr.Name, new SlashCommand(slashAttr.Name, 215 | Array.Empty(), 216 | slashAttr.GuildId)); 217 | 218 | if(commands.TryGetValue(slashAttr.Name, out var slashCommand)) 219 | { //... and then make sure it has subcommands ... 220 | if (slashCommand.Subcommands is null) 221 | throw new Exception("Can't add a subcommand to a Slash Command without subcommands."); 222 | // ... then get or add the subcommand for this command method ... 223 | if(!slashCommand.Subcommands.ContainsKey(subGroupAttr.Name)) 224 | slashCommand.Subcommands.Add(subGroupAttr.Name, 225 | new SlashSubcommandGroup(subGroupAttr.Name, 226 | subGroupClass.GetCustomAttribute()?.Description ?? "n/a")); 227 | 228 | if (slashCommand.Subcommands.TryGetValue(subGroupAttr.Name, out var slashSubcommandGroup)) 229 | { //... and ensure the command does not already exsist ... 230 | if (slashSubcommandGroup.Commands.ContainsKey(attr.Name)) 231 | throw new Exception("Can't have two subcommands of the same name!"); 232 | 233 | // ... then build an instance of the command ... 234 | // TODO: Actually make this dependency injection isntead of just passing the 235 | // services into the base slash command class. 236 | var instance = Activator.CreateInstance(subGroupClass, _services); 237 | // ... verify it was made correctly ... 238 | if (instance is null) 239 | throw new Exception("Failed to build command class instance"); 240 | // ... and save the subcommand. 241 | slashSubcommandGroup.Commands.Add(attr.Name, 242 | new SlashSubcommand(attr.Name, 243 | desc: cmd.GetCustomAttribute()?.Description ?? "n/a", 244 | cmd, 245 | (BaseSlashCommandModule)instance 246 | ) 247 | ); 248 | } 249 | else 250 | { //... otherwise tell the user no subcommand was found. 251 | throw new Exception("Failed to get a subcommand grouping!"); 252 | } 253 | } 254 | else 255 | { // ... otherwise tell the user no slash command was found. 256 | throw new Exception("Failed to get Slash Command"); 257 | } 258 | } 259 | else 260 | { // ... otherwise tell the user a subcommand group needs to be in a slash command class 261 | throw new Exception("A Subcommand Group is required to be a child of a class marked with a SlashCommand attribute"); 262 | } 263 | } 264 | else 265 | { // ... otherwise tell the user a subcommand needs to be in a subcommand group 266 | throw new Exception("A Subcommand is required to be inside a class marked with a SubcommandGroup attribute"); 267 | } 268 | } 269 | else 270 | { // ... if there was no subcommand attribute, store if for checking 271 | // if the method is a non-subcommand command. 272 | nonSubcommandCommands.Add(cmd); 273 | } 274 | } 275 | 276 | _logger.LogInformation("... Added subcommand groupings, reading non-subcommand methods ..."); 277 | 278 | // ... take the non-subcommand list we built in the last loop ... 279 | foreach(var cmd in nonSubcommandCommands) 280 | { 281 | // ... and see if any of the methods have a SlashCommand attribute ... 282 | SlashCommandAttribute? attr; 283 | if((attr = cmd.GetCustomAttribute(false)) is not null) 284 | { 285 | // ... if they do, make sure it is not also a subcommand ... 286 | if (cmd.GetCustomAttribute(false) is not null) 287 | throw new Exception("A command can not be a subcommand as well."); 288 | // ... and that it does not already exsist ... 289 | if (commands.ContainsKey(attr.Name)) 290 | throw new Exception($"A command with the name {attr.Name} already exsists."); 291 | // ... and that it has a declaring type AND that type is a subclass of SlashCommandBase ... 292 | if (cmd.DeclaringType is null 293 | || !cmd.DeclaringType.IsSubclassOf(typeof(BaseSlashCommandModule))) 294 | throw new Exception("A SlashCommand method needs to be in a class."); 295 | // ... then build and instance of the class ... 296 | // TODO: Actually make this dependency injection isntead of just passing the 297 | // services into the base slash command class. 298 | var instance = Activator.CreateInstance(cmd.DeclaringType, _services); 299 | // ... verify the instance is not null ... 300 | if (instance is null) 301 | throw new Exception("Failed to build command class instance"); 302 | // ... and the full comamnd object to the command dict. 303 | commands.Add(attr.Name, 304 | new SlashCommand(attr.Name, 305 | new SlashSubcommand( 306 | attr.Name, 307 | desc: cmd.GetCustomAttribute()?.Description ?? "n/a", 308 | cmd, 309 | (BaseSlashCommandModule)instance 310 | ), 311 | attr.GuildId 312 | )); 313 | } 314 | // ... otherwise, ignore the method. 315 | } 316 | 317 | _logger.LogInformation("... Commands from source loaded."); 318 | 319 | Commands = new(commands); 320 | } 321 | 322 | private async Task BulkUpdateCommands() 323 | { 324 | List commandList = new(); 325 | foreach (SlashCommand c in Commands.Values) 326 | commandList.Add(BuildApplicationCommand(c)); 327 | 328 | var json = JsonConvert.SerializeObject(commandList, Formatting.None, new JsonSerializerSettings 329 | { 330 | NullValueHandling = NullValueHandling.Ignore, 331 | DefaultValueHandling = DefaultValueHandling.Ignore 332 | }); 333 | 334 | HttpRequestMessage msg = new(); 335 | msg.Method = HttpMethod.Put; 336 | msg.Content = new StringContent(json); 337 | msg.Content.Headers.ContentType = new("application/json"); 338 | msg.RequestUri = new Uri($"https://discord.com/api/applications/{BotId}/commands"); 339 | 340 | _logger.LogInformation("Executing command update"); 341 | 342 | var response = await _client.SendAsync(msg); 343 | 344 | if(response.IsSuccessStatusCode) 345 | { 346 | var responseJson = await response.Content.ReadAsStringAsync(); 347 | 348 | var commands = JsonConvert.DeserializeObject>(responseJson); 349 | 350 | foreach(DiscordApplicationCommand newCommand in commands) 351 | { 352 | // ... and the old command data ... 353 | var oldCommand = Commands[newCommand.Name]; 354 | // ... then update the old command with the new command. 355 | if (newCommand is not null && oldCommand is not null) 356 | { 357 | oldCommand.ApplicationId = newCommand.ApplicationId; 358 | oldCommand.CommandId = newCommand.Id; 359 | } 360 | } 361 | 362 | _logger.LogInformation("Command update complete."); 363 | } 364 | else 365 | { 366 | _logger.LogCritical($"Command update failed. {response.ReasonPhrase}"); 367 | } 368 | } 369 | 370 | private DiscordApplicationCommand BuildApplicationCommand(SlashCommand cmd) 371 | { 372 | // Create the command builder object ... 373 | var builder = new ApplicationCommandBuilder() 374 | .WithName(cmd.Name) // ... set the command name ... 375 | .WithDescription(cmd.Description); // ... and its description ... 376 | // ... then, if it has subcommands ... 377 | if(cmd.Subcommands is not null) 378 | { // ... for every subcommand, add the option for it. 379 | foreach (var sub in cmd.Subcommands) 380 | builder.AddOption(GetSubcommandOption(sub.Value)); 381 | } 382 | else if(cmd.Command is not null) 383 | { // ... otherwise directly add the paramater options for this command ... 384 | var parameters = cmd.Command.ExecutionMethod.GetParameters(); 385 | if (parameters.Length > 1) 386 | { // ... if there are any other paramaters besides the Interaction. 387 | builder.Options = GetCommandAttributeOptions(parameters[1..]); 388 | } // ... otherwise we leave this as null. 389 | } 390 | // ... then build and return the command. 391 | return builder.Build(); 392 | } 393 | 394 | private ApplicationCommandOptionBuilder GetSubcommandOption(SlashSubcommandGroup commandGroup) 395 | { // ... propogate the subcommand group ... 396 | var builder = new ApplicationCommandOptionBuilder() 397 | .WithName(commandGroup.Name) // ... with a name ... 398 | .WithDescription(commandGroup.Description) // ... description ... 399 | .WithType(ApplicationCommandOptionType.SubCommandGroup); // ... a group type ... 400 | // ... then load the commands into the group ... 401 | foreach (var cmd in commandGroup.Commands) 402 | builder.AddOption(GetSubcommandOption(cmd.Value)); 403 | // ... and return the command option builder. 404 | return builder; 405 | } 406 | 407 | private ApplicationCommandOptionBuilder GetSubcommandOption(SlashSubcommand cmd) 408 | { // ... propogate the subcommand ... 409 | var builder = new ApplicationCommandOptionBuilder() 410 | .WithName(cmd.Name) // ... with a name ... 411 | .WithDescription(cmd.Description) // ... its description ... 412 | .WithType(ApplicationCommandOptionType.SubCommand); // ... the subcommand type ... 413 | // ... then get its parameter ... 414 | var parameters = cmd.ExecutionMethod.GetParameters(); 415 | // ... and if there is more than just the Interaction parameter ... 416 | if (parameters.Length > 1) 417 | { // ... load the parmeter options in. 418 | builder.Options = GetCommandAttributeOptions(parameters[1..]); 419 | } 420 | // ... then return the builder. 421 | return builder; 422 | } 423 | 424 | private List GetCommandAttributeOptions(ParameterInfo[] parameters) 425 | { // ... create a list for all the command options ... 426 | List builders = new(); 427 | // ... and for each parameter ... 428 | foreach(var param in parameters) 429 | { // ... propograte the inital command options ... 430 | var b = new ApplicationCommandOptionBuilder() 431 | .WithName(param.Name ?? "noname") // ... with a name ... 432 | .WithDescription(param.GetCustomAttribute()?.Description ?? "n/a") // ... a description ... 433 | .IsRequired(!param.HasDefaultValue) // ... if it is required or not ... 434 | .IsDefault(param.GetCustomAttribute() is not null); // ... if it is the default ... 435 | // ... then see if it is an enum ... 436 | if(param.ParameterType.IsEnum) 437 | { //... and load it in as an int with choices ... 438 | b.WithType(ApplicationCommandOptionType.Integer) 439 | .WithChoices(param.ParameterType); 440 | } 441 | else 442 | { // ... or as a regualr parameter ... 443 | var type = ApplicationCommandOptionTypeExtensions.GetOptionType(param); 444 | if (type is null) // ... and get the type and verify it is valid ... 445 | throw new Exception("Invalid paramater type of slash command."); 446 | // ... and add the type. 447 | b.WithType(type.Value); 448 | } 449 | // ... then store it for the return value. 450 | builders.Add(b); 451 | } 452 | // ... then return the builders list. 453 | return builders; 454 | } 455 | #endregion 456 | } 457 | } 458 | -------------------------------------------------------------------------------- /ExampleGatewayBot/Commands/SlashCommandTesting.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | using DSharpPlus; 8 | using DSharpPlus.Entities; 9 | using DSharpPlus.SlashCommands.Attributes; 10 | using DSharpPlus.SlashCommands.Entities; 11 | using DSharpPlus.SlashCommands.Entities.Builders; 12 | using DSharpPlus.SlashCommands.Enums; 13 | 14 | namespace ExampleGatewayBot.Commands 15 | { 16 | public class SlashCommandTesting : BaseSlashCommandModule 17 | { 18 | public SlashCommandTesting(IServiceProvider p) : base(p) { } 19 | 20 | [SlashCommand("ping", 431462786900688896)] 21 | public async Task SlashCommandTestingAsync(InteractionContext ctx) 22 | => await ctx.ReplyAsync($"Pong: {Program.Discord.Ping}"); 23 | 24 | [SlashCommand("say", 431462786900688896)] 25 | public async Task SlashCommandTestingTwoAsync(InteractionContext ctx, string toSay) 26 | => await ctx.ReplyAsync(toSay); 27 | 28 | [SlashCommand("add", 431462786900688896)] 29 | public async Task MathCommandAsync(InteractionContext ctx, int num1, int num2) 30 | => await ctx.ReplyAsync($"{num1 + num2}"); 31 | 32 | [SlashCommand("subtract", 431462786900688896)] 33 | public async Task SubtractCommandAsync(InteractionContext ctx, int num1, int num3) 34 | => await ctx.ReplyAsync($"{num1 - num3}"); 35 | } 36 | 37 | [SlashCommand("subs", 431462786900688896)] 38 | public class ArgumentSubcommandCommand : BaseSlashCommandModule 39 | { 40 | 41 | public ArgumentSubcommandCommand(IServiceProvider provider) : base(provider) 42 | { 43 | 44 | } 45 | } 46 | 47 | [SlashSubcommandGroup("params")] 48 | public class ArgumentSubcommandCommandGroup : ArgumentSubcommandCommand 49 | { 50 | 51 | public ArgumentSubcommandCommandGroup(IServiceProvider provider) : base(provider) 52 | { 53 | 54 | } 55 | 56 | [SlashSubcommand("test")] 57 | public async Task ArgumentSubcommandCommandAsync(InteractionContext ctx, TestChoices choice, int age, string name, bool female, 58 | DiscordUser user, DiscordChannel channel, DiscordRole role) 59 | { 60 | var response = new InteractionResponseBuilder() 61 | .WithType(InteractionResponseType.ChannelMessageWithSource) 62 | .WithData(new InteractionApplicationCommandCallbackDataBuilder() 63 | .WithEmbed(new DiscordEmbedBuilder() 64 | .WithTitle("Testing Arguments!") 65 | .WithDescription($"Choice: {choice}\n" + 66 | $"Age: {age}\n" + 67 | $"Name: {name}\n" + 68 | $"Female? {female}\n" + 69 | $"User: {user.Username}\n" + 70 | $"Channel: {channel.Name}\n" + 71 | $"Role: {role.Name}")) 72 | .WithContent("How's Life?")); 73 | 74 | await ctx.ReplyAsync(response.Build()); 75 | } 76 | 77 | public enum TestChoices 78 | { 79 | Happy = 2, 80 | Sad, 81 | Quiet, 82 | Tall, 83 | Short 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /ExampleGatewayBot/Configuration Examples/bot_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "token": "token_here", 3 | "prefix": "." 4 | } -------------------------------------------------------------------------------- /ExampleGatewayBot/ExampleGatewayBot.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net5.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | PreserveNewest 15 | true 16 | PreserveNewest 17 | 18 | 19 | Never 20 | true 21 | PreserveNewest 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /ExampleGatewayBot/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Reflection; 4 | using System.Threading.Tasks; 5 | 6 | using DSharpPlus; 7 | using DSharpPlus.CommandsNext; 8 | using DSharpPlus.SlashCommands; 9 | using DSharpPlus.SlashCommands.Entities.Builders; 10 | using DSharpPlus.SlashCommands.Enums; 11 | 12 | using Microsoft.Extensions.DependencyInjection; 13 | using Microsoft.Extensions.Logging; 14 | 15 | using Newtonsoft.Json.Linq; 16 | 17 | namespace ExampleGatewayBot 18 | { 19 | class Program 20 | { 21 | public static DiscordClient Discord; 22 | public static DiscordSlashClient Slash; 23 | static void Main(string[] args) 24 | { 25 | // Read the config json file ... 26 | using FileStream fs = new(Path.Join("Config", "bot_config.json"), FileMode.Open); 27 | using StreamReader sr = new(fs); 28 | var json = sr.ReadToEnd(); 29 | 30 | var jobj = JObject.Parse(json); 31 | // ... create a new DiscordClient for the bot ... 32 | Discord = new DiscordClient(new DiscordConfiguration 33 | { 34 | Token = jobj["token"].ToString(), 35 | TokenType = TokenType.Bot, 36 | ShardCount = 1, 37 | Intents = DiscordIntents.AllUnprivileged, 38 | MinimumLogLevel = Microsoft.Extensions.Logging.LogLevel.Debug 39 | }); 40 | 41 | // ... create a custom default response ... 42 | var defaultResponseData = new InteractionApplicationCommandCallbackDataBuilder() 43 | .WithContent("`Test Automated Response`"); 44 | 45 | // ... use the discord client to build the Slash Client config ... 46 | Slash = new DiscordSlashClient(new DiscordSlashConfiguration 47 | { 48 | Client = Discord, 49 | Token = jobj["token"].ToString(), 50 | DefaultResponseType = InteractionResponseType.ChannelMessageWithSource, 51 | DefaultResponseData = defaultResponseData, 52 | Logger = Discord.Logger 53 | }); 54 | 55 | // ... register normal bot commands ... 56 | var next = Discord.UseCommandsNext(new CommandsNextConfiguration 57 | { 58 | StringPrefixes = new string[] { jobj["prefix"].ToString() } 59 | }); 60 | 61 | next.RegisterCommands(Assembly.GetExecutingAssembly()); 62 | // ... register the interaction event ... 63 | Discord.InteractionCreated += Slash.HandleGatewayEvent; 64 | Discord.InteractionCreated += (x, y) => 65 | { 66 | Discord.Logger.LogInformation("Interaction Created Received"); 67 | return Task.CompletedTask; 68 | }; 69 | Discord.ApplicationCommandCreated += Discord_ApplicationCommandCreated; 70 | Discord.ApplicationCommandDeleted += Discord_ApplicationCommandDeleted; 71 | Discord.ApplicationCommandUpdated += Discord_ApplicationCommandUpdated; 72 | 73 | // ... connect to discord ... 74 | Discord.ConnectAsync().GetAwaiter().GetResult(); 75 | // ... register the slash commands ... 76 | Slash.RegisterCommands(Assembly.GetExecutingAssembly()); 77 | 78 | // ... start the slash client ... 79 | Slash.StartAsync().GetAwaiter().GetResult(); 80 | 81 | // ... and prevent this from stopping. 82 | Task.Delay(-1).GetAwaiter().GetResult(); 83 | } 84 | 85 | private static Task Discord_ApplicationCommandUpdated(DiscordClient sender, DSharpPlus.EventArgs.ApplicationCommandEventArgs e) 86 | { 87 | Discord.Logger.LogInformation($"Shard {sender.ShardId} sent application command updated: {e.Command.Name}: {e.Command.Id} for {e.Command.ApplicationId}"); 88 | return Task.CompletedTask; 89 | } 90 | private static Task Discord_ApplicationCommandDeleted(DiscordClient sender, DSharpPlus.EventArgs.ApplicationCommandEventArgs e) 91 | { 92 | Discord.Logger.LogInformation($"Shard {sender.ShardId} sent application command deleted: {e.Command.Name}: {e.Command.Id} for {e.Command.ApplicationId}"); 93 | return Task.CompletedTask; 94 | } 95 | private static Task Discord_ApplicationCommandCreated(DiscordClient sender, DSharpPlus.EventArgs.ApplicationCommandEventArgs e) 96 | { 97 | Discord.Logger.LogInformation($"Shard {sender.ShardId} sent application command created: {e.Command.Name}: {e.Command.Id} for {e.Command.ApplicationId}"); 98 | return Task.CompletedTask; 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /ExampleHTTPBot/Api/DiscordSlashCommandController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | 6 | using DSharpPlus; 7 | using DSharpPlus.SlashCommands.Entities.Builders; 8 | using DSharpPlus.SlashCommands.Enums; 9 | 10 | using Microsoft.AspNetCore.Mvc; 11 | using Microsoft.Extensions.Logging; 12 | 13 | using Newtonsoft.Json; 14 | using Newtonsoft.Json.Linq; 15 | 16 | using Sodium; 17 | 18 | namespace ExampleBot.Api 19 | { 20 | [Route("api/discordslash")] 21 | [ApiController] 22 | public class DiscordSlashCommandController : ControllerBase 23 | { 24 | private readonly ILogger _logger; 25 | 26 | public DiscordSlashCommandController(ILogger logger) 27 | { 28 | _logger = logger; 29 | } 30 | 31 | [HttpPost("")] 32 | //[ApiExplorerSettings(IgnoreApi = true)] 33 | public async Task DiscordEndpointHandler() 34 | { 35 | string raw; 36 | // Request validation 37 | try 38 | { 39 | // Get the verification headers from the request ... 40 | var signature = Request.Headers["X-Signature-Ed25519"].ToString(); 41 | var timestamp = Request.Headers["X-Signature-Timestamp"].ToString(); 42 | // ... convert the signature and public key to byte[] to use in verification ... 43 | var byteSig = Utils.HexStringToByteArray(signature); 44 | var byteKey = Utils.HexStringToByteArray(Startup.PublicKey); 45 | // ... read the body from the request ... 46 | using var reader = new StreamReader(Request.Body); 47 | if (reader.BaseStream.CanSeek) 48 | reader.BaseStream.Seek(0, SeekOrigin.Begin); 49 | raw = await reader.ReadToEndAsync(); 50 | // ... add the timestamp and convert it to a byte[] ... 51 | string body = timestamp + raw; 52 | var byteBody = Encoding.Default.GetBytes(body); 53 | // ... run through a verification with all the byte[]s ... 54 | bool validated = PublicKeyAuth.VerifyDetached(byteSig, byteBody, byteKey); 55 | // ... if it is not validated ... 56 | if(!validated) 57 | { // ... log a warning and return a 401 Unauthorized. 58 | _logger.LogWarning("Failed to validate POST request for Discord API."); 59 | return Unauthorized("Invalid Request Signature"); 60 | } 61 | else 62 | { // ... otherwise continue onwards. 63 | _logger.LogInformation("Received POST from Discord"); 64 | } 65 | } 66 | catch (Exception ex) 67 | { // ... if an error occurred, log the error and return at 401 Unauthorized. 68 | _logger.LogInformation(ex, "Decryption failed."); 69 | _logger.LogWarning("Failed to validate POST request for Discord API."); 70 | return Unauthorized("Invalid Request Signature"); 71 | } 72 | 73 | // Response parsing 74 | JObject json; 75 | try 76 | { // ... attempt to create a json object from the body ... 77 | json = JObject.Parse(raw); 78 | } 79 | catch 80 | { // ... if that fails, return a 400 Bad Request. 81 | return BadRequest(); 82 | } 83 | // ... check to see if this is a ping to the webhook ... 84 | if (json.ContainsKey("type") && (int)json["type"] == (int)InteractionType.Ping) 85 | { 86 | return Ok( 87 | JsonConvert.SerializeObject( 88 | new InteractionResponseBuilder() 89 | .WithType(InteractionResponseType.Pong) 90 | .Build() 91 | ) 92 | ); // ... and return the pong if it is. 93 | } 94 | else 95 | {// ... then pass the raw request body to the client ... 96 | var response = await Program.Slash.HandleWebhookPost(raw); 97 | if (response is not null) // ... if the clients response is not null ... 98 | return Ok(JsonConvert.SerializeObject(response)); // ... serialize it and send it. 99 | else return BadRequest("Failed to parse request JSON."); // ... or send a bad request message. 100 | } 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /ExampleHTTPBot/Commands/Discord/PingDiscordCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | using DSharpPlus.CommandsNext; 8 | using DSharpPlus.CommandsNext.Attributes; 9 | using DSharpPlus.Entities; 10 | 11 | namespace ExampleBot.Commands.Discord 12 | { 13 | public class PingDiscordCommand : BaseCommandModule 14 | { 15 | [Command("ping")] 16 | public async Task Ping(CommandContext ctx) 17 | { 18 | Stopwatch timer = new Stopwatch(); 19 | var pingEmbed = new DiscordEmbedBuilder().WithColor(DiscordColor.CornflowerBlue).WithTitle($"Ping for Shard {ctx.Client.ShardId}"); 20 | pingEmbed.AddField("WS Latency:", $"{ctx.Client.Ping}ms"); 21 | timer.Start(); 22 | DiscordMessage msg = await ctx.RespondAsync(pingEmbed); 23 | await msg.ModifyAsync(null, pingEmbed.AddField("Response Time: (:ping_pong:)", $"{timer.ElapsedMilliseconds}ms").Build()); 24 | timer.Stop(); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /ExampleHTTPBot/Commands/Slash/ArgumentExampleCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | using DSharpPlus; 7 | using DSharpPlus.Entities; 8 | using DSharpPlus.SlashCommands.Attributes; 9 | using DSharpPlus.SlashCommands.Entities; 10 | using DSharpPlus.SlashCommands.Entities.Builders; 11 | using DSharpPlus.SlashCommands.Enums; 12 | 13 | using Microsoft.Extensions.DependencyInjection; 14 | 15 | using static ExampleBot.Program; 16 | 17 | namespace ExampleBot.Commands.Slash 18 | { 19 | public class ArgumentExampleCommand : BaseSlashCommandModule 20 | { 21 | TestService service; 22 | 23 | public ArgumentExampleCommand(IServiceProvider provider) : base(provider) 24 | { 25 | service = provider.GetService(); 26 | } 27 | 28 | [SlashCommand("args", 750486424469372970)] 29 | public async Task ArgumentExampleCommandAsync(InteractionContext ctx, TestChoices choice, int age, string name, bool female, 30 | DiscordUser user, DiscordChannel channel, DiscordRole role) 31 | { 32 | var response = new InteractionResponseBuilder() 33 | .WithType(InteractionResponseType.ChannelMessageWithSource) 34 | .WithData(new InteractionApplicationCommandCallbackDataBuilder() 35 | .WithEmbed(new DiscordEmbedBuilder() 36 | .WithTitle("Testing Arguments!") 37 | .WithDescription($"Choice: {choice}\n" + 38 | $"Age: {age}\n" + 39 | $"Name: {name}\n" + 40 | $"Female? {female}\n" + 41 | $"User: {user.Username}\n" + 42 | $"Channel: {channel.Name}\n" + 43 | $"Role: {role.Name}")) 44 | .WithContent("How's Life?")); 45 | 46 | await ctx.ReplyAsync(response.Build()); 47 | } 48 | 49 | public enum TestChoices 50 | { 51 | Happy = 2, 52 | Sad, 53 | Quiet, 54 | Tall, 55 | Short 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /ExampleHTTPBot/Commands/Slash/ArgumentSubcommandCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | using DSharpPlus; 7 | using DSharpPlus.Entities; 8 | using DSharpPlus.SlashCommands.Attributes; 9 | using DSharpPlus.SlashCommands.Entities; 10 | using DSharpPlus.SlashCommands.Entities.Builders; 11 | using DSharpPlus.SlashCommands.Enums; 12 | 13 | using Microsoft.Extensions.DependencyInjection; 14 | 15 | using static ExampleBot.Program; 16 | 17 | namespace ExampleBot.Commands.Slash 18 | { 19 | [SlashCommand("subs", 750486424469372970)] 20 | public class ArgumentSubcommandCommand : BaseSlashCommandModule 21 | { 22 | TestService service; 23 | 24 | public ArgumentSubcommandCommand(IServiceProvider provider) : base(provider) 25 | { 26 | service = provider.GetService(); 27 | } 28 | } 29 | 30 | [SlashSubcommandGroup("params")] 31 | public class ArgumentSubcommandCommandGroup : ArgumentSubcommandCommand 32 | { 33 | TestService service; 34 | 35 | public ArgumentSubcommandCommandGroup(IServiceProvider provider) : base(provider) 36 | { 37 | service = provider.GetService(); 38 | } 39 | 40 | [SlashSubcommand("test")] 41 | public async Task ArgumentSubcommandCommandAsync(InteractionContext ctx, TestChoices choice, int age, string name, bool female, 42 | DiscordUser user, DiscordChannel channel, DiscordRole role) 43 | { 44 | var response = new InteractionResponseBuilder() 45 | .WithType(InteractionResponseType.ChannelMessageWithSource) 46 | .WithData(new InteractionApplicationCommandCallbackDataBuilder() 47 | .WithEmbed(new DiscordEmbedBuilder() 48 | .WithTitle("Testing Arguments!") 49 | .WithDescription($"Choice: {choice}\n" + 50 | $"Age: {age}\n" + 51 | $"Name: {name}\n" + 52 | $"Female? {female}\n" + 53 | $"User: {user.Username}\n" + 54 | $"Channel: {channel.Name}\n" + 55 | $"Role: {role.Name}")) 56 | .WithContent("How's Life?")); 57 | 58 | await ctx.ReplyAsync(response.Build()); 59 | } 60 | 61 | public enum TestChoices 62 | { 63 | Happy = 2, 64 | Sad, 65 | Quiet, 66 | Tall, 67 | Short 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /ExampleHTTPBot/Commands/Slash/HelloWorldSlashCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | using DSharpPlus; 7 | using DSharpPlus.Entities; 8 | using DSharpPlus.SlashCommands.Attributes; 9 | using DSharpPlus.SlashCommands.Entities; 10 | using DSharpPlus.SlashCommands.Entities.Builders; 11 | using DSharpPlus.SlashCommands.Enums; 12 | 13 | using Microsoft.Extensions.DependencyInjection; 14 | 15 | using static ExampleBot.Program; 16 | 17 | namespace ExampleBot.Commands.Slash 18 | { 19 | public class HelloWorldSlashCommand : BaseSlashCommandModule 20 | { 21 | TestService service; 22 | 23 | public HelloWorldSlashCommand(IServiceProvider provider) : base(provider) 24 | { 25 | service = provider.GetService(); 26 | } 27 | 28 | [SlashCommand("hello", 750486424469372970)] 29 | public async Task HelloWorldSlashCommandAsync(InteractionContext ctx) 30 | { 31 | var response = new InteractionResponseBuilder() 32 | .WithType(InteractionResponseType.ChannelMessageWithSource) 33 | .WithData(new InteractionApplicationCommandCallbackDataBuilder() 34 | .WithEmbed(new DiscordEmbedBuilder() 35 | .WithTitle("Hello World!") 36 | .WithDescription($"And hello to you too, {ctx.Interaction.User.Username}")) 37 | .WithContent("How's Life?")); 38 | 39 | await ctx.ReplyAsync(response.Build()); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ExampleHTTPBot/Commands/Slash/SubcommandExampleSlashCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | using DSharpPlus.SlashCommands.Attributes; 5 | using DSharpPlus.SlashCommands.Entities; 6 | 7 | namespace ExampleBot.Commands.Slash 8 | { 9 | [SlashCommand("sub", 750486424469372970)] 10 | public class SubcommandExampleSlashCommand : BaseSlashCommandModule 11 | { 12 | // NOTE: This way of DI will change at some point when I get around to making it actual DI 13 | public SubcommandExampleSlashCommand(IServiceProvider p) : base(p) { } 14 | } 15 | 16 | [SlashSubcommandGroup("group")] 17 | public class SubcommandGroup : SubcommandExampleSlashCommand 18 | { 19 | public SubcommandGroup(IServiceProvider p) : base(p) { } 20 | 21 | [SlashSubcommand("command")] 22 | public async Task CommandAsync(InteractionContext ctx) 23 | { 24 | await ctx.ReplyAsync("This is a subcommand"); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /ExampleHTTPBot/Configuration Examples/bot_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "token": "token_here", 3 | "prefix": "." 4 | } -------------------------------------------------------------------------------- /ExampleHTTPBot/ExampleHTTPBot.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | Never 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /ExampleHTTPBot/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Threading.Tasks; 7 | 8 | using DSharpPlus; 9 | using DSharpPlus.CommandsNext; 10 | using DSharpPlus.SlashCommands; 11 | using DSharpPlus.SlashCommands.Entities.Builders; 12 | using DSharpPlus.SlashCommands.Enums; 13 | using Microsoft.AspNetCore.Hosting; 14 | using Microsoft.Extensions.Configuration; 15 | using Microsoft.Extensions.DependencyInjection; 16 | using Microsoft.Extensions.Hosting; 17 | using Microsoft.Extensions.Logging; 18 | 19 | using Newtonsoft.Json.Linq; 20 | 21 | namespace ExampleBot 22 | { 23 | public class Program 24 | { 25 | public static DiscordClient Discord { get; private set; } 26 | public static DiscordSlashClient Slash { get; private set; } 27 | 28 | public static void Main(string[] args) 29 | { 30 | MainAsync(args).GetAwaiter().GetResult(); 31 | } 32 | 33 | public class TestService 34 | { 35 | 36 | } 37 | 38 | public static async Task MainAsync(string[] args) 39 | { 40 | // Read the config json file ... 41 | using FileStream fs = new(Path.Join("Config", "bot_config.json"), FileMode.Open); 42 | using StreamReader sr = new(fs); 43 | var json = await sr.ReadToEndAsync(); 44 | 45 | var jobj = JObject.Parse(json); 46 | // ... create a new DiscordClient for the bot ... 47 | Discord = new DiscordClient(new DiscordConfiguration 48 | { 49 | Token = jobj["token"].ToString(), 50 | TokenType = TokenType.Bot, 51 | ShardCount = 1, 52 | Intents = DiscordIntents.AllUnprivileged 53 | }); 54 | // ... register commands ... 55 | var next = Discord.UseCommandsNext(new CommandsNextConfiguration 56 | { 57 | StringPrefixes = new string[] { jobj["prefix"].ToString() } 58 | }); 59 | 60 | next.RegisterCommands(Assembly.GetExecutingAssembly()); 61 | // ... connect to discord ... 62 | await Discord.ConnectAsync(); 63 | 64 | var defaultResponseData = new InteractionApplicationCommandCallbackDataBuilder() 65 | .WithContent("`Test Automated Response`"); 66 | 67 | IServiceCollection c = new ServiceCollection(); 68 | c.AddTransient(); 69 | 70 | var provider = c.BuildServiceProvider(); 71 | 72 | // ... use the discord connection to build the Slash Client config ... 73 | Slash = new DiscordSlashClient(new DiscordSlashConfiguration 74 | { 75 | Client = Discord, 76 | Token = jobj["token"].ToString(), 77 | DefaultResponseType = InteractionResponseType.ChannelMessageWithSource, 78 | DefaultResponseData = defaultResponseData, 79 | Services = provider 80 | }); 81 | 82 | Slash.RegisterCommands(Assembly.GetExecutingAssembly()); 83 | 84 | // ... start the slash client ... 85 | await Slash.StartAsync(); 86 | // ... build the web server for receiving HTTP POSTs from discord ... 87 | var host = Host.CreateDefaultBuilder(args) 88 | .ConfigureWebHostDefaults(webBuilder => 89 | { 90 | webBuilder.UseStartup(); 91 | }); 92 | // ... and start the webserver ... 93 | await host.Build().StartAsync(); 94 | // ... then hold here to prevent premature closing. 95 | await Task.Delay(-1); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /ExampleHTTPBot/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:55782", 8 | "sslPort": 44398 9 | } 10 | }, 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": true, 15 | "launchUrl": "swagger", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "ExampleBot": { 21 | "commandName": "Project", 22 | "dotnetRunMessages": "true", 23 | "launchBrowser": true, 24 | "launchUrl": "swagger", 25 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 26 | "environmentVariables": { 27 | "ASPNETCORE_ENVIRONMENT": "Development" 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ExampleHTTPBot/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | using Microsoft.AspNetCore.Builder; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.AspNetCore.HttpOverrides; 9 | using Microsoft.AspNetCore.HttpsPolicy; 10 | using Microsoft.AspNetCore.Mvc; 11 | using Microsoft.Extensions.Configuration; 12 | using Microsoft.Extensions.DependencyInjection; 13 | using Microsoft.Extensions.Hosting; 14 | using Microsoft.Extensions.Logging; 15 | using Microsoft.OpenApi.Models; 16 | 17 | namespace ExampleBot 18 | { 19 | public class Startup 20 | { 21 | public static string PublicKey = ""; 22 | 23 | public Startup(IConfiguration configuration) 24 | { 25 | Configuration = configuration; 26 | PublicKey = Configuration["PublicKey"]; 27 | } 28 | 29 | public IConfiguration Configuration { get; } 30 | 31 | // This method gets called by the runtime. Use this method to add services to the container. 32 | public void ConfigureServices(IServiceCollection services) 33 | { 34 | 35 | services.AddControllers(); 36 | services.AddSwaggerGen(c => 37 | { 38 | c.SwaggerDoc("v1", new OpenApiInfo { Title = "ExampleBot", Version = "v1" }); 39 | }); 40 | } 41 | 42 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 43 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 44 | { 45 | app.UseForwardedHeaders(new ForwardedHeadersOptions 46 | { 47 | ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto 48 | }); 49 | 50 | if (env.IsDevelopment()) 51 | { 52 | app.UseDeveloperExceptionPage(); 53 | app.UseSwagger(); 54 | app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "ExampleBot v1")); 55 | } 56 | 57 | app.UseHttpsRedirection(); 58 | 59 | app.UseRouting(); 60 | 61 | app.UseAuthorization(); 62 | 63 | app.UseEndpoints(endpoints => 64 | { 65 | endpoints.MapControllers(); 66 | }); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /ExampleHTTPBot/Utils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace ExampleBot 7 | { 8 | public static class Utils 9 | { 10 | public static readonly int[] HexValue = new int[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 11 | 0x06, 0x07, 0x08, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 12 | 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F }; 13 | 14 | // Code Snippet from: https://stackoverflow.com/a/5919521/11682098 15 | // Code Snippet by: Nathan Moinvaziri 16 | public static byte[] HexStringToByteArray(string Hex) 17 | { 18 | byte[] Bytes = new byte[Hex.Length / 2]; 19 | 20 | for (int x = 0, i = 0; i < Hex.Length; i += 2, x += 1) 21 | { 22 | Bytes[x] = (byte)(HexValue[Char.ToUpper(Hex[i + 0]) - '0'] << 4 | 23 | HexValue[Char.ToUpper(Hex[i + 1]) - '0']); 24 | } 25 | 26 | return Bytes; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /ExampleHTTPBot/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "PublicKey": "65f146bface11e87e28d5b5d677babfc07da6ef3fc6a7b13e74fecc7e85a92a4" 10 | } 11 | -------------------------------------------------------------------------------- /ExampleHTTPBot/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*", 10 | "PublicKey": "" 11 | } 12 | -------------------------------------------------------------------------------- /ExampleHTTPBot/sccfg_774671144719482881.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "hello", 4 | "version": 1, 5 | "guild_id": 750486424469372970, 6 | "command_id": 789290437416189955 7 | }, 8 | { 9 | "name": "subs", 10 | "version": 1, 11 | "guild_id": 750486424469372970, 12 | "command_id": 793528723516162048 13 | }, 14 | { 15 | "name": "sub", 16 | "version": 1, 17 | "guild_id": 750486424469372970, 18 | "command_id": 789511124517257287 19 | }, 20 | { 21 | "name": "args", 22 | "version": 1, 23 | "guild_id": 750486424469372970, 24 | "command_id": 793516028608643082 25 | } 26 | ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Andrew B 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NuGet.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DSharpPlus.SlashCommands 2 | A SlashCommand implementation for DSharpPlus. This does not connect to the gateway and gateway events or HTTP post events must be handled by the client. 3 | 4 | # Notices 5 | ### While commands will be added to Discord, not all limit checks are done by this utility. Please make sure you are not violating them yourself by checking the limits [here](#command-limits). 6 | ### Standard DI is not implemented, the IServiceProvider is how you can get services as of right now 7 | 8 | # Quickstart 9 | 10 | ## Install the Lib 11 | **Nuget Package:** `Soyvolon.DSharpPlus.SlashCommands` 12 | 13 | The package requires `DSharpPlus 4.0.0-nightly-00820`, along with `DSharpPlus.CommandsNext` and `DSharpPlus.Rest` of the same version. 14 | 15 | ## Creating a new Project 16 | Create a new .NET Core project for your Discord Bot (or add to an existing one). 17 | 18 | *If you are planning on using an HTTP connection, you must have an ASP.NET Core project (or equivalent web application).* 19 | 20 | ## Create the `DiscordSlashClient` 21 | A Discord Slash Client requires two things: 22 | 1. A Bot token for verification with Discord 23 | 2. An active Discord Client. Use `Client =` for a `DiscordClient` and `DiscordRestClient`, and `ShardedClient =` for a `DiscordShardedClient`. 24 | 25 | The Token and Application ID need to be from the same application. 26 | 27 | First, create a new DiscordSlashConfiguration: 28 | ```csharp 29 | var config = new DiscordSlashConfiguration 30 | { 31 | Client = , 32 | Token = 33 | } 34 | ``` 35 | Or by using a ShardedClient instead: 36 | ```csharp 37 | var config = new DiscordSlashConfiguration 38 | { 39 | ShardedClient = , 40 | Token = 41 | } 42 | ``` 43 | 44 | Then, just like in DSharpPlus, pass the Configuration into the constructor for the `DiscordSlashClient` to get your client object: 45 | ```csharp 46 | var client = new DiscordSlashClient(config); 47 | ``` 48 | 49 | You can customize the `DiscordSlashClient`'s custom responses with additional options in the configuration. For Example: 50 | ```csharp 51 | DefaultResponseType = InteractionResponseType.ChannelMessageWithSource, 52 | DefaultResponseData = new InteractionApplicationCommandCallbackDataBuilder() 53 | ``` 54 | 55 | ## Adding Commands 56 | Commands are created similarly to in `DSharpPlus.CommandsNext`. Some of the attributes that are looked for in commands are taken from `CommandsNext`. 57 | 58 | A command with no subcommands can be created like this: 59 | ```csharp 60 | public class HelloWorldSlashCommand : BaseSlashCommandModule 61 | { 62 | public HelloWorldSlashCommand(IServiceProvider provider) : base(provider) { } 63 | 64 | [SlashCommand("hello", 1, 750486424469372970)] 65 | public async Task HelloWorldSlashCommandAsync(InteractionContext ctx) 66 | { 67 | // Command Code here. 68 | } 69 | } 70 | ``` 71 | This creates the first version of the hello command, for the guild `750486424469372970`. If you want a global command, leave the guild out of the attribute: 72 | ```csharp 73 | [SlashCommand("hello", 1)] 74 | ``` 75 | 76 | > A Commands version number is used to tell the Lib when to send an update to discord. If you change the parameters of a command, update the version number or the command will not be updated with Discord. 77 | 78 | From there, you need to tell the library what Assemblies to look for commands in: 79 | ```csharp 80 | client.RegisterCommands(Assembly.GetExecutingAssembly()); 81 | ``` 82 | This will get all commands that are in the same Assembly as the executing assembly and register them with the Library. 83 | 84 | For how to create subcommands, see [Creating Subcommands](#creating-subcommands) 85 | 86 | ## Starting the Slash Client 87 | Starting the client is simple, just run: 88 | ```csharp 89 | await client.StartAsync(); 90 | ``` 91 | After you have registered commands. 92 | 93 | As the client starts, it will build a JSON file inside the executing assembly. This JSON file is needed to tell the Library what commands have already been registered with Discord. It is named `sccfg_.json`. 94 | > Deleting the JSON file can case unexpected command behavior where commands don't get deleted when they are supposed to, or commands are not updated correctly after version numbers update. 95 | 96 | > This JSON file is for a single application only, running the same client on two different applications can cause unexpected behavior as well. 97 | 98 | ## Next Steps 99 | 100 | 1. [Quickstart for Gateway Connections](#gateway-quickstart) 101 | 2. [Quickstart for HTTP Connections](#http-quickstart) 102 | 103 | # Gateway Quickstart 104 | > This is for basic uses. Use this if you do not have any experience in HTTP applications and/or web APIs! 105 | 106 | **The example for this style of project is under [`ExampleGatewayBot`](https://github.com/Soyvolon/DSharpPlus.SlashCommands/tree/master/ExampleGatewayBot)** 107 | 108 | ## Handling Incoming Gateway Messages 109 | On your `DiscordClient` or `DiscordShardedClient`, handle the `InteractionCreated` event just like any other event by passing the `Slash.HandleGatewayEvent` method to the `DiscordClient.InteractionCreated` event. 110 | 111 | ```csharp 112 | public static void Main(string[] args) 113 | { 114 | DiscordClient MyDiscordClient = new DiscordClient(MyDiscordConfiguration); 115 | DiscordSlashClient Slash = new DiscordSlashClient(MyDiscordSlashClientConfiguration); 116 | // ... 117 | MyDiscordClient.InteractionCreated += Slash.HandleGatewayEvent; 118 | // ... 119 | } 120 | ``` 121 | 122 | Its that simple with the gateway! 123 | 124 | > **Congrats, you now have SlashCommands setup!**
125 | *Example code was from the [`ExampleGatewayBot`](https://github.com/Soyvolon/DSharpPlus.SlashCommands/tree/master/ExampleGatewayBot) project.* 126 | 127 | For more things to do with slash commands, see [Further Options](#further-options) 128 | 129 | # HTTP Quickstart 130 | > This is for advanced uses! There is a lot more setup to be completed in this tutorial. For basic slash commands, the [Gateway Quickstart](#gateway-quickstart) is recommended. 131 | 132 | **The example for this style of project is under [`ExampleHTTPBot`](https://github.com/Soyvolon/DSharpPlus.SlashCommands/tree/master/ExampleHTTPBot)** 133 | 134 | ## Handling Incoming Webhooks 135 | Now that the `DiscordSlashClient` is running, you need to handle incoming webhooks from Discord. 136 | 137 | Create a new `Controller` in your ASP.NET Core project. In this example, we also get the ASP.NET Core logger to log events in the API: 138 | ```csharp 139 | [Route("api/discordslash")] 140 | [ApiController] 141 | public class DiscordSlashCommandController : ControllerBase 142 | { 143 | private readonly ILogger _logger; 144 | 145 | public DiscordSlashCommandController(ILogger logger) 146 | { 147 | _logger = logger; 148 | } 149 | } 150 | ``` 151 | The `[Route("api/discordslash")]` attribute determines where the program needs to listen for incoming requests. In this case, we are listening at `https://slash.example.com/api/discordslash` for incoming requests. 152 | 153 | The `[ApiController]` attribute tells ASP.NET that this class is apart of our API. 154 | 155 | Discord will send `POST` requests, so lets build a method to handle those within our class: 156 | ```csharp 157 | [HttpPost("")] 158 | //[ApiExplorerSettings(IgnoreApi = true)] 159 | public async Task DiscordEndpointHandler() 160 | { 161 | 162 | } 163 | ``` 164 | The `[HttpPost("")]` attribute tells ASP.NET to run this method when a `POST` request comes to the default route, or `/api/discordslash`. 165 | 166 | For Webhooks, you need to validate the request and response with a `401 Unauthorized` if it is a bad request as per [Discord Docs](https://discord.com/developers/docs/interactions/slash-commands#security-and-authorization) 167 | 168 | With that in mind, our example program is using [Sodium Core](https://github.com/tabrath/libsodium-core/) to validate our responses, along with some helper code form [StackOverflow](https://stackoverflow.com/a/5919521/11682098) that parses the Hex tokens to a `byte[]`. 169 | 170 | The Util Class: 171 | ```csharp 172 | public static class Utils 173 | { 174 | public static readonly int[] HexValue = new int[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 175 | 0x06, 0x07, 0x08, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 176 | 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F }; 177 | 178 | // Code Snippet from: https://stackoverflow.com/a/5919521/11682098 179 | // Code Snippet by: Nathan Moinvaziri 180 | public static byte[] HexStringToByteArray(string Hex) 181 | { 182 | byte[] Bytes = new byte[Hex.Length / 2]; 183 | 184 | for (int x = 0, i = 0; i < Hex.Length; i += 2, x += 1) 185 | { 186 | Bytes[x] = (byte)(HexValue[Char.ToUpper(Hex[i + 0]) - '0'] << 4 | 187 | HexValue[Char.ToUpper(Hex[i + 1]) - '0']); 188 | } 189 | 190 | return Bytes; 191 | } 192 | } 193 | ``` 194 | Request Validation (Goes inside the `DiscordEndpointHandler` method): 195 | ```csharp 196 | string raw; 197 | // Request validation 198 | try 199 | { 200 | // Get the verification headers from the request ... 201 | var signature = Request.Headers["X-Signature-Ed25519"].ToString(); 202 | var timestamp = Request.Headers["X-Signature-Timestamp"].ToString(); 203 | // ... convert the signature and public key to byte[] to use in verification ... 204 | var byteSig = Utils.HexStringToByteArray(signature); 205 | // NOTE: This reads your Public Key that you need to store somewhere. 206 | var byteKey = Utils.HexStringToByteArray(PublicKey); 207 | // ... read the body from the request ... 208 | using var reader = new StreamReader(Request.Body); 209 | if (reader.BaseStream.CanSeek) 210 | reader.BaseStream.Seek(0, SeekOrigin.Begin); 211 | raw = await reader.ReadToEndAsync(); 212 | // ... add the timestamp and convert it to a byte[] ... 213 | string body = timestamp + raw; 214 | var byteBody = Encoding.Default.GetBytes(body); 215 | // ... run through a verification with all the byte[]s ... 216 | bool validated = PublicKeyAuth.VerifyDetached(byteSig, byteBody, byteKey); 217 | // ... if it is not validated ... 218 | if(!validated) 219 | { // ... log a warning and return a 401 Unauthorized. 220 | _logger.LogWarning("Failed to validate POST request for Discord API."); 221 | return Unauthorized("Invalid Request Signature"); 222 | } 223 | else 224 | { // ... otherwise continue onwards. 225 | _logger.LogInformation("Received POST from Discord"); 226 | } 227 | } 228 | catch (Exception ex) 229 | { // ... if an error occurred, log the error and return at 401 Unauthorized. 230 | _logger.LogInformation(ex, "Decryption failed."); 231 | _logger.LogWarning("Failed to validate POST request for Discord API."); 232 | return Unauthorized("Invalid Request Signature"); 233 | } 234 | ``` 235 | > The body of the request is stored in the `string raw;` variable that is defined before the try catch block so it can be used in the next part. 236 | 237 | As explained in the code comments, this snippet does the following: 238 | 1. Get the request headers from the incoming request. 239 | 2. Converts the request signature to a `byte[]`. 240 | 3. Converts your application's public key (see discord developers page for your application to obtain this) into a `byte[]`. 241 | 4. Combines the timestamp and request body, and parses it into a `byte[]`. 242 | 5. Takes the three `byte[]`s and uses Sodium Core to validate the request. 243 | 6. If the code is invalid, returns the required `401 Unauthorized`, otherwise continues onward. 244 | 245 | After the request is validated, we can parse the request into either a `PONG` response, or pass it to the `DiscordSlashClient` which will return the default response for us to send back to Discord. 246 | 247 | In this case, we first attempt to parse the body into a `JObject` so we can see if this is a `PING`: 248 | ```csharp 249 | // Response parsing 250 | JObject json; 251 | try 252 | { // ... attempt to create a json object from the body ... 253 | json = JObject.Parse(raw); 254 | } 255 | catch 256 | { // ... if that fails, return a 400 Bad Request. 257 | return BadRequest(); 258 | } 259 | ``` 260 | Once that is done, we test if it is a `PING` and response accordingly: 261 | ```csharp 262 | // ... check to see if this is a ping to the webhook ... 263 | if (json.ContainsKey("type") && (int)json["type"] == (int)InteractionType.Ping) 264 | { 265 | return Ok( 266 | JsonConvert.SerializeObject( 267 | new InteractionResponseBuilder() 268 | .WithType(InteractionResponseType.Pong) 269 | .Build() 270 | ) 271 | ); // ... and return the pong if it is. 272 | } 273 | ``` 274 | Otherwise, we send this to the `DiscordSlashClient` to be handled `async`: 275 | ```csharp 276 | else 277 | {// ... then pass the raw request body to the client ... 278 | var response = await client.HandleWebhookPost(raw); 279 | if (response is not null) // ... if the clients response is not null ... 280 | return Ok(JsonConvert.SerializeObject(response)); // ... serialize it and send it. 281 | else return BadRequest("Failed to parse request JSON."); // ... or send a bad request message. 282 | } 283 | ``` 284 | The full Controller class looks like this: 285 | ```csharp 286 | using System; 287 | using System.IO; 288 | using System.Text; 289 | using System.Threading.Tasks; 290 | 291 | using DSharpPlus.SlashCommands.Entities.Builders; 292 | using DSharpPlus.SlashCommands.Enums; 293 | 294 | using Microsoft.AspNetCore.Mvc; 295 | using Microsoft.Extensions.Logging; 296 | 297 | using Newtonsoft.Json; 298 | using Newtonsoft.Json.Linq; 299 | 300 | using Sodium; 301 | 302 | namespace ExampleBot.Api 303 | { 304 | [Route("api/discordslash")] 305 | [ApiController] 306 | public class DiscordSlashCommandController : ControllerBase 307 | { 308 | private readonly ILogger _logger; 309 | 310 | public DiscordSlashCommandController(ILogger logger) 311 | { 312 | _logger = logger; 313 | } 314 | 315 | [HttpPost("")] 316 | //[ApiExplorerSettings(IgnoreApi = true)] 317 | public async Task DiscordEndpointHandler() 318 | { 319 | string raw; 320 | // Request validation 321 | try 322 | { 323 | // Get the verification headers from the request ... 324 | var signature = Request.Headers["X-Signature-Ed25519"].ToString(); 325 | var timestamp = Request.Headers["X-Signature-Timestamp"].ToString(); 326 | // ... convert the signature and public key to byte[] to use in verification ... 327 | var byteSig = Utils.HexStringToByteArray(signature); 328 | var byteKey = Utils.HexStringToByteArray(Startup.PublicKey); 329 | // ... read the body from the request ... 330 | using var reader = new StreamReader(Request.Body); 331 | if (reader.BaseStream.CanSeek) 332 | reader.BaseStream.Seek(0, SeekOrigin.Begin); 333 | raw = await reader.ReadToEndAsync(); 334 | // ... add the timestamp and convert it to a byte[] ... 335 | string body = timestamp + raw; 336 | var byteBody = Encoding.Default.GetBytes(body); 337 | // ... run through a verification with all the byte[]s ... 338 | bool validated = PublicKeyAuth.VerifyDetached(byteSig, byteBody, byteKey); 339 | // ... if it is not validated ... 340 | if(!validated) 341 | { // ... log a warning and return a 401 Unauthorized. 342 | _logger.LogWarning("Failed to validate POST request for Discord API."); 343 | return Unauthorized("Invalid Request Signature"); 344 | } 345 | else 346 | { // ... otherwise continue onwards. 347 | _logger.LogInformation("Received POST from Discord"); 348 | } 349 | } 350 | catch (Exception ex) 351 | { // ... if an error occurred, log the error and return at 401 Unauthorized. 352 | _logger.LogInformation(ex, "Decryption failed."); 353 | _logger.LogWarning("Failed to validate POST request for Discord API."); 354 | return Unauthorized("Invalid Request Signature"); 355 | } 356 | 357 | // Response parsing 358 | JObject json; 359 | try 360 | { // ... attempt to create a json object from the body ... 361 | json = JObject.Parse(raw); 362 | } 363 | catch 364 | { // ... if that fails, return a 400 Bad Request. 365 | return BadRequest(); 366 | } 367 | // ... check to see if this is a ping to the webhook ... 368 | if (json.ContainsKey("type") && (int)json["type"] == (int)InteractionType.Ping) 369 | { 370 | return Ok( 371 | JsonConvert.SerializeObject( 372 | new InteractionResponseBuilder() 373 | .WithType(InteractionResponseType.Pong) 374 | .Build() 375 | ) 376 | ); // ... and return the pong if it is. 377 | } 378 | else 379 | {// ... then pass the raw request body to the client ... 380 | var response = await Program.Slash.HandleWebhookPost(raw); 381 | if (response is not null) // ... if the clients response is not null ... 382 | return Ok(JsonConvert.SerializeObject(response)); // ... serialize it and send it. 383 | else return BadRequest("Failed to parse request JSON."); // ... or send a bad request message. 384 | } 385 | } 386 | } 387 | } 388 | ``` 389 | ## Telling Discord to send you Interactions over webhooks 390 | In order for everything to work, Discord needs to know to send you information over Webhook and not the Gateway. This means you need at least a development version of the bot running on the server you intend to release it to. 391 | 392 | Once the bot is running, and your API is ready to receive requests, head over to your discord developer portal and select your application. In the General Information tab, near the bottom there is an Interactions Endpoint URL field. Input your API endpoint there. For example, using the URL that was used earlier our endpoint would be: `https://slash.example.com/api/discordslash` 393 | 394 | Once you hit save, Discord is going to send a `POST` request to your URL (thus why it needs to be port-forwarded or on a server). This is where the Ping response comes in. Your app will recognize the Ping, respond with Pong, and Discord will save your endpoint. 395 | 396 | > **Congrats, you now have SlashCommands setup!**
397 | *Example code was from the [`ExampleHTTPBot`](https://github.com/Soyvolon/DSharpPlus.SlashCommands/tree/master/ExampleHTTPBot) project.* 398 | 399 | For more things to do with slash commands, see [Further Options](#further-options) 400 | 401 | # Further Options 402 | ## Creating Subcommands 403 | Due to how Discord has setup the commands for Slash Interactions, the setup used when creating Subcommands and Subcommand groups is a little wonky. 404 | 405 | See the [Discord Docs](https://discord.com/developers/docs/interactions/slash-commands#subcommands-and-subcommand-groups) for more information. 406 | 407 | Firstly, you need your command class: 408 | ```csharp 409 | [SlashCommand("sub", 1, 750486424469372970)] 410 | public class SubcommandExampleSlashCommand : BaseSlashCommandModule 411 | { 412 | // NOTE: THis way of DI will change at some point when I get around to making it actual DI 413 | public SubcommandExampleSlashCommand(IServiceProvider p) : base(p) { } 414 | } 415 | ``` 416 | Then, you need a child class of that command class: 417 | ```csharp 418 | [SlashSubcommandGroup("group")] 419 | public class SubcommandGroup : SubcommandExampleSlashCommand 420 | { 421 | public SubcommandGroup(IServiceProvider p) : base(p) { } 422 | 423 | // command methods go here. 424 | } 425 | ``` 426 | And finally, the command method: 427 | ```csharp 428 | [SlashSubcommand("command")] 429 | public async Task CommandAsync(InteractionContext ctx) 430 | { 431 | await ctx.ReplyAsync("This is a subcommand"); 432 | } 433 | ``` 434 | 435 | Once done, your file should look a bit like this: 436 | ```csharp 437 | using System; 438 | using System.Threading.Tasks; 439 | 440 | using DSharpPlus.SlashCommands.Attributes; 441 | using DSharpPlus.SlashCommands.Entities; 442 | 443 | namespace ExampleBot.Commands.Slash 444 | { 445 | [SlashCommand("sub", 1, 750486424469372970)] 446 | public class SubcommandExampleSlashCommand : BaseSlashCommandModule 447 | { 448 | // NOTE: This way of DI will change at some point when I get around to making it actual DI 449 | public SubcommandExampleSlashCommand(IServiceProvider p) : base(p) { } 450 | } 451 | 452 | [SlashSubcommandGroup("group")] 453 | public class SubcommandGroup : SubcommandExampleSlashCommand 454 | { 455 | public SubcommandGroup(IServiceProvider p) : base(p) { } 456 | 457 | [SlashSubcommand("command")] 458 | public async Task CommandAsync(InteractionContext ctx) 459 | { 460 | await ctx.ReplyAsync("This is a subcommand"); 461 | } 462 | } 463 | } 464 | ``` 465 | This creates a command named `sub` with a subcommand group called `group` and a subcommand named `command`. It is called from discord like so: `/sub group command` 466 | 467 | A few things to note: 468 | - If you have a subcommand, you can not have a default command. 469 | - There is a max of 10 subcommand groups per command 470 | - There is a max of 10 subcommands per subcommand group. 471 | 472 | ## Command Limits 473 | Some rules to keep in mind for adding parameters: 474 | 475 | *From the [Discord Docs](https://discord.com/developers/docs/interactions/slash-commands#a-quick-note-on-limits)* 476 | - An app can have up to 100 top-level global commands (50 commands with unique names) 477 | - An app can have up to an additional 100 guild commands per guild 478 | - An app can have up to 25 subcommand groups on a top-level command 479 | - An app can have up to 25 subcommands within a subcommand group 480 | - commands can have up to 25 options per command 481 | - `choices` can have up to 25 values per option 482 | - Limitations on [command names](https://discord.com/developers/docs/interactions/slash-commands#registering-a-command) 483 | - Limitations on [nesting subcommands and groups](https://discord.com/developers/docs/interactions/slash-commands#nested-subcommands-and-groups) 484 | ### Using Enums as Parameters for Commands 485 | You can use an `enum` as a parameter for a command. However, due to the limits Discord sets, you can only have up to ten values for your `enum`. 486 | 487 | > An enum is automatically assigned the `choices` type in the Discord Slash command. 488 | 489 | To set a description for your `enum` value, use the `System.ComponentModel.DescriptionAttribute` over the `DSharpPlus` version, as the `DSharpPlus` description attribute does not work on `enum` values. 490 | --------------------------------------------------------------------------------