Adding Regex Commands to Discord.Net's Command System
Discord.Net includes a basic string + parameters Command System, which is enough for most peoples’ needs. However I like to use Regex for my commands. In this blog post, I explain how I added Regex Commands support for my personal admin bot Einherji.
Why Regex Commands?
Rigid string (that is - type exactly X, Y and Z in correct order) commands are simple and do the job - that’s why most people use them. And it’s all fine, they work and don’t require complex syntax like one of regex.
However, I do not fear regex - maybe I’m no regex expert, but I am fluent with it enough to use it with confidence. And this allows me to do things fancy way, exactly like I like to. Example? Einherji’s move all command allows for following format: move all (from) <src-channel> (to) <dest-channel>
- where channels can both be just an ID or a channel ping (in theory - Discord doesn’t allow pinging voice channels for now), and “from” and “to” are completely optional but fully accepted. This command, including finding of the channel IDs, is a complete one-liner:
|
|
I used to support Regex Commands by adding them in a handler constructor through my Discord.Net-Helper project, but this approach, along with the entire project itself, was less than perfect and suffered from some issues - such as dependency on concrete classes and almost no Dependency Injection support, so I abandoned it since. It should still work and you’re free to check it out or even use if you want - but I consider it obsolete.
Additionally, I plan to add Commands System to my Wolfringo library for WOLF/Palringo - Commands System is one of the main things still missing. I figured that extending Discord.Net’s Command System will be a good practice before writing my own system from scratch - it will let me know what to keep in mind, what to avoid, and where to be cautious.
Discord.Net’s existing commands
Discord.Net includes its own Command System. It belongs to the rigid category - it supports a constant string and parameters for input. As I mentioned before, it is enough for needs of most people - especially ones that aren’t as crazy for fancy command structures like me. I decided to try to build on top of that.
Sadly Discord.Net isn’t one of the libraries that really care about extensibility, and some things depend on concrete classes. It is less than perfect, but Discord.Net’s commands also have a few really strong characteristics - some I might even end up borrowing for my Wolfringo:
- It requires writing your own Command Handler - for many it’s an annoyance, but I see it as a place to be flexible - and that means I can use this to make Regex Commands work.
- Its attributes (such as Preconditions) are modular in nature - you just add multiple attributes to command method. This allows me to reuse them for my own purposes.
- It supports Dependency Injection using .NET’s native interfaces - this is huge. I mean, HUGE. This allows passing virtually anything to the command method/class - and it fits extremely nicely with .NET Generic Host approach, such as ASP.NET Core. This is a big win.
Regex Commands Implementation
CommandsOptions
Options Pattern really works well with Dependency Injection - especially if used with .NET Generic Host. For that reason, I set my regex commands to use a CommandsOptions class. It is basically a POCO object for settings that can be overwritten in ASP.NET Core settings:
|
|
Regex Command Instance
[RegexCommand] Attribute
Let’s start with defining Regex Command as an attribute - this will allow to mark a method to be handled as a Regex Command.
|
|
This class is pretty self-explanatory. We have pattern, and default RegexOptions
that can be overriden in a constructor. The attribute can be set on the method multiple times - it’ll work as an alias.
RegexCommandInstance Constructor and Properties
The 2nd class, RegexCommandInstance
, is a tad bigger, so let’s break it up. If you’d like to see the full class all at once, check it out on GitHub.
First, let’s have the things we need provided through a constructor.
|
|
IRegexCommandModuleProvider
- a service that will cache results of lookups on how to create command instances. Don’t worry, it’ll be explained below.Preconditions and Priority
We want Preconditions and Priority to be supported. We will use Discord.Net’s existing [Precondition] Attribute and [Priority] Attribute - they’re mostly fine, with one exception.
Discord.Net Precondition’s CheckPermissionsAsync
method takes a CommandInfo as a parameter. CommandInfo is closely related to CommandService, which is designed for the ‘rigid’ commands. This is one place where Discord.Net’s lack of extensibility support shows.
To work this around, we pass null
in place of CheckPermissionsAsync
. This could cause issue for some preconditions, but as of Discord.Net 2.2.0, none of the built-in Preconditions use that param, so with these, we’re safe… for now.
|
|
Building the Command
Next, we want a method that performs building of the command instance. First, let’s add an additional property that will determine how the command is executed:
|
|
Next, let’s add a static Build method - it’ll be called when initializing the command instance by Command Handler.
|
|
This method will grab provided MethodInfo and [RegexCommand] Attribute, along with DI ServiceProvider.
Then it takes CommandsOptions and IRegexCommandModuleProvider from the DI ServiceProvider, and uses them to create an instance of the class. Lastly, it calls LoadCustomAttributes twice - first time to grab any attributes on the class that the method is in - and then overwrite with or add the ones that are set on the method itself.
Execute Method
Now we can add a method to let actually execute the command - this method will be called by Command Handler if all preconditions passed.
|
|
This method requires some explanation.
- First, the method simply checks if the regex matches the message sent to the bot. If not, it’ll return quickly, telling the Command Handler that the execute was not successful, so it should try another command.
- If the regex match was found, the method iterates over parameters excpected by the command method. For each param, it checks the type of the parameter. If the type was found, it’ll be provided to the method. Otherwise, it’ll return an error of unsupported param type, or if the param is optional, provide a default.
- First it checks if it’s the ICommandContext that is already used by Discord.Net’s default Command System, or any of the classes that implement it.
- Then it checks if it is a Regex match. This allows the command method to use Regex match to grab groups etc, which can be used as command arguments.
- Then it checks if it is any of the properties that are provided by ICommandContext. This allows command method to take a guild or user as one of the params.
- Then it checks if it’s a
CancellationToken
, to support async execution cancellation. - If none of these are true, as last resort it’ll try to use
IServiceProvider
- this allows for injecting any service from DI into the method as a param.
- Once values for all method params are found, it’ll use IRegexCommandModuleProvider to get the module instance.
- Lastly, it’ll actually execute the method. If the method returns a
Task
, it’ll await it, or put it on a thread pool, depending on RunMode setting. - Once done, it’ll return success to Command Handler. Yay!
Command Module Provider
I mentioned a module provider multiple times, now it’s time to implement it. You can also see it on GitHub.
IRegexCommandModuleProvider
in my Commands System is designed to improve performance of command execution. To stay consistent with Discord.Net default approach to re-initialize a fresh instance for every execution, a lot of reflection would be used to find a constructor that can be resolved with things that are in DI container. To avoid this overhead, I chose to cache constructor selection for each command instance.
[PersistentModule] Attribute
On top of caching known constructors, using a module provider allows for having a persistent instances - ones that should NOT be recreated and scrapped for every execution. I find this useful for command classes that either listen to gateway events, or have a background Task. In Einherji I used that in a few places - for example with Elite Dangerous Community Goals feature.
To enable with this behaviour, I added a new attribute, which I called [PersistentModule]
:
|
|
This attribute allows for specifying additional behaviours that are related:
SharedInstance
- if set to true, all command methods inside the same class will share the instance of the command.PreInitialize
- if set to true, the command instance will be added to IRegexCommandModuleProvider as soon as it’s built. If false, it’ll be added when executing for the first time.
Before moving on to implementation of IRegexCommandModuleProvider itself, let’s just add support for PreInitialize
being false. To do so, we need to modify Build method a little bit.
Let’s add these 3 lines just before the method returns:
|
|
These lines will check for existence of the [PersistentModule]
attribute, check if PreInitialize
is true, and if so, request the module from IRegexCommandModuleProvider - this will trigger its instantiation.
IRegexCommandModuleProvider
Now we can create the module provider itself.
The IRegexCommandModuleProvider
is really simple:
|
|
The concrete implementation itself is relatively easy, too, but has some parts that need explaining, so let’s go step by step.
IRegexCommandModuleProvider Constructor and Properties
The constructor and properties for this class is relatively simple - we just take an IServiceProvider
, store it, and initialize empty collections:
- Dictionary of known modules, which is simply a cache of “which RegexCommandModuleInfo should I use for this RegexCommandInstance?”.
- Dictionary of module instances with [PersistentModule] Attribute, so they can be easily reused.
- Dictionary of shared module instances, defined with
SharedInstance
in [PersistentModule] Attribute, keyed by the class type that defines the command methods.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
public class RegexComandModuleProvider : IRegexCommandModuleProvider { private readonly IServiceProvider _services; private readonly IDictionary<Type, object> _sharedInstances; private readonly IDictionary<RegexCommandInstance, object> _persistentInstances; private readonly IDictionary<RegexCommandInstance, RegexCommandModuleInfo> _knownModules; public RegexComandModuleProvider(IServiceProvider services) { this._services = services; this._sharedInstances = new Dictionary<Type, object>(); this._persistentInstances = new Dictionary<RegexCommandInstance, object>(); this._knownModules = new Dictionary<RegexCommandInstance, RegexCommandModuleInfo>(); } }
RegexCommandModuleInfo
RegexCommandModuleInfo
is a simple class, used only by IRegexCommandModuleProvider
, which holds information on the [PersistentModule] Attribute values, the constructor that was found suitable, and the parameters to use when using that constructor. Let’s create it!
|
|
To initialize this class, let’s add a new method InitializeModuleInfo
into our RegexCommandModuleProvider
. This method will take a constructor info as input, and try to use IServiceProvider
to resolve all of its params. If it can resolve all, it’ll return a new RegexCommandModuleInfo, otherwise it’ll return null. It’ll also check if param is optional - if it is and service cannot be resolved, it’ll just use the default.
|
|
Retrieving the module
Now for the main star of the module provider - method GetModuleInstance
. Don’t worry, it isn’t too complex. To make it simpler, let’s break it into steps.
First, let’s check if the RegexCommandModuleInfo was already created before - if so, it’ll be cached in _knownModules
dictionary. If it’s not found, we will grab all constructors in the command class, and order it from the one with most parameters to the one with the least - this will allow to attempt to resolve most services possible - ASP.NET Core IServiceProvider
does by default, too.
For each of the constructors, we attempt to create a RegexCommandModuleInfo using InitializeModuleInfo
method we created just a moment ago. If it returns null, we check next constructor, otherwise we found our constructor and can cache it in _knownModules
dictionary!
If we checked all constructors and all returned null, we have an error, so let’s throw an exception.
|
|
Next, if we determined we can create the command module instance by finding a constructor, let’s check if this RegexCommandInstance already has a persistent module, using a simple dictionary check.
|
|
Then, if it’s not a persistent instance we already created - it could be that the command class is persistent AND shared - and that means, if any other method of that class created a module instance, we can use it for this command too! If that’s the case, we add that instance to _persistentInstances
dictionary - this means the persistent checks step will find this shared instance correctly next time.
|
|
If we still didn’t find a cached instance, it means that either it’s the first time we’re requesting it, or it’s not a persistent module at all. In either case, we create it. Then we can check if it’s a persistent or shared module - if so, we add it to cache dictionaries. Once we do this, we can return the instance.
|
|
And that covers the IRegexCommandModuleProvider
implementation. Wasn’t that bad, eh? And we’re almost done, we just need a Command Handler now.
Regex Command Handler
Discord.Net requires you to write a Command Handler for normal commands. For regex commands, we do something really similar. The only difference is that we don’t have a CommandService that would load the commands for us - but don’t worry, it’s quite easy.
For full code, check the code on GitHub.
RegexCommandHandler Constructor and Properties
First we need properties to store all the handler needs for modules, and constructor. I am using .NET Core Hosting Dependency Injection to add them all. We also want the handler to implement IDisposable
to stop listening to Discord.Net Client’s events when it’s deconstruction time. If you’re using .NET Generic Host, you also want to implement IHostedService
- this will ensure the handler is started when the Host starts.
|
|
Initializing Commands
As you can see in constructor and StartAsync
, we need a method InitializeCommandsAsync
, so let’s create it!
|
|
In this method, we load each assembly and each class type included in CommandsOptions. Once that is done, we order the commands by Priority
, to respect [Priority] Attribute.
Loading Commands
Initializing is simple, but AddAssembly
and AddType
do not exist yet - so let’s add them, too!
These methods use reflection to find the types. AddAssembly
checks all types in assembly that aren’t abstract or generic, aren’t generated by the compiler, and [LoadRegexCommands] Attribute - don’t worry, we’ll create it in a moment.AddType
does similar, but for methods - it finds all methods that aren’t static, generated by the compiler, and have at least one [RegexCommand] Attribute.
Lastly, AddMethod
builds a new Regex Command Instance for each [RegexCommand] Attribute it finds on the method.
|
|
[LoadRegexCommands] Attribute
I mentioned [LoadRegexCommands]
attribute, even though we never created it. But don’t worry, it’s really simple. Really:
|
|
AddAssembly
method will only attempt to load classes that have this attribute present, instead of every single class in your bot. Now, for every class with commands, you add a [LoadRegexCommands]
attribute, and handler will know it should try to load that class.This might sound like an inconvenience, but Discord.Net does something similar for its own Command System - except it requires you to inherit from ModuleBase class.
I found attribute to be more fitting. Yes, not inheriting from a class means you don’t get to use its properties, like Context
- but it’s okay, since we can just add it as a paremeter to the command method. In return, it means we have our ‘one inheritance spot’ free, and can inherit from any other class we would want to. If you ask me, that’s a win!
Handling a client message
Now, the final piece of our handler - actually handling the incoming messages.
This works very similar to an example provided by Discord.Net - the main difference is the last call to ExecuteAsync
method of the CommandService. Since we don’t use CommandService here, we can’t use it. Instead, replace that call with a snippet like following:
|
|
In above snippet, we iterate over each loaded command instance. For each instance we perform following steps.
- Check preconditions. If preconditions failed, we proceed to next command. This is similar to Discord.Net’s CommandService behaviour.
- Try to execute the command. We provide in context, arg position,
IServiceProvider
, and_hostCancellationToken
as a cancellation token. - If the execution was successful, we finish.
- Catch
OperationCanceledException
. We do it separately, as operation canceled is a normal occurence - it’ll happen whenever we stop the bot (and therefore set_hostCancellationToken
to cancelled) when a command execution is still in progress. You can log a warning before returning, it’s okay too. - Catch any other
Exception
and log it as error.
In my code, before the snippet I have shown above, I also do typical command handler stuff - prefix checking and context class creation. I utilize CommandsOptions during my prefix checks, so feel free to check the method on GitHub to see how I do it.
Using Commands System
That’s all the core code needed for regex commands. It was a long and perhaps even confusing, I know, but we’re almost done! Now we just need to mark all our command classes with [LoadRegexCommands] Attribute, and add a [RegexCommand] Attribute to every method that we want to act as a command. Many real commands can be seen in Einherji source code, but here I’ll throw one as an example:
|
|
With commands prepared, we just need to create an instances of IRegexCommandModuleProvider, RegexCommandHandler and their required services, and call InitializeCommandsAsync on the RegexCommandHandler
. The exact way you do it depends on how you start your bot.
.NET Generic Host / ASP.NET Core
If you use .NET Generic Host approach (for example, in ASP.NET Core, but not only), I have a good news for you - this tutorial includes Dependency Injection-enabled classes, and I have a helper class you can add to your project!
|
|
Once you added this class, all you need to do is call services.AddCommands();
in your ConfigureServices
and you’re good to go! This of course assumes you created a hosted discord client and added it to services as well - if you need an example, feel free to check the client created for Einherji on GitHub.
Other
Other methods might need some more work to get this started. You’ll need to manually create RegexCommandModuleProvider and RegexCommandHandler and IServiceProvider
. You might need to remove ILogger
and IOptionsMonitor
from RegexCommandHandler constructor, or figure out a way to create them without .NET Generic Host - but I’ll leave that up to you.
Summary
Whoa, was this a journey! I won’t say it will be easy for everyone to add this, but it’s not as difficult as it might initially seem. Yes, there was a fair amount of components and reflection needed, and it might not be 100% perfect, but well, it works!
But most importantly, that was a good learning experience - exactly what I needed before creating my own commands system for Wolfringo. I hope to make it easier to extend than Discord.Net’s system without making it harder to use - but we’ll see once I actually do it!
As I mentioned before - you can find full implementation of the Regex Commands System on GitHub in EinherjiBot repository.