├── .gitattributes ├── .gitignore ├── ApplicationCommandModule.cs ├── Attributes ├── ChannelTypesAttribute.cs ├── ChoiceAttribute.cs ├── ChoiceNameAttribute.cs ├── ChoiceProviderAttribute.cs ├── ContextMenuAttribute.cs ├── DontInjectAttribute.cs ├── OptionAttribute.cs ├── SlashCommandAttribute.cs ├── SlashCommandGroupAttribute.cs └── SlashModuleLifespanAttribute.cs ├── Built-in Checks ├── SlashRequireBotPermissionsAttribute.cs ├── SlashRequireDirectMessageAttribute.cs ├── SlashRequireGuildAttribute.cs ├── SlashRequireOwnerAttribute.cs ├── SlashRequirePermissionsAttribute.cs └── SlashRequireUserPermissionsAttribute.cs ├── ChoiceProvider.cs ├── ContextMenuCheckBaseAttribute.cs ├── ContextMenuExecutionChecksFailedException.cs ├── Contexts ├── BaseContext.cs ├── ContextMenuContext.cs └── InteractionContext.cs ├── DSharpPlus.SlashCommands.csproj ├── DSharpPlus.SlashCommands.sln ├── EventArgs ├── ContextMenuErrorEventArgs.cs ├── ContextMenuExecutedEventArgs.cs ├── SlashCommandErrorEventArgs.cs └── SlashCommandExecutedEventArgs.cs ├── ExtensionMethods.cs ├── IChoiceProvider.cs ├── LICENSE ├── README.md ├── SlashCheckBaseAttribute.cs ├── SlashCommandConfiguration.cs ├── SlashCommandsExtension.cs └── SlashExecutionChecksFailedException.cs /.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 | -------------------------------------------------------------------------------- /.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 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Oo]ut/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # ASP.NET Scaffolding 67 | ScaffoldingReadMe.txt 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio LightSwitch build output 298 | **/*.HTMLClient/GeneratedArtifacts 299 | **/*.DesktopClient/GeneratedArtifacts 300 | **/*.DesktopClient/ModelManifest.xml 301 | **/*.Server/GeneratedArtifacts 302 | **/*.Server/ModelManifest.xml 303 | _Pvt_Extensions 304 | 305 | # Paket dependency manager 306 | .paket/paket.exe 307 | paket-files/ 308 | 309 | # FAKE - F# Make 310 | .fake/ 311 | 312 | # CodeRush personal settings 313 | .cr/personal 314 | 315 | # Python Tools for Visual Studio (PTVS) 316 | __pycache__/ 317 | *.pyc 318 | 319 | # Cake - Uncomment if you are using it 320 | # tools/** 321 | # !tools/packages.config 322 | 323 | # Tabs Studio 324 | *.tss 325 | 326 | # Telerik's JustMock configuration file 327 | *.jmconfig 328 | 329 | # BizTalk build output 330 | *.btp.cs 331 | *.btm.cs 332 | *.odx.cs 333 | *.xsd.cs 334 | 335 | # OpenCover UI analysis results 336 | OpenCover/ 337 | 338 | # Azure Stream Analytics local run output 339 | ASALocalRun/ 340 | 341 | # MSBuild Binary and Structured Log 342 | *.binlog 343 | 344 | # NVidia Nsight GPU debugger configuration file 345 | *.nvuser 346 | 347 | # MFractors (Xamarin productivity tool) working folder 348 | .mfractor/ 349 | 350 | # Local History for Visual Studio 351 | .localhistory/ 352 | 353 | # BeatPulse healthcheck temp database 354 | healthchecksdb 355 | 356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 357 | MigrationBackup/ 358 | 359 | # Ionide (cross platform F# VS Code tools) working folder 360 | .ionide/ 361 | 362 | # Fody - auto-generated XML schema 363 | FodyWeavers.xsd -------------------------------------------------------------------------------- /ApplicationCommandModule.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace DSharpPlus.SlashCommands 4 | { 5 | /// 6 | /// Represents a base class for slash command modules. 7 | /// 8 | public abstract class ApplicationCommandModule 9 | { 10 | /// 11 | /// Called before the execution of a slash command in the module. 12 | /// 13 | /// The context. 14 | /// Whether or not to execute the slash command. 15 | public virtual Task BeforeSlashExecutionAsync(InteractionContext ctx) 16 | => Task.FromResult(true); 17 | 18 | /// 19 | /// Called after the execution of a slash command in the module. 20 | /// 21 | /// The context. 22 | /// 23 | public virtual Task AfterSlashExecutionAsync(InteractionContext ctx) 24 | => Task.CompletedTask; 25 | 26 | /// 27 | /// Called before the execution of a context menu in the module. 28 | /// 29 | /// The context. 30 | /// Whether or not to execute the slash command. 31 | public virtual Task BeforeContextMenuExecutionAsync(ContextMenuContext ctx) 32 | => Task.FromResult(true); 33 | 34 | /// 35 | /// Called after the execution of a context menu in the module. 36 | /// 37 | /// The context. 38 | /// 39 | public virtual Task AfterContextMenuExecutionAsync(ContextMenuContext ctx) 40 | => Task.CompletedTask; 41 | 42 | } 43 | } -------------------------------------------------------------------------------- /Attributes/ChannelTypesAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace DSharpPlus.SlashCommands 5 | { 6 | /// 7 | /// Defines allowed channel types for a channel parameter. 8 | /// 9 | [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] 10 | public class ChannelTypesAttribute : Attribute 11 | { 12 | /// 13 | /// Allowed channel types. 14 | /// 15 | public IEnumerable ChannelTypes { get; } 16 | 17 | /// 18 | /// Defines allowed channel types for a channel parameter. 19 | /// 20 | /// The channel types to allow. 21 | public ChannelTypesAttribute(params ChannelType[] channelTypes) 22 | { 23 | this.ChannelTypes = channelTypes; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Attributes/ChoiceAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DSharpPlus.SlashCommands 4 | { 5 | /// 6 | /// Adds a choice for this slash command option. 7 | /// 8 | [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true)] 9 | public sealed class ChoiceAttribute : Attribute 10 | { 11 | /// 12 | /// Gets the name of the choice. 13 | /// 14 | public string Name { get; } 15 | 16 | /// 17 | /// Gets the value of the choice. 18 | /// 19 | public object Value { get; } 20 | 21 | /// 22 | /// Adds a choice to the slash command option. 23 | /// 24 | /// The name of the choice. 25 | /// The value of the choice. 26 | public ChoiceAttribute(string name, string value) 27 | { 28 | Name = name; 29 | Value = value; 30 | } 31 | 32 | /// 33 | /// Adds a choice to the slash command option. 34 | /// 35 | /// The name of the choice. 36 | /// The value of the choice. 37 | public ChoiceAttribute(string name, long value) 38 | { 39 | Name = name; 40 | Value = value; 41 | } 42 | 43 | /// 44 | /// Adds a choice to the slash command option. 45 | /// 46 | /// The name of the choice. 47 | /// The value of the choice. 48 | public ChoiceAttribute(string name, double value) 49 | { 50 | Name = name; 51 | Value = value; 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /Attributes/ChoiceNameAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DSharpPlus.SlashCommands 4 | { 5 | /// 6 | /// Sets the name for this enum choice. 7 | /// 8 | [AttributeUsage(AttributeTargets.All, AllowMultiple = false)] 9 | public sealed class ChoiceNameAttribute : Attribute 10 | { 11 | /// 12 | /// The name. 13 | /// 14 | public string Name { get; } 15 | 16 | /// 17 | /// Sets the name for this enum choice. 18 | /// 19 | /// The name for this enum choice. 20 | public ChoiceNameAttribute(string name) 21 | { 22 | Name = name; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Attributes/ChoiceProviderAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DSharpPlus.SlashCommands 4 | { 5 | /// 6 | /// Sets a IChoiceProvider for a command options. ChoiceProviders can be used to provide 7 | /// DiscordApplicationCommandOptionChoice from external sources such as a database. 8 | /// 9 | [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = true)] 10 | public sealed class ChoiceProviderAttribute : Attribute 11 | { 12 | /// 13 | /// The type of the provider. 14 | /// 15 | public Type ProviderType { get; } 16 | 17 | /// 18 | /// Adds a choice provider to this command. 19 | /// 20 | /// The type of the provider. 21 | public ChoiceProviderAttribute(Type providerType) 22 | { 23 | ProviderType = providerType; 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /Attributes/ContextMenuAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DSharpPlus.SlashCommands 4 | { 5 | /// 6 | /// Marks this method as a context menu. 7 | /// 8 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] 9 | public sealed class ContextMenuAttribute : Attribute 10 | { 11 | /// 12 | /// Gets the name of this context menu. 13 | /// 14 | public string Name { get; internal set; } 15 | 16 | /// 17 | /// Gets the type of this context menu. 18 | /// 19 | public ApplicationCommandType Type { get; internal set; } 20 | 21 | /// 22 | /// Gets whether this command is enabled by default. 23 | /// 24 | public bool DefaultPermission { get; internal set;} 25 | 26 | /// 27 | /// Marks this method as a context menu. 28 | /// 29 | /// The type of the context menu. 30 | /// The name of the context menu. 31 | /// Sets whether the command should be enabled by default. 32 | public ContextMenuAttribute(ApplicationCommandType type, string name, bool defaultPermission = true) 33 | { 34 | if (type == ApplicationCommandType.SlashCommand) 35 | throw new ArgumentException("Context menus cannot be of type SlashCommand."); 36 | 37 | this.Type = type; 38 | this.Name = name; 39 | this.DefaultPermission = defaultPermission; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Attributes/DontInjectAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DSharpPlus.SlashCommands 4 | { 5 | /// 6 | /// Prevents this field or property from having its value injected by dependency injection. 7 | /// 8 | [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = true)] 9 | public sealed class DontInjectAttribute : Attribute 10 | { } 11 | } -------------------------------------------------------------------------------- /Attributes/OptionAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DSharpPlus.SlashCommands 4 | { 5 | /// 6 | /// Marks this parameter as an option for a slash command. 7 | /// 8 | [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] 9 | public sealed class OptionAttribute : Attribute 10 | { 11 | /// 12 | /// Gets the name of this option. 13 | /// 14 | public string Name { get; } 15 | 16 | /// 17 | /// Gets the description of this option. 18 | /// 19 | public string Description { get; } 20 | 21 | /// 22 | /// Marks this parameter as an option for a slash command. 23 | /// 24 | /// The name of the option. 25 | /// The description of the option. 26 | public OptionAttribute(string name, string description) 27 | { 28 | if(name.Length > 32) 29 | throw new ArgumentException("Slash command option names cannot go over 32 characters."); 30 | if (description.Length > 100) 31 | throw new ArgumentException("Slash command option descriptions cannot go over 100 characters."); 32 | Name = name.ToLower(); 33 | Description = description; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Attributes/SlashCommandAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DSharpPlus.SlashCommands 4 | { 5 | /// 6 | /// Marks this method as a slash command. 7 | /// 8 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] 9 | public sealed class SlashCommandAttribute : Attribute 10 | { 11 | /// 12 | /// Gets the name of this command. 13 | /// 14 | public string Name { get; } 15 | 16 | /// 17 | /// Gets the description of this command. 18 | /// 19 | public string Description { get; } 20 | 21 | /// 22 | /// Gets whether this command is enabled by default. 23 | /// 24 | public bool DefaultPermission { get; } 25 | 26 | /// 27 | /// Marks this method as a slash command. 28 | /// 29 | /// Sets the name of this slash command. 30 | /// Sets the description of this slash command. 31 | /// Sets whether the command should be enabled by default. 32 | public SlashCommandAttribute(string name, string description, bool defaultPermission = true) 33 | { 34 | Name = name.ToLower(); 35 | Description = description; 36 | DefaultPermission = defaultPermission; 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /Attributes/SlashCommandGroupAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DSharpPlus.SlashCommands 4 | { 5 | /// 6 | /// Marks this class a slash command group. 7 | /// 8 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] 9 | public sealed class SlashCommandGroupAttribute : Attribute 10 | { 11 | /// 12 | /// Gets the name of this slash command group. 13 | /// 14 | public string Name { get; } 15 | 16 | /// 17 | /// Gets the description of this slash command group. 18 | /// 19 | public string Description { get; } 20 | 21 | /// 22 | /// Gets whether this command is enabled on default. 23 | /// 24 | public bool DefaultPermission { get; } 25 | 26 | /// 27 | /// Marks this class as a slash command group. 28 | /// 29 | /// Sets the name of this command group. 30 | /// Sets the description of this command group. 31 | /// Sets whether this command group is enabled on default. 32 | public SlashCommandGroupAttribute(string name, string description, bool defaultPermission = true) 33 | { 34 | Name = name.ToLower(); 35 | Description = description; 36 | DefaultPermission = defaultPermission; 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /Attributes/SlashModuleLifespanAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DSharpPlus.SlashCommands 4 | { 5 | /// 6 | /// Defines this slash command module's lifespan. Module lifespans are transient by default. 7 | /// 8 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] 9 | public sealed class SlashModuleLifespanAttribute : Attribute 10 | { 11 | /// 12 | /// Gets the lifespan. 13 | /// 14 | public SlashModuleLifespan Lifespan { get; } 15 | 16 | /// 17 | /// Defines this slash command module's lifespan. 18 | /// 19 | /// The lifespan of the module. Module lifespans are transient by default. 20 | public SlashModuleLifespanAttribute(SlashModuleLifespan lifespan) 21 | { 22 | this.Lifespan = lifespan; 23 | } 24 | } 25 | 26 | /// 27 | /// Represents a slash command module lifespan. 28 | /// 29 | public enum SlashModuleLifespan 30 | { 31 | /// 32 | /// Whether this module should be initiated every time a command is run, with dependencies injected from a scope. 33 | /// 34 | Scoped, 35 | 36 | /// 37 | /// Whether this module should be initiated every time a command is run. 38 | /// 39 | Transient, 40 | 41 | /// 42 | /// Whether this module should be initiated at startup. 43 | /// 44 | Singleton 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Built-in Checks/SlashRequireBotPermissionsAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace DSharpPlus.SlashCommands.Attributes 5 | { 6 | /// 7 | /// Defines that usage of this slash command is only possible when the bot is granted a specific permission. 8 | /// 9 | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = false)] 10 | public sealed class SlashRequireBotPermissionsAttribute : SlashCheckBaseAttribute 11 | { 12 | /// 13 | /// Gets the permissions required by this attribute. 14 | /// 15 | public Permissions Permissions { get; } 16 | 17 | /// 18 | /// Gets or sets this check's behaviour in DMs. True means the check will always pass in DMs, whereas false means that it will always fail. 19 | /// 20 | public bool IgnoreDms { get; } = true; 21 | 22 | /// 23 | /// Defines that usage of this slash command is only possible when the bot is granted a specific permission. 24 | /// 25 | /// Permissions required to execute this command. 26 | /// Sets this check's behaviour in DMs. True means the check will always pass in DMs, whereas false means that it will always fail. 27 | public SlashRequireBotPermissionsAttribute(Permissions permissions, bool ignoreDms = true) 28 | { 29 | this.Permissions = permissions; 30 | this.IgnoreDms = ignoreDms; 31 | } 32 | 33 | /// 34 | /// Runs checks. 35 | /// 36 | public override async Task ExecuteChecksAsync(InteractionContext ctx) 37 | { 38 | if (ctx.Guild == null) 39 | return this.IgnoreDms; 40 | 41 | var bot = await ctx.Guild.GetMemberAsync(ctx.Client.CurrentUser.Id).ConfigureAwait(false); 42 | if (bot == null) 43 | return false; 44 | 45 | if (bot.Id == ctx.Guild.OwnerId) 46 | return true; 47 | 48 | var pbot = ctx.Channel.PermissionsFor(bot); 49 | 50 | if ((pbot & Permissions.Administrator) != 0) 51 | return true; 52 | 53 | return (pbot & this.Permissions) == this.Permissions; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Built-in Checks/SlashRequireDirectMessageAttribute.cs: -------------------------------------------------------------------------------- 1 | using DSharpPlus.Entities; 2 | using System; 3 | using System.Threading.Tasks; 4 | 5 | namespace DSharpPlus.SlashCommands.Attributes 6 | { 7 | /// 8 | /// Defines that this slash command is only usable within a direct message channel. 9 | /// 10 | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = false)] 11 | public sealed class SlashRequireDirectMessageAttribute : SlashCheckBaseAttribute 12 | { 13 | /// 14 | /// Defines that this command is only usable within a direct message channel. 15 | /// 16 | public SlashRequireDirectMessageAttribute() 17 | { } 18 | 19 | /// 20 | /// Runs checks. 21 | /// 22 | public override Task ExecuteChecksAsync(InteractionContext ctx) 23 | => Task.FromResult(ctx.Channel is DiscordDmChannel); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Built-in Checks/SlashRequireGuildAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace DSharpPlus.SlashCommands.Attributes 5 | { 6 | /// 7 | /// Defines that this slash command is only usable within a guild. 8 | /// 9 | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = false)] 10 | public sealed class SlashRequireGuildAttribute : SlashCheckBaseAttribute 11 | { 12 | /// 13 | /// Defines that this command is only usable within a guild. 14 | /// 15 | public SlashRequireGuildAttribute() 16 | { } 17 | 18 | /// 19 | /// Runs checks. 20 | /// 21 | public override Task ExecuteChecksAsync(InteractionContext ctx) 22 | => Task.FromResult(ctx.Guild != null); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Built-in Checks/SlashRequireOwnerAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | 5 | namespace DSharpPlus.SlashCommands.Attributes 6 | { 7 | /// 8 | /// Defines that this slash command is restricted to the owner of the bot. 9 | /// 10 | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = false)] 11 | public sealed class SlashRequireOwnerAttribute : SlashCheckBaseAttribute 12 | { 13 | /// 14 | /// Defines that this slash command is restricted to the owner of the bot. 15 | /// 16 | public SlashRequireOwnerAttribute() 17 | { } 18 | 19 | /// 20 | /// Runs checks. 21 | /// 22 | public override Task ExecuteChecksAsync(InteractionContext ctx) 23 | { 24 | var app = ctx.Client.CurrentApplication; 25 | var me = ctx.Client.CurrentUser; 26 | 27 | return app != null ? Task.FromResult(app.Owners.Any(x => x.Id == ctx.User.Id)) : Task.FromResult(ctx.User.Id == me.Id); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Built-in Checks/SlashRequirePermissionsAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace DSharpPlus.SlashCommands.Attributes 5 | { 6 | /// 7 | /// Defines that usage of this slash command is restricted to members with specified permissions. This check also verifies that the bot has the same permissions. 8 | /// 9 | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = false)] 10 | public sealed class SlashRequirePermissionsAttribute : SlashCheckBaseAttribute 11 | { 12 | /// 13 | /// Gets the permissions required by this attribute. 14 | /// 15 | public Permissions Permissions { get; } 16 | 17 | /// 18 | /// Gets or sets this check's behaviour in DMs. True means the check will always pass in DMs, whereas false means that it will always fail. 19 | /// 20 | public bool IgnoreDms { get; } = true; 21 | 22 | /// 23 | /// Defines that usage of this command is restricted to members with specified permissions. This check also verifies that the bot has the same permissions. 24 | /// 25 | /// Permissions required to execute this command. 26 | /// Sets this check's behaviour in DMs. True means the check will always pass in DMs, whereas false means that it will always fail. 27 | public SlashRequirePermissionsAttribute(Permissions permissions, bool ignoreDms = true) 28 | { 29 | this.Permissions = permissions; 30 | this.IgnoreDms = ignoreDms; 31 | } 32 | 33 | /// 34 | /// Runs checks. 35 | /// 36 | public override async Task ExecuteChecksAsync(InteractionContext ctx) 37 | { 38 | if (ctx.Guild == null) 39 | return this.IgnoreDms; 40 | 41 | var usr = ctx.Member; 42 | if (usr == null) 43 | return false; 44 | var pusr = ctx.Channel.PermissionsFor(usr); 45 | 46 | var bot = await ctx.Guild.GetMemberAsync(ctx.Client.CurrentUser.Id).ConfigureAwait(false); 47 | if (bot == null) 48 | return false; 49 | var pbot = ctx.Channel.PermissionsFor(bot); 50 | 51 | var usrok = ctx.Guild.OwnerId == usr.Id; 52 | var botok = ctx.Guild.OwnerId == bot.Id; 53 | 54 | if (!usrok) 55 | usrok = (pusr & Permissions.Administrator) != 0 || (pusr & this.Permissions) == this.Permissions; 56 | 57 | if (!botok) 58 | botok = (pbot & Permissions.Administrator) != 0 || (pbot & this.Permissions) == this.Permissions; 59 | 60 | return usrok && botok; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Built-in Checks/SlashRequireUserPermissionsAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace DSharpPlus.SlashCommands.Attributes 5 | { 6 | /// 7 | /// Defines that usage of this command is restricted to members with specified permissions. 8 | /// 9 | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = false)] 10 | public sealed class SlashRequireUserPermissionsAttribute : SlashCheckBaseAttribute 11 | { 12 | /// 13 | /// Gets the permissions required by this attribute. 14 | /// 15 | public Permissions Permissions { get; } 16 | 17 | /// 18 | /// Gets or sets this check's behaviour in DMs. True means the check will always pass in DMs, whereas false means that it will always fail. 19 | /// 20 | public bool IgnoreDms { get; } = true; 21 | 22 | /// 23 | /// Defines that usage of this command is restricted to members with specified permissions. 24 | /// 25 | /// Permissions required to execute this command. 26 | /// Sets this check's behaviour in DMs. True means the check will always pass in DMs, whereas false means that it will always fail. 27 | public SlashRequireUserPermissionsAttribute(Permissions permissions, bool ignoreDms = true) 28 | { 29 | this.Permissions = permissions; 30 | this.IgnoreDms = ignoreDms; 31 | } 32 | 33 | /// 34 | /// Runs checks. 35 | /// 36 | public override Task ExecuteChecksAsync(InteractionContext ctx) 37 | { 38 | if (ctx.Guild == null) 39 | return Task.FromResult(this.IgnoreDms); 40 | 41 | var usr = ctx.Member; 42 | if (usr == null) 43 | return Task.FromResult(false); 44 | 45 | if (usr.Id == ctx.Guild.OwnerId) 46 | return Task.FromResult(true); 47 | 48 | var pusr = ctx.Channel.PermissionsFor(usr); 49 | 50 | if ((pusr & Permissions.Administrator) != 0) 51 | return Task.FromResult(true); 52 | 53 | return (pusr & this.Permissions) == this.Permissions ? Task.FromResult(true) : Task.FromResult(false); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /ChoiceProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using DSharpPlus.Entities; 5 | 6 | namespace DSharpPlus.SlashCommands 7 | { 8 | /// 9 | /// Implementation of with access to service collection. 10 | /// 11 | public abstract class ChoiceProvider : IChoiceProvider 12 | { 13 | /// 14 | /// Sets the choices for the slash command. 15 | /// 16 | public abstract Task> Provider(); 17 | 18 | /// 19 | /// Sets the service provider. 20 | /// 21 | public IServiceProvider Services { get; set; } 22 | 23 | /// 24 | /// The optional ID of the Guild the command got registered for. 25 | /// 26 | public ulong? GuildId { get; set; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ContextMenuCheckBaseAttribute.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using System; 3 | 4 | namespace DSharpPlus.SlashCommands 5 | { 6 | /// 7 | /// The base class for a pre-execution check for a context menu. 8 | /// 9 | public abstract class ContextMenuCheckBaseAttribute : Attribute 10 | { 11 | /// 12 | /// Checks whether this command can be executed within the current context. 13 | /// 14 | /// The context. 15 | /// Whether the checks passed. 16 | public abstract Task ExecuteChecksAsync(ContextMenuContext ctx); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ContextMenuExecutionChecksFailedException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace DSharpPlus.SlashCommands 5 | { 6 | /// 7 | /// Thrown when a pre-execution check for a slash command fails. 8 | /// 9 | public sealed class ContextMenuExecutionChecksFailedException : Exception 10 | { 11 | /// 12 | /// The list of failed checks. 13 | /// 14 | public IReadOnlyList FailedChecks; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Contexts/BaseContext.cs: -------------------------------------------------------------------------------- 1 | using DSharpPlus.Entities; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Threading.Tasks; 6 | 7 | namespace DSharpPlus.SlashCommands 8 | { 9 | /// 10 | /// Respresents a base context for application command contexts. 11 | /// 12 | public class BaseContext 13 | { 14 | /// 15 | /// Gets the interaction that was created. 16 | /// 17 | public DiscordInteraction Interaction { get; internal set; } 18 | 19 | /// 20 | /// Gets the client for this interaction. 21 | /// 22 | public DiscordClient Client { get; internal set; } 23 | 24 | /// 25 | /// Gets the guild this interaction was executed in. 26 | /// 27 | public DiscordGuild Guild { get; internal set; } 28 | 29 | /// 30 | /// Gets the channel this interaction was executed in. 31 | /// 32 | public DiscordChannel Channel { get; internal set; } 33 | 34 | /// 35 | /// Gets the user which executed this interaction. 36 | /// 37 | public DiscordUser User { get; internal set; } 38 | 39 | /// 40 | /// Gets the member which executed this interaction, or null if the command is in a DM. 41 | /// 42 | public DiscordMember Member 43 | => this.User is DiscordMember member ? member : null; 44 | 45 | /// 46 | /// Gets the slash command module this interaction was created in. 47 | /// 48 | public SlashCommandsExtension SlashCommandsExtension { get; internal set; } 49 | 50 | /// 51 | /// Gets the token for this interaction. 52 | /// 53 | public string Token { get; internal set; } 54 | 55 | /// 56 | /// Gets the id for this interaction. 57 | /// 58 | public ulong InteractionId { get; internal set; } 59 | 60 | /// 61 | /// Gets the name of the command. 62 | /// 63 | public string CommandName { get; internal set; } 64 | 65 | /// 66 | /// Gets the type of this interaction. 67 | /// 68 | public ApplicationCommandType Type { get; internal set;} 69 | 70 | /// 71 | /// Gets the service provider. 72 | /// This allows passing data around without resorting to static members. 73 | /// Defaults to null. 74 | /// 75 | public IServiceProvider Services { get; internal set; } = new ServiceCollection().BuildServiceProvider(true); 76 | 77 | /// 78 | /// Creates a response to this interaction. 79 | /// You must create a response within 3 seconds of this interaction being executed; if the command has the potential to take more than 3 seconds, create a at the start, and edit the response later. 80 | /// 81 | /// The type of the response. 82 | /// The data to be sent, if any. 83 | /// 84 | public Task CreateResponseAsync(InteractionResponseType type, DiscordInteractionResponseBuilder builder = null) 85 | => this.Interaction.CreateResponseAsync(type, builder); 86 | 87 | /// 88 | /// Edits the interaction response. 89 | /// 90 | /// The data to edit the response with. 91 | /// Attached files to keep. 92 | /// 93 | public Task EditResponseAsync(DiscordWebhookBuilder builder, IEnumerable attachments = default) 94 | => this.Interaction.EditOriginalResponseAsync(builder, attachments); 95 | 96 | /// 97 | /// Deletes the interaction response. 98 | /// 99 | /// 100 | public Task DeleteResponseAsync() 101 | => this.Interaction.DeleteOriginalResponseAsync(); 102 | 103 | /// 104 | /// Creates a follow up message to the interaction. 105 | /// 106 | /// The message to be sent, in the form of a webhook. 107 | /// The created message. 108 | public Task FollowUpAsync(DiscordFollowupMessageBuilder builder) 109 | => this.Interaction.CreateFollowupMessageAsync(builder); 110 | 111 | /// 112 | /// Edits a followup message. 113 | /// 114 | /// The id of the followup message to edit. 115 | /// The webhook builder. 116 | /// Attached files to keep. 117 | /// 118 | public Task EditFollowupAsync(ulong followupMessageId, DiscordWebhookBuilder builder, IEnumerable attachments = default) 119 | => this.Interaction.EditFollowupMessageAsync(followupMessageId, builder, attachments); 120 | 121 | /// 122 | /// Deletes a followup message. 123 | /// 124 | /// The id of the followup message to delete. 125 | /// 126 | public Task DeleteFollowupAsync(ulong followupMessageId) 127 | => this.Interaction.DeleteFollowupMessageAsync(followupMessageId); 128 | 129 | /// 130 | /// Gets the original interaction response. 131 | /// 132 | /// The original interaction response. 133 | public Task GetOriginalResponseAsync() 134 | => this.Interaction.GetOriginalResponseAsync(); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /Contexts/ContextMenuContext.cs: -------------------------------------------------------------------------------- 1 | using DSharpPlus.Entities; 2 | 3 | namespace DSharpPlus.SlashCommands 4 | { 5 | /// 6 | /// Respresents a context for a context menu. 7 | /// 8 | public sealed class ContextMenuContext : BaseContext 9 | { 10 | /// 11 | /// The user this command targets, if applicable. 12 | /// 13 | public DiscordUser TargetUser { get; internal set; } 14 | 15 | /// 16 | /// The member this command targets, if applicable. 17 | /// 18 | public DiscordMember TargetMember 19 | => this.TargetUser is DiscordMember member ? member : null; 20 | 21 | /// 22 | /// The message this command targets, if applicable. 23 | /// 24 | public DiscordMessage TargetMessage { get; internal set; } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Contexts/InteractionContext.cs: -------------------------------------------------------------------------------- 1 | using DSharpPlus.Entities; 2 | using System.Collections.Generic; 3 | 4 | namespace DSharpPlus.SlashCommands 5 | { 6 | /// 7 | /// Represents a context for an interaction. 8 | /// 9 | public sealed class InteractionContext : BaseContext 10 | { 11 | /// 12 | /// Gets the users mentioned in the command parameters. 13 | /// 14 | public IReadOnlyList ResolvedUserMentions { get; internal set; } 15 | 16 | /// 17 | /// Gets the roles mentioned in the command parameters. 18 | /// 19 | public IReadOnlyList ResolvedRoleMentions { get; internal set; } 20 | 21 | /// 22 | /// Gets the channels mentioned in the command parameters. 23 | /// 24 | public IReadOnlyList ResolvedChannelMentions { get; internal set; } 25 | } 26 | } -------------------------------------------------------------------------------- /DSharpPlus.SlashCommands.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | latest 6 | IDoEverything, Epictek, SakuraIsayeki, sssvt-drabek-stepan, tygore587, VelvetThePanda, OoLunar 7 | An extension for DSharpPlus to make slash commands easy 8 | 9 | https://github.com/IDoEverything/DSharpPlus.SlashCommands 10 | true 11 | https://github.com/IDoEverything/DSharpPlus.SlashCommands 12 | IDoEverything.DSharpPlus.SlashCommands 13 | 2.0.4 14 | IDoEverything 15 | 16 | 17 | 18 | 3 19 | obj\Debug\netstandard2.0\DSharpPlus.SlashCommands.xml 20 | 21 | 22 | 23 | 5 24 | obj\Debug\netstandard2.0\DSharpPlus.SlashCommands.xml 25 | 1701;1702;NU5104 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /DSharpPlus.SlashCommands.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31129.286 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DSharpPlus.SlashCommands", "DSharpPlus.SlashCommands.csproj", "{C4C9E7CA-1958-47A6-893D-B85FD75C0D59}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {C4C9E7CA-1958-47A6-893D-B85FD75C0D59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {C4C9E7CA-1958-47A6-893D-B85FD75C0D59}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {C4C9E7CA-1958-47A6-893D-B85FD75C0D59}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {C4C9E7CA-1958-47A6-893D-B85FD75C0D59}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {FB0312E0-4A1D-4EC9-BB50-02A59A95925B} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /EventArgs/ContextMenuErrorEventArgs.cs: -------------------------------------------------------------------------------- 1 | using Emzi0767.Utilities; 2 | using System; 3 | 4 | namespace DSharpPlus.SlashCommands.EventArgs 5 | { 6 | /// 7 | /// Represents arguments for a 8 | /// 9 | public class ContextMenuErrorEventArgs : AsyncEventArgs 10 | { 11 | /// 12 | /// The context of the command. 13 | /// 14 | public ContextMenuContext Context { get; internal set; } 15 | 16 | /// 17 | /// The exception thrown. 18 | /// 19 | public Exception Exception { get; internal set; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /EventArgs/ContextMenuExecutedEventArgs.cs: -------------------------------------------------------------------------------- 1 | using Emzi0767.Utilities; 2 | 3 | namespace DSharpPlus.SlashCommands.EventArgs 4 | { 5 | /// 6 | /// Represents arguments for a 7 | /// 8 | public sealed class ContextMenuExecutedEventArgs : AsyncEventArgs 9 | { 10 | /// 11 | /// The context of the command. 12 | /// 13 | public ContextMenuContext Context { get; internal set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /EventArgs/SlashCommandErrorEventArgs.cs: -------------------------------------------------------------------------------- 1 | using Emzi0767.Utilities; 2 | using System; 3 | 4 | namespace DSharpPlus.SlashCommands.EventArgs 5 | { 6 | /// 7 | /// Represents arguments for a event. 8 | /// 9 | public sealed class SlashCommandErrorEventArgs : AsyncEventArgs 10 | { 11 | /// 12 | /// The context of the command. 13 | /// 14 | public InteractionContext Context { get; internal set; } 15 | 16 | /// 17 | /// The exception thrown. 18 | /// 19 | public Exception Exception { get; internal set; } 20 | } 21 | } -------------------------------------------------------------------------------- /EventArgs/SlashCommandExecutedEventArgs.cs: -------------------------------------------------------------------------------- 1 | using Emzi0767.Utilities; 2 | 3 | namespace DSharpPlus.SlashCommands.EventArgs 4 | { 5 | /// 6 | /// Represents the arguments for a event. 7 | /// 8 | public sealed class SlashCommandExecutedEventArgs : AsyncEventArgs 9 | { 10 | /// 11 | /// The context of the command. 12 | /// 13 | public InteractionContext Context { get; internal set; } 14 | } 15 | } -------------------------------------------------------------------------------- /ExtensionMethods.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using System.Reflection; 5 | using System.Globalization; 6 | using System.Linq; 7 | 8 | namespace DSharpPlus.SlashCommands 9 | { 10 | /// 11 | /// Defines various extension methods for slash commands. 12 | /// 13 | public static class ExtensionMethods 14 | { 15 | /// 16 | /// Enables slash commands on this . 17 | /// 18 | /// Client to enable slash commands for. 19 | /// Configuration to use. 20 | /// Created . 21 | public static SlashCommandsExtension UseSlashCommands(this DiscordClient client, 22 | SlashCommandsConfiguration config = null) 23 | { 24 | if (client.GetExtension() != null) 25 | throw new InvalidOperationException("Slash commands are already enabled for that client."); 26 | 27 | var scomm = new SlashCommandsExtension(config); 28 | client.AddExtension(scomm); 29 | return scomm; 30 | } 31 | 32 | /// 33 | /// Gets the slash commands module for this client. 34 | /// 35 | /// Client to get slash commands for. 36 | /// The module, or null if not activated. 37 | public static SlashCommandsExtension GetSlashCommands(this DiscordClient client) 38 | => client.GetExtension(); 39 | 40 | /// 41 | /// Enables slash commands on this . 42 | /// 43 | /// Client to enable slash commands on. 44 | /// Configuration to use. 45 | /// A dictionary of created with the key being the shard id. 46 | public static async Task> UseSlashCommandsAsync(this DiscordShardedClient client, SlashCommandsConfiguration config = null) 47 | { 48 | var modules = new Dictionary(); 49 | await (Task)client.GetType().GetMethod("InitializeShardsAsync", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(client, null); 50 | foreach(var shard in client.ShardClients.Values) 51 | { 52 | var scomm = shard.GetSlashCommands(); 53 | if (scomm == null) 54 | scomm = shard.UseSlashCommands(config); 55 | 56 | modules[shard.ShardId] = scomm; 57 | } 58 | 59 | return modules; 60 | } 61 | 62 | /// 63 | /// Registers a commands class. 64 | /// 65 | /// The command class to register. 66 | /// The modules to register it on. 67 | /// The guild id to register it on. If you want global commands, leave it null. 68 | public static void RegisterCommands(this IReadOnlyDictionary modules, ulong? guildId = null) where T : ApplicationCommandModule 69 | { 70 | foreach (var module in modules.Values) 71 | module.RegisterCommands(guildId); 72 | } 73 | 74 | /// 75 | /// Registers a command class. 76 | /// 77 | /// The modules to register it on. 78 | /// The of the command class to register. 79 | /// The guild id to register it on. If you want global commands, leave it null. 80 | public static void RegisterCommands(this IReadOnlyDictionary modules, Type type, ulong? guildId = null) 81 | { 82 | foreach (var module in modules.Values) 83 | module.RegisterCommands(type, guildId); 84 | } 85 | 86 | /// 87 | /// Gets the name from the for this enum value. 88 | /// 89 | /// The name. 90 | public static string GetName(this T e) where T : IConvertible 91 | { 92 | if (e is Enum) 93 | { 94 | Type type = e.GetType(); 95 | Array values = Enum.GetValues(type); 96 | 97 | foreach (int val in values) 98 | { 99 | if (val == e.ToInt32(CultureInfo.InvariantCulture)) 100 | { 101 | var memInfo = type.GetMember(type.GetEnumName(val)); 102 | var nameAttribute = memInfo[0] 103 | .GetCustomAttributes(typeof(ChoiceNameAttribute), false) 104 | .FirstOrDefault() as ChoiceNameAttribute; 105 | 106 | return nameAttribute != null ? nameAttribute.Name : type.GetEnumName(val); 107 | } 108 | } 109 | } 110 | return null; 111 | } 112 | } 113 | } -------------------------------------------------------------------------------- /IChoiceProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | 5 | using DSharpPlus.Entities; 6 | 7 | namespace DSharpPlus.SlashCommands 8 | { 9 | /// 10 | /// All choice providers must inherit from this interface. 11 | /// 12 | public interface IChoiceProvider 13 | { 14 | /// 15 | /// Sets the choices for the slash command. 16 | /// 17 | Task> Provider(); 18 | } 19 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Adi Mathur 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## This extension has been merged into the main DSharpPlus library. This repository will no longer be maintained and further development will continue on the main repository. The nuget package will still be available if you wish to use it with the stable version (1.7.6 works with stable version 4.1.0), but if you're using the nightlies use the official DSharpPlus.SlashCommands package from now on. Thanks for all the feedback and support, it's been a (mostly) fun ride. 2 | 3 | # DSharpPlus.SlashCommands 4 | 5 | [![Nuget](https://img.shields.io/nuget/vpre/IDoEverything.DSharpPlus.SlashCommands?style=flat-square)](https://www.nuget.org/packages/IDoEverything.DSharpPlus.SlashCommands) 6 | [![Discord](https://img.shields.io/discord/801857343930761281?label=Discord&logo=Discord&style=flat-square)](https://discord.gg/2ZhXXVJYhU) 7 | 8 | An extension for [DSharpPlus](https://github.com/DSharpPlus/DSharpPlus) to make slash commands easier. 9 | 10 | Join the [Discord server](https://discord.gg/2ZhXXVJYhU) for any questions, help or discussion. 11 | 12 | # Documentation 13 | 14 | Documentation and further maintenance has moved over to https://github.com/DSharpPlus/DSharpPlus/tree/master/DSharpPlus.SlashCommands 15 | -------------------------------------------------------------------------------- /SlashCheckBaseAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace DSharpPlus.SlashCommands 5 | { 6 | /// 7 | /// The base class for a pre-execution check for a slash command. 8 | /// 9 | public abstract class SlashCheckBaseAttribute : Attribute 10 | { 11 | /// 12 | /// Checks whether this command can be executed within the current context. 13 | /// 14 | /// The context. 15 | /// Whether the checks passed. 16 | public abstract Task ExecuteChecksAsync(InteractionContext ctx); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /SlashCommandConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace DSharpPlus.SlashCommands 5 | { 6 | /// 7 | /// A configuration for a . 8 | /// 9 | public sealed class SlashCommandsConfiguration 10 | { 11 | /// 12 | /// Sets the service provider. 13 | /// Objects in this provider are used when instantiating slash command modules. This allows passing data around without resorting to static members. 14 | /// Defaults to null. 15 | /// 16 | public IServiceProvider Services { internal get; set; } = new ServiceCollection().BuildServiceProvider(true); 17 | } 18 | } -------------------------------------------------------------------------------- /SlashCommandsExtension.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Threading.Tasks; 6 | 7 | using DSharpPlus.Entities; 8 | using DSharpPlus.EventArgs; 9 | using DSharpPlus.Exceptions; 10 | using DSharpPlus.SlashCommands.EventArgs; 11 | 12 | using Emzi0767.Utilities; 13 | 14 | using Microsoft.Extensions.DependencyInjection; 15 | using Microsoft.Extensions.Logging; 16 | 17 | namespace DSharpPlus.SlashCommands 18 | { 19 | /// 20 | /// A class that handles slash commands for a client. 21 | /// 22 | public sealed class SlashCommandsExtension : BaseExtension 23 | { 24 | //A list of methods for top level commands 25 | private static List _commandMethods { get; set; } = new(); 26 | //List of groups 27 | private static List _groupCommands { get; set; } = new(); 28 | //List of groups with subgroups 29 | private static List _subGroupCommands { get; set; } = new(); 30 | //List of context menus 31 | private static List _contextMenuCommands { get; set; } = new(); 32 | 33 | //Singleton modules 34 | private static List _singletonModules { get; set; } = new(); 35 | 36 | //List of modules to register 37 | private List> _updateList { get; set; } = new(); 38 | //Configuration for DI 39 | private readonly SlashCommandsConfiguration _configuration; 40 | //Set to true if anything fails when registering 41 | private static bool _errored { get; set; } = false; 42 | 43 | /// 44 | /// Gets a list of registered commands. The key is the guild id (null if global). 45 | /// 46 | public IReadOnlyList>> RegisteredCommands => _registeredCommands; 47 | private static List>> _registeredCommands = new(); 48 | 49 | internal SlashCommandsExtension(SlashCommandsConfiguration configuration) 50 | { 51 | _configuration = configuration; 52 | } 53 | 54 | /// 55 | /// Runs setup. DO NOT RUN THIS MANUALLY. DO NOT DO ANYTHING WITH THIS. 56 | /// 57 | /// The client to setup on. 58 | protected override void Setup(DiscordClient client) 59 | { 60 | if (this.Client != null) 61 | throw new InvalidOperationException("What did I tell you?"); 62 | 63 | this.Client = client; 64 | 65 | _slashError = new AsyncEvent("SLASHCOMMAND_ERRORED", TimeSpan.Zero, null); 66 | _slashExecuted = new AsyncEvent("SLASHCOMMAND_EXECUTED", TimeSpan.Zero, null); 67 | _contextMenuErrored = new AsyncEvent("CONTEXTMENU_ERRORED", TimeSpan.Zero, null); 68 | _contextMenuExecuted = new AsyncEvent("CONTEXTMENU_EXECUTED", TimeSpan.Zero, null); 69 | 70 | Client.Ready += Update; 71 | Client.InteractionCreated += InteractionHandler; 72 | Client.ContextMenuInteractionCreated += ContextMenuHandler; 73 | } 74 | 75 | /// 76 | /// Registers a command class. 77 | /// 78 | /// The command class to register. 79 | /// The guild id to register it on. If you want global commands, leave it null. 80 | public void RegisterCommands(ulong? guildId = null) where T : ApplicationCommandModule 81 | { 82 | if (Client.ShardId is 0) 83 | _updateList.Add(new(guildId, typeof(T))); 84 | } 85 | 86 | /// 87 | /// Registers a command class. 88 | /// 89 | /// The of the command class to register. 90 | /// The guild id to register it on. If you want global commands, leave it null. 91 | public void RegisterCommands(Type type, ulong? guildId = null) 92 | { 93 | if (!typeof(ApplicationCommandModule).IsAssignableFrom(type)) 94 | throw new ArgumentException("Command classes have to inherit from ApplicationCommandModule", nameof(type)); 95 | //If sharding, only register for shard 0 96 | if (Client.ShardId is 0) 97 | _updateList.Add(new(guildId, type)); 98 | } 99 | 100 | //To be run on ready 101 | internal Task Update(DiscordClient client, ReadyEventArgs e) => Update(); 102 | 103 | //Actual method for registering, used for RegisterCommands and on Ready 104 | internal Task Update() 105 | { 106 | //Only update for shard 0 107 | if (Client.ShardId is 0) 108 | { 109 | //Groups commands by guild id or global 110 | foreach (var key in _updateList.Select(x => x.Key).Distinct()) 111 | { 112 | RegisterCommands(_updateList.Where(x => x.Key == key).Select(x => x.Value), key); 113 | } 114 | } 115 | return Task.CompletedTask; 116 | } 117 | 118 | //Method for registering commands for a target from modules 119 | private void RegisterCommands(IEnumerable types, ulong? guildId) 120 | { 121 | //Initialize empty lists to be added to the global ones at the end 122 | var commandMethods = new List(); 123 | var groupCommands = new List(); 124 | var subGroupCommands = new List(); 125 | var contextMenuCommands = new List(); 126 | var updateList = new List(); 127 | 128 | _ = Task.Run(async () => 129 | { 130 | //Iterates over all the modules 131 | foreach (var type in types) 132 | { 133 | try 134 | { 135 | var module = type.GetTypeInfo(); 136 | var classes = new List(); 137 | 138 | //Add module to classes list if it's a group 139 | if (module.GetCustomAttribute() != null) 140 | { 141 | classes.Add(module); 142 | } 143 | else 144 | { 145 | //Otherwise add the nested groups 146 | classes = module.DeclaredNestedTypes.Where(x => x.GetCustomAttribute() != null).ToList(); 147 | } 148 | 149 | //Handles groups 150 | foreach (var subclassinfo in classes) 151 | { 152 | //Gets the attribute and methods in the group 153 | var groupAttribute = subclassinfo.GetCustomAttribute(); 154 | var submethods = subclassinfo.DeclaredMethods.Where(x => x.GetCustomAttribute() != null); 155 | var subclasses = subclassinfo.DeclaredNestedTypes.Where(x => x.GetCustomAttribute() != null); 156 | if (subclasses.Any() && submethods.Any()) 157 | { 158 | throw new ArgumentException("Slash command groups cannot have both subcommands and subgroups!"); 159 | } 160 | 161 | //Group context menus 162 | var contextMethods = subclassinfo.DeclaredMethods.Where(x => x.GetCustomAttribute() != null); 163 | AddContextMenus(contextMethods); 164 | 165 | //Initializes the command 166 | var payload = new DiscordApplicationCommand(groupAttribute.Name, groupAttribute.Description, defaultPermission: groupAttribute.DefaultPermission); 167 | 168 | var commandmethods = new List>(); 169 | //Handles commands in the group 170 | foreach (var submethod in submethods) 171 | { 172 | var commandAttribute = submethod.GetCustomAttribute(); 173 | 174 | //Gets the paramaters and accounts for InteractionContext 175 | var parameters = submethod.GetParameters(); 176 | if (parameters?.Length is null or 0 || !ReferenceEquals(parameters.First().ParameterType, typeof(InteractionContext))) 177 | throw new ArgumentException($"The first argument must be an InteractionContext!"); 178 | parameters = parameters.Skip(1).ToArray(); 179 | 180 | var options = await ParseParameters(parameters, guildId); 181 | 182 | //Creates the subcommand and adds it to the main command 183 | var subpayload = new DiscordApplicationCommandOption(commandAttribute.Name, commandAttribute.Description, ApplicationCommandOptionType.SubCommand, null, null, options); 184 | payload = new DiscordApplicationCommand(payload.Name, payload.Description, payload.Options?.Append(subpayload) ?? new[] { subpayload }, payload.DefaultPermission); 185 | 186 | //Adds it to the method lists 187 | commandmethods.Add(new(commandAttribute.Name, submethod)); 188 | groupCommands.Add(new() { Name = groupAttribute.Name, Methods = commandmethods }); 189 | } 190 | 191 | var command = new SubGroupCommand { Name = groupAttribute.Name }; 192 | //Handles subgroups 193 | foreach (var subclass in subclasses) 194 | { 195 | var subGroupAttribute = subclass.GetCustomAttribute(); 196 | //I couldn't think of more creative naming 197 | var subsubmethods = subclass.DeclaredMethods.Where(x => x.GetCustomAttribute() != null); 198 | 199 | var options = new List(); 200 | 201 | var currentMethods = new List>(); 202 | 203 | //Similar to the one for regular groups 204 | foreach (var subsubmethod in subsubmethods) 205 | { 206 | var suboptions = new List(); 207 | var commatt = subsubmethod.GetCustomAttribute(); 208 | var parameters = subsubmethod.GetParameters(); 209 | if (parameters?.Length is null or 0 || !ReferenceEquals(parameters.First().ParameterType, typeof(InteractionContext))) 210 | throw new ArgumentException($"The first argument must be an InteractionContext!"); 211 | parameters = parameters.Skip(1).ToArray(); 212 | suboptions = suboptions.Concat(await ParseParameters(parameters, guildId)).ToList(); 213 | 214 | var subsubpayload = new DiscordApplicationCommandOption(commatt.Name, commatt.Description, ApplicationCommandOptionType.SubCommand, null, null, suboptions); 215 | options.Add(subsubpayload); 216 | commandmethods.Add(new(commatt.Name, subsubmethod)); 217 | currentMethods.Add(new(commatt.Name, subsubmethod)); 218 | } 219 | 220 | //Subgroups Context Menus 221 | var subContextMethods = subclass.DeclaredMethods.Where(x => x.GetCustomAttribute() != null); 222 | AddContextMenus(subContextMethods); 223 | 224 | //Adds the group to the command and method lists 225 | var subpayload = new DiscordApplicationCommandOption(subGroupAttribute.Name, subGroupAttribute.Description, ApplicationCommandOptionType.SubCommandGroup, null, null, options); 226 | command.SubCommands.Add(new() { Name = subGroupAttribute.Name, Methods = currentMethods }); 227 | payload = new DiscordApplicationCommand(payload.Name, payload.Description, payload.Options?.Append(subpayload) ?? new[] { subpayload }, payload.DefaultPermission); 228 | 229 | //Accounts for lifespans for the sub group 230 | if (subclass.GetCustomAttribute() is not null and { Lifespan: SlashModuleLifespan.Singleton }) 231 | { 232 | _singletonModules.Add(CreateInstance(subclass, _configuration?.Services)); 233 | } 234 | } 235 | 236 | if (command.SubCommands.Any()) 237 | subGroupCommands.Add(command); 238 | 239 | updateList.Add(payload); 240 | 241 | //Accounts for lifespans 242 | if (subclassinfo.GetCustomAttribute() is not null and { Lifespan: SlashModuleLifespan.Singleton }) 243 | { 244 | _singletonModules.Add(CreateInstance(subclassinfo, _configuration?.Services)); 245 | } 246 | } 247 | 248 | //Handles methods, only if the module isn't a group itself 249 | if (module.GetCustomAttribute() is null) 250 | { 251 | //Slash commands (again, similar to the one for groups) 252 | var methods = module.DeclaredMethods.Where(x => x.GetCustomAttribute() != null); 253 | 254 | foreach (var method in methods) 255 | { 256 | var commandattribute = method.GetCustomAttribute(); 257 | 258 | var parameters = method.GetParameters(); 259 | if (parameters?.Length is null or 0 || !ReferenceEquals(parameters.FirstOrDefault()?.ParameterType, typeof(InteractionContext))) 260 | throw new ArgumentException($"The first argument must be an InteractionContext!"); 261 | parameters = parameters.Skip(1).ToArray(); 262 | var options = await ParseParameters(parameters, guildId); 263 | 264 | commandMethods.Add(new() { Method = method, Name = commandattribute.Name }); 265 | 266 | var payload = new DiscordApplicationCommand(commandattribute.Name, commandattribute.Description, options, commandattribute.DefaultPermission); 267 | updateList.Add(payload); 268 | } 269 | 270 | //Context Menus 271 | var contextMethods = module.DeclaredMethods.Where(x => x.GetCustomAttribute() != null); 272 | AddContextMenus(contextMethods); 273 | 274 | //Accounts for lifespans 275 | if (module.GetCustomAttribute() is not null and { Lifespan: SlashModuleLifespan.Singleton }) 276 | { 277 | _singletonModules.Add(CreateInstance(module, _configuration?.Services)); 278 | } 279 | } 280 | 281 | void AddContextMenus(IEnumerable contextMethods) 282 | { 283 | foreach (var contextMethod in contextMethods) 284 | { 285 | var contextAttribute = contextMethod.GetCustomAttribute(); 286 | var command = new DiscordApplicationCommand(contextAttribute.Name, null, type: contextAttribute.Type, defaultPermission: contextAttribute.DefaultPermission); 287 | 288 | var parameters = contextMethod.GetParameters(); 289 | if (parameters?.Length is null or 0 || !ReferenceEquals(parameters.FirstOrDefault()?.ParameterType, typeof(ContextMenuContext))) 290 | throw new ArgumentException($"The first argument must be a ContextMenuContext!"); 291 | if (parameters.Length > 1) 292 | throw new ArgumentException($"A context menu cannot have parameters!"); 293 | 294 | contextMenuCommands.Add(new ContextMenuCommand { Method = contextMethod, Name = contextAttribute.Name }); 295 | 296 | updateList.Add(command); 297 | } 298 | } 299 | } 300 | catch (Exception ex) 301 | { 302 | //This isn't really much more descriptive but I added a separate case for it anyway 303 | if (ex is BadRequestException brex) 304 | Client.Logger.LogCritical(brex, $"There was an error registering application commands: {brex.JsonMessage}"); 305 | else 306 | Client.Logger.LogCritical(ex, $"There was an error registering application commands"); 307 | 308 | _errored = true; 309 | } 310 | } 311 | 312 | if (!_errored) 313 | { 314 | try 315 | { 316 | IEnumerable commands; 317 | //Creates a guild command if a guild id is specified, otherwise global 318 | commands = guildId is null 319 | ? await Client.BulkOverwriteGlobalApplicationCommandsAsync(updateList) 320 | : await Client.BulkOverwriteGuildApplicationCommandsAsync(guildId.Value, updateList); 321 | 322 | //Checks against the ids and adds them to the command method lists 323 | foreach (var command in commands) 324 | { 325 | if (commandMethods.Any(x => x.Name == command.Name)) 326 | commandMethods.First(x => x.Name == command.Name).CommandId = command.Id; 327 | 328 | else if (groupCommands.Any(x => x.Name == command.Name)) 329 | groupCommands.First(x => x.Name == command.Name).CommandId = command.Id; 330 | 331 | else if (subGroupCommands.Any(x => x.Name == command.Name)) 332 | subGroupCommands.First(x => x.Name == command.Name).CommandId = command.Id; 333 | 334 | else if (contextMenuCommands.Any(x => x.Name == command.Name)) 335 | contextMenuCommands.First(x => x.Name == command.Name).CommandId = command.Id; 336 | } 337 | //Adds to the global lists finally 338 | _commandMethods.AddRange(commandMethods); 339 | _groupCommands.AddRange(groupCommands); 340 | _subGroupCommands.AddRange(subGroupCommands); 341 | _contextMenuCommands.AddRange(contextMenuCommands); 342 | 343 | _registeredCommands.Add(new(guildId, commands.ToList())); 344 | } 345 | catch (Exception ex) 346 | { 347 | if (ex is BadRequestException brex) 348 | Client.Logger.LogCritical(brex, $"There was an error registering application commands: {brex.JsonMessage}"); 349 | else 350 | Client.Logger.LogCritical(ex, $"There was an error registering application commands"); 351 | 352 | _errored = true; 353 | } 354 | } 355 | }); 356 | } 357 | 358 | private Task InteractionHandler(DiscordClient client, InteractionCreateEventArgs e) 359 | { 360 | _ = Task.Run(async () => 361 | { 362 | if (e.Interaction.Type == InteractionType.ApplicationCommand) 363 | { 364 | //Creates the context 365 | InteractionContext context = new InteractionContext 366 | { 367 | Interaction = e.Interaction, 368 | Channel = e.Interaction.Channel, 369 | Guild = e.Interaction.Guild, 370 | User = e.Interaction.User, 371 | Client = client, 372 | SlashCommandsExtension = this, 373 | CommandName = e.Interaction.Data.Name, 374 | InteractionId = e.Interaction.Id, 375 | Token = e.Interaction.Token, 376 | Services = this._configuration?.Services, 377 | ResolvedUserMentions = e.Interaction.Data.Resolved?.Users?.Values.ToList(), 378 | ResolvedRoleMentions = e.Interaction.Data.Resolved?.Roles?.Values.ToList(), 379 | ResolvedChannelMentions = e.Interaction.Data.Resolved?.Channels?.Values.ToList(), 380 | Type = ApplicationCommandType.SlashCommand 381 | }; 382 | 383 | try 384 | { 385 | if (_errored) 386 | throw new InvalidOperationException("Slash commands failed to register properly on startup."); 387 | 388 | //Gets the method for the command 389 | var methods = _commandMethods.Where(x => x.CommandId == e.Interaction.Data.Id); 390 | var groups = _groupCommands.Where(x => x.CommandId == e.Interaction.Data.Id); 391 | var subgroups = _subGroupCommands.Where(x => x.CommandId == e.Interaction.Data.Id); 392 | if (!methods.Any() && !groups.Any() && !subgroups.Any()) 393 | throw new InvalidOperationException("A slash command was executed, but no command was registered for it."); 394 | 395 | //Just read the code you'll get it 396 | if (methods.Any()) 397 | { 398 | var method = methods.First().Method; 399 | 400 | var args = await ResolveInteractionCommandParameters(e, context, method, e.Interaction.Data.Options); 401 | 402 | await RunCommand(context, method, args); 403 | } 404 | else if (groups.Any()) 405 | { 406 | var command = e.Interaction.Data.Options.First(); 407 | var method = groups.First().Methods.First(x => x.Key == command.Name).Value; 408 | 409 | var args = await ResolveInteractionCommandParameters(e, context, method, e.Interaction.Data.Options.First().Options); 410 | 411 | await RunCommand(context, method, args); 412 | } 413 | else if (subgroups.Any()) 414 | { 415 | var command = e.Interaction.Data.Options.First(); 416 | var group = subgroups.First().SubCommands.First(x => x.Name == command.Name); 417 | 418 | var method = group.Methods.First(x => x.Key == command.Options.First().Name).Value; 419 | 420 | var args = await ResolveInteractionCommandParameters(e, context, method, e.Interaction.Data.Options.First().Options.First().Options); 421 | 422 | await RunCommand(context, method, args); 423 | } 424 | 425 | await _slashExecuted.InvokeAsync(this, new SlashCommandExecutedEventArgs { Context = context }); 426 | } 427 | catch (Exception ex) 428 | { 429 | await _slashError.InvokeAsync(this, new SlashCommandErrorEventArgs { Context = context, Exception = ex }); 430 | } 431 | } 432 | }); 433 | return Task.CompletedTask; 434 | } 435 | 436 | private Task ContextMenuHandler(DiscordClient client, ContextMenuInteractionCreateEventArgs e) 437 | { 438 | _ = Task.Run(async () => 439 | { 440 | //Creates the context 441 | ContextMenuContext context = new ContextMenuContext 442 | { 443 | Interaction = e.Interaction, 444 | Channel = e.Interaction.Channel, 445 | Client = client, 446 | Services = this._configuration?.Services, 447 | CommandName = e.Interaction.Data.Name, 448 | SlashCommandsExtension = this, 449 | Guild = e.Interaction.Guild, 450 | InteractionId = e.Interaction.Id, 451 | User = e.Interaction.User, 452 | Token = e.Interaction.Token, 453 | TargetUser = e.TargetUser, 454 | TargetMessage = e.TargetMessage, 455 | Type = e.Type 456 | }; 457 | 458 | try 459 | { 460 | if (_errored) 461 | throw new InvalidOperationException("Context menus failed to register properly on startup."); 462 | 463 | //Gets the method for the command 464 | var method = _contextMenuCommands.FirstOrDefault(x => x.CommandId == e.Interaction.Data.Id); 465 | 466 | if (method == null) 467 | throw new InvalidOperationException("A context menu was executed, but no command was registered for it."); 468 | 469 | await RunCommand(context, method.Method, new[] { context }); 470 | 471 | await _contextMenuExecuted.InvokeAsync(this, new ContextMenuExecutedEventArgs { Context = context }); 472 | } 473 | catch (Exception ex) 474 | { 475 | await _contextMenuErrored.InvokeAsync(this, new ContextMenuErrorEventArgs { Context = context, Exception = ex }); 476 | } 477 | }); 478 | 479 | return Task.CompletedTask; 480 | } 481 | 482 | internal async Task RunCommand(BaseContext context, MethodInfo method, IEnumerable args) 483 | { 484 | object classInstance; 485 | 486 | //Accounts for lifespans 487 | SlashModuleLifespan moduleLifespan = (method.DeclaringType.GetCustomAttribute() != null ? method.DeclaringType.GetCustomAttribute()?.Lifespan : SlashModuleLifespan.Transient) ?? SlashModuleLifespan.Transient; 488 | switch (moduleLifespan) 489 | { 490 | case SlashModuleLifespan.Scoped: 491 | //Accounts for static methods and adds DI 492 | classInstance = method.IsStatic ? ActivatorUtilities.CreateInstance(_configuration?.Services.CreateScope().ServiceProvider, method.DeclaringType) : CreateInstance(method.DeclaringType, _configuration?.Services.CreateScope().ServiceProvider); 493 | break; 494 | 495 | case SlashModuleLifespan.Transient: 496 | //Accounts for static methods and adds DI 497 | classInstance = method.IsStatic ? ActivatorUtilities.CreateInstance(_configuration?.Services, method.DeclaringType) : CreateInstance(method.DeclaringType, _configuration?.Services); 498 | break; 499 | 500 | //If singleton, gets it from the singleton list 501 | case SlashModuleLifespan.Singleton: 502 | classInstance = _singletonModules.First(x => ReferenceEquals(x.GetType(), method.DeclaringType)); 503 | break; 504 | 505 | default: 506 | throw new Exception($"An unknown {nameof(SlashModuleLifespanAttribute)} scope was specified on command {context.CommandName}"); 507 | } 508 | 509 | ApplicationCommandModule module = null; 510 | if (classInstance is ApplicationCommandModule mod) 511 | module = mod; 512 | 513 | //Slash commands 514 | if (context is InteractionContext slashContext) 515 | { 516 | await RunPreexecutionChecksAsync(method, slashContext); 517 | 518 | //Runs BeforeExecution and accounts for groups that don't inherit from ApplicationCommandModule 519 | var shouldExecute = await (module?.BeforeSlashExecutionAsync(slashContext) ?? Task.FromResult(true)); 520 | 521 | if (shouldExecute) 522 | { 523 | await (Task)method.Invoke(classInstance, args.ToArray()); 524 | 525 | //Runs AfterExecution and accounts for groups that don't inherit from ApplicationCommandModule 526 | await (module?.AfterSlashExecutionAsync(slashContext) ?? Task.CompletedTask); 527 | } 528 | } 529 | //Context menus 530 | if (context is ContextMenuContext CMContext) 531 | { 532 | await RunPreexecutionChecksAsync(method, CMContext); 533 | 534 | //This null check actually shouldn't be necessary for context menus but I'll keep it in just in case 535 | var shouldExecute = await (module?.BeforeContextMenuExecutionAsync(CMContext) ?? Task.FromResult(true)); 536 | 537 | if (shouldExecute) 538 | { 539 | await (Task)method.Invoke(classInstance, args.ToArray()); 540 | 541 | await (module?.AfterContextMenuExecutionAsync(CMContext) ?? Task.CompletedTask); 542 | } 543 | } 544 | } 545 | 546 | //Property injection copied over from CommandsNext 547 | internal object CreateInstance(Type t, IServiceProvider services) 548 | { 549 | var ti = t.GetTypeInfo(); 550 | var constructors = ti.DeclaredConstructors 551 | .Where(xci => xci.IsPublic) 552 | .ToArray(); 553 | 554 | if (constructors.Length != 1) 555 | throw new ArgumentException("Specified type does not contain a public constructor or contains more than one public constructor."); 556 | 557 | var constructor = constructors[0]; 558 | var constructorArgs = constructor.GetParameters(); 559 | var args = new object[constructorArgs.Length]; 560 | 561 | if (constructorArgs.Length != 0 && services == null) 562 | throw new InvalidOperationException("Dependency collection needs to be specified for parameterized constructors."); 563 | 564 | // inject via constructor 565 | if (constructorArgs.Length != 0) 566 | for (var i = 0; i < args.Length; i++) 567 | args[i] = services.GetRequiredService(constructorArgs[i].ParameterType); 568 | 569 | var moduleInstance = Activator.CreateInstance(t, args); 570 | 571 | // inject into properties 572 | var props = t.GetRuntimeProperties().Where(xp => xp.CanWrite && xp.SetMethod != null && !xp.SetMethod.IsStatic && xp.SetMethod.IsPublic); 573 | foreach (var prop in props) 574 | { 575 | if (prop.GetCustomAttribute() != null) 576 | continue; 577 | 578 | var service = services.GetService(prop.PropertyType); 579 | if (service == null) 580 | continue; 581 | 582 | prop.SetValue(moduleInstance, service); 583 | } 584 | 585 | // inject into fields 586 | var fields = t.GetRuntimeFields().Where(xf => !xf.IsInitOnly && !xf.IsStatic && xf.IsPublic); 587 | foreach (var field in fields) 588 | { 589 | if (field.GetCustomAttribute() != null) 590 | continue; 591 | 592 | var service = services.GetService(field.FieldType); 593 | if (service == null) 594 | continue; 595 | 596 | field.SetValue(moduleInstance, service); 597 | } 598 | 599 | return moduleInstance; 600 | } 601 | 602 | //Parses slash command parameters 603 | private async Task> ResolveInteractionCommandParameters(InteractionCreateEventArgs e, InteractionContext context, MethodInfo method, IEnumerable options) 604 | { 605 | var args = new List { context }; 606 | var parameters = method.GetParameters().Skip(1); 607 | 608 | for (int i = 0; i < parameters.Count(); i++) 609 | { 610 | var parameter = parameters.ElementAt(i); 611 | 612 | //Accounts for optional arguments without values given 613 | if (parameter.IsOptional && (options == null || 614 | (!options?.Any(x => x.Name == parameter.GetCustomAttribute().Name.ToLower()) ?? true))) 615 | args.Add(parameter.DefaultValue); 616 | else 617 | { 618 | var option = options.Single(x => x.Name == parameter.GetCustomAttribute().Name.ToLower()); 619 | 620 | //Checks the type and casts/references resolved and adds the value to the list 621 | //This can probably reference the slash command's type property that didn't exist when I wrote this and it could use a cleaner switch instead, but if it works it works 622 | if (parameter.ParameterType == typeof(string)) 623 | args.Add(option.Value.ToString()); 624 | else if (parameter.ParameterType.IsEnum) 625 | args.Add(Enum.Parse(parameter.ParameterType, (string)option.Value)); 626 | else if (parameter.ParameterType == typeof(long) || parameter.ParameterType == typeof(long?)) 627 | args.Add((long?)option.Value); 628 | else if (parameter.ParameterType == typeof(bool) || parameter.ParameterType == typeof(bool?)) 629 | args.Add((bool?)option.Value); 630 | else if (parameter.ParameterType == typeof(double) || parameter.ParameterType == typeof(double?)) 631 | args.Add((double?)option.Value); 632 | else if (parameter.ParameterType == typeof(DiscordUser)) 633 | { 634 | //Checks through resolved 635 | if (e.Interaction.Data.Resolved.Members != null && 636 | e.Interaction.Data.Resolved.Members.TryGetValue((ulong)option.Value, out var member)) 637 | args.Add(member); 638 | else if (e.Interaction.Data.Resolved.Users != null && 639 | e.Interaction.Data.Resolved.Users.TryGetValue((ulong)option.Value, out var user)) 640 | args.Add(user); 641 | else 642 | args.Add(await Client.GetUserAsync((ulong)option.Value)); 643 | } 644 | else if (parameter.ParameterType == typeof(DiscordChannel)) 645 | { 646 | //Checks through resolved 647 | if (e.Interaction.Data.Resolved.Channels != null && 648 | e.Interaction.Data.Resolved.Channels.TryGetValue((ulong)option.Value, out var channel)) 649 | args.Add(channel); 650 | else 651 | args.Add(e.Interaction.Guild.GetChannel((ulong)option.Value)); 652 | } 653 | else if (parameter.ParameterType == typeof(DiscordRole)) 654 | { 655 | //Checks through resolved 656 | if (e.Interaction.Data.Resolved.Roles != null && 657 | e.Interaction.Data.Resolved.Roles.TryGetValue((ulong)option.Value, out var role)) 658 | args.Add(role); 659 | else 660 | args.Add(e.Interaction.Guild.GetRole((ulong)option.Value)); 661 | } 662 | else if (parameter.ParameterType == typeof(SnowflakeObject)) 663 | { 664 | //Checks through resolved 665 | if (e.Interaction.Data.Resolved.Roles != null && e.Interaction.Data.Resolved.Roles.TryGetValue((ulong)option.Value, out var role)) 666 | args.Add(role); 667 | else if (e.Interaction.Data.Resolved.Members != null && e.Interaction.Data.Resolved.Members.TryGetValue((ulong)option.Value, out var member)) 668 | args.Add(member); 669 | else if (e.Interaction.Data.Resolved.Users != null && e.Interaction.Data.Resolved.Users.TryGetValue((ulong)option.Value, out var user)) 670 | args.Add(user); 671 | else 672 | throw new ArgumentException("Error resolving mentionable option."); 673 | } 674 | else if (parameter.ParameterType == typeof(DiscordEmoji)) 675 | { 676 | var value = option.Value.ToString(); 677 | 678 | if (DiscordEmoji.TryFromUnicode(Client, value, out var emoji) || DiscordEmoji.TryFromName(Client, value, out emoji)) 679 | args.Add(emoji); 680 | else 681 | throw new ArgumentException("Error parsing emoji parameter."); 682 | } 683 | else 684 | throw new ArgumentException("Error resolving interaction."); 685 | } 686 | } 687 | 688 | return args; 689 | } 690 | 691 | //Runs pre-execution checks 692 | private async Task RunPreexecutionChecksAsync(MethodInfo method, BaseContext context) 693 | { 694 | if (context is InteractionContext ctx) 695 | { 696 | //Gets all attributes from parent classes as well and stuff 697 | var attributes = new List(); 698 | attributes.AddRange(method.GetCustomAttributes(true)); 699 | attributes.AddRange(method.DeclaringType.GetCustomAttributes()); 700 | if (method.DeclaringType.DeclaringType != null) 701 | { 702 | attributes.AddRange(method.DeclaringType.DeclaringType.GetCustomAttributes()); 703 | if (method.DeclaringType.DeclaringType.DeclaringType != null) 704 | { 705 | attributes.AddRange(method.DeclaringType.DeclaringType.DeclaringType.GetCustomAttributes()); 706 | } 707 | } 708 | 709 | var dict = new Dictionary(); 710 | foreach (var att in attributes) 711 | { 712 | //Runs the check and adds the result to a list 713 | var result = await att.ExecuteChecksAsync(ctx); 714 | dict.Add(att, result); 715 | } 716 | 717 | //Checks if any failed, and throws an exception 718 | if (dict.Any(x => x.Value == false)) 719 | throw new SlashExecutionChecksFailedException { FailedChecks = dict.Where(x => x.Value == false).Select(x => x.Key).ToList() }; 720 | } 721 | if (context is ContextMenuContext CMctx) 722 | { 723 | var attributes = new List(); 724 | attributes.AddRange(method.GetCustomAttributes(true)); 725 | attributes.AddRange(method.DeclaringType.GetCustomAttributes()); 726 | if (method.DeclaringType.DeclaringType != null) 727 | { 728 | attributes.AddRange(method.DeclaringType.DeclaringType.GetCustomAttributes()); 729 | if (method.DeclaringType.DeclaringType.DeclaringType != null) 730 | { 731 | attributes.AddRange(method.DeclaringType.DeclaringType.DeclaringType.GetCustomAttributes()); 732 | } 733 | } 734 | 735 | var dict = new Dictionary(); 736 | foreach (var att in attributes) 737 | { 738 | //Runs the check and adds the result to a list 739 | var result = await att.ExecuteChecksAsync(CMctx); 740 | dict.Add(att, result); 741 | } 742 | 743 | //Checks if any failed, and throws an exception 744 | if (dict.Any(x => x.Value == false)) 745 | throw new ContextMenuExecutionChecksFailedException { FailedChecks = dict.Where(x => x.Value == false).Select(x => x.Key).ToList() }; 746 | } 747 | } 748 | 749 | //Gets the choices from a choice provider 750 | private async Task> GetChoiceAttributesFromProvider( 751 | IEnumerable customAttributes, 752 | ulong? guildId 753 | ) 754 | { 755 | var choices = new List(); 756 | foreach (var choiceProviderAttribute in customAttributes) 757 | { 758 | var method = choiceProviderAttribute.ProviderType.GetMethod(nameof(IChoiceProvider.Provider)); 759 | 760 | if (method == null) 761 | throw new ArgumentException("ChoiceProviders must inherit from IChoiceProvider."); 762 | else 763 | { 764 | var instance = Activator.CreateInstance(choiceProviderAttribute.ProviderType); 765 | 766 | // Abstract class offers more properties that can be set 767 | if (choiceProviderAttribute.ProviderType.IsSubclassOf(typeof(ChoiceProvider))) 768 | { 769 | choiceProviderAttribute.ProviderType.GetProperty(nameof(ChoiceProvider.GuildId)) 770 | ?.SetValue(instance, guildId); 771 | 772 | choiceProviderAttribute.ProviderType.GetProperty(nameof(ChoiceProvider.Services)) 773 | ?.SetValue(instance, _configuration.Services); 774 | } 775 | 776 | //Gets the choices from the method 777 | var result = await (Task>)method.Invoke(instance, null); 778 | 779 | if (result.Any()) 780 | { 781 | choices.AddRange(result); 782 | } 783 | } 784 | } 785 | 786 | return choices; 787 | } 788 | 789 | //Gets choices from an enum 790 | private static List GetChoiceAttributesFromEnumParameter(Type enumParam) 791 | { 792 | var choices = new List(); 793 | foreach (Enum enumValue in Enum.GetValues(enumParam)) 794 | { 795 | choices.Add(new DiscordApplicationCommandOptionChoice(enumValue.GetName(), enumValue.ToString())); 796 | } 797 | return choices; 798 | } 799 | 800 | //Small method to get the parameter's type from its type 801 | private ApplicationCommandOptionType GetParameterType(Type type) 802 | { 803 | if (type == typeof(string)) 804 | return ApplicationCommandOptionType.String; 805 | else if (type == typeof(long) || type == typeof(long?)) 806 | return ApplicationCommandOptionType.Integer; 807 | else if (type == typeof(bool) || type == typeof(bool?)) 808 | return ApplicationCommandOptionType.Boolean; 809 | else if (type == typeof(double) || type == typeof(double?)) 810 | return ApplicationCommandOptionType.Number; 811 | else if (type == typeof(DiscordChannel)) 812 | return ApplicationCommandOptionType.Channel; 813 | else if (type == typeof(DiscordUser)) 814 | return ApplicationCommandOptionType.User; 815 | else if (type == typeof(DiscordRole)) 816 | return ApplicationCommandOptionType.Role; 817 | else if (type == typeof(DiscordEmoji)) 818 | return ApplicationCommandOptionType.String; 819 | else if (type == typeof(SnowflakeObject)) 820 | return ApplicationCommandOptionType.Mentionable; 821 | 822 | else if (type.IsEnum) 823 | return ApplicationCommandOptionType.String; 824 | 825 | else 826 | throw new ArgumentException("Cannot convert type! Argument types must be string, long, bool, double, DiscordChannel, DiscordUser, DiscordRole, DiscordEmoji, SnowflakeObject or an Enum."); 827 | } 828 | 829 | //Gets choices from choice attributes 830 | private List GetChoiceAttributesFromParameter(IEnumerable choiceattributes) 831 | { 832 | if (!choiceattributes.Any()) 833 | { 834 | return null; 835 | } 836 | 837 | return choiceattributes.Select(att => new DiscordApplicationCommandOptionChoice(att.Name, att.Value)).ToList(); 838 | } 839 | 840 | //Handles the parameters for a slash command 841 | private async Task> ParseParameters(ParameterInfo[] parameters, ulong? guildId) 842 | { 843 | var options = new List(); 844 | foreach (var parameter in parameters) 845 | { 846 | //Gets the attribute 847 | var optionattribute = parameter.GetCustomAttribute(); 848 | if (optionattribute == null) 849 | throw new ArgumentException("Arguments must have the Option attribute!"); 850 | 851 | //Sets the type 852 | var type = parameter.ParameterType; 853 | var parametertype = GetParameterType(type); 854 | 855 | //Handles choices 856 | //From attributes 857 | var choices = GetChoiceAttributesFromParameter(parameter.GetCustomAttributes()); 858 | //From enums 859 | if (parameter.ParameterType.IsEnum) 860 | { 861 | choices = GetChoiceAttributesFromEnumParameter(parameter.ParameterType); 862 | } 863 | //From choice provider 864 | var choiceProviders = parameter.GetCustomAttributes(); 865 | if (choiceProviders.Any()) 866 | { 867 | choices = await GetChoiceAttributesFromProvider(choiceProviders, guildId); 868 | } 869 | 870 | var channelTypes = parameter.GetCustomAttribute()?.ChannelTypes ?? null; 871 | 872 | options.Add(new DiscordApplicationCommandOption(optionattribute.Name, optionattribute.Description, parametertype, !parameter.IsOptional, choices, null, channelTypes)); 873 | } 874 | 875 | return options; 876 | } 877 | 878 | /// 879 | /// Refreshes your commands, used for refreshing choice providers or applying commands registered after the ready event on the discord client. 880 | /// Should only be run on the slash command extension linked to shard 0 if sharding. 881 | /// Not recommended and should be avoided since it can make slash commands be unresponsive for a while. 882 | /// 883 | public async Task RefreshCommands() 884 | { 885 | _commandMethods.Clear(); 886 | _groupCommands.Clear(); 887 | _subGroupCommands.Clear(); 888 | _registeredCommands.Clear(); 889 | 890 | await Update(); 891 | } 892 | 893 | /// 894 | /// Fires when the execution of a slash command fails. 895 | /// 896 | public event AsyncEventHandler SlashCommandErrored 897 | { 898 | add { _slashError.Register(value); } 899 | remove { _slashError.Unregister(value); } 900 | } 901 | private AsyncEvent _slashError; 902 | 903 | /// 904 | /// Fires when the execution of a slash command is successful. 905 | /// 906 | public event AsyncEventHandler SlashCommandExecuted 907 | { 908 | add { _slashExecuted.Register(value); } 909 | remove { _slashExecuted.Unregister(value); } 910 | } 911 | private AsyncEvent _slashExecuted; 912 | 913 | /// 914 | /// Fires when the execution of a context menu fails. 915 | /// 916 | public event AsyncEventHandler ContextMenuErrored 917 | { 918 | add { _contextMenuErrored.Register(value); } 919 | remove { _contextMenuErrored.Unregister(value); } 920 | } 921 | private AsyncEvent _contextMenuErrored; 922 | 923 | /// 924 | /// Fire when the execution of a context menu is successful. 925 | /// 926 | public event AsyncEventHandler ContextMenuExecuted 927 | { 928 | add { _contextMenuExecuted.Register(value); } 929 | remove { _contextMenuExecuted.Unregister(value); } 930 | } 931 | private AsyncEvent _contextMenuExecuted; 932 | } 933 | 934 | //I'm not sure if creating separate classes is the cleanest thing here but I can't think of anything else so these stay 935 | 936 | internal class CommandMethod 937 | { 938 | public ulong CommandId { get; set; } 939 | public string Name { get; set; } 940 | public MethodInfo Method { get; set; } 941 | } 942 | 943 | internal class GroupCommand 944 | { 945 | public ulong CommandId { get; set; } 946 | public string Name { get; set; } 947 | public List> Methods { get; set; } = null; 948 | } 949 | 950 | internal class SubGroupCommand 951 | { 952 | public ulong CommandId { get; set; } 953 | public string Name { get; set; } 954 | public List SubCommands { get; set; } = new List(); 955 | } 956 | 957 | internal class ContextMenuCommand 958 | { 959 | public ulong CommandId { get; set; } 960 | public string Name { get; set; } 961 | public MethodInfo Method { get; set; } 962 | } 963 | } 964 | -------------------------------------------------------------------------------- /SlashExecutionChecksFailedException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace DSharpPlus.SlashCommands 5 | { 6 | /// 7 | /// Thrown when a pre-execution check for a slash command fails. 8 | /// 9 | public sealed class SlashExecutionChecksFailedException : Exception 10 | { 11 | /// 12 | /// The list of failed checks. 13 | /// 14 | public IReadOnlyList FailedChecks; 15 | } 16 | } --------------------------------------------------------------------------------