Creating Commands
Handlers
Wolfringo Commands System works using "Handlers". Commands Handlers are classes where all command methods reside. Commands System will automatically create instances of these classes.
Commands Handlers have a few requirements:
- They cannot be static.
- They cannot be abstract.
- If they have a custom constructor, all of its parameters must be possible to resolve using Dependency Injection.
- Are marked with [CommandsHandler] attribute.
An exception to this rule are handlers that are added individually to CommandsOptions.Classes.
[CommandsHandler]
private class MyCommandsHandler
{
// any code goes here
}
Handlers lifetime
By default, all handlers are non-persistent (aka transient), and are stateless. A new instance is created just before a command is executed, and discarded as soon as the command execution finishes.
For majority of use cases, transient handlers are perfectly fine. There are 2 exceptions to this:
- Your handler needs to listen to any events - be it of IWolfClient or any other object.
- Your handler needs to store some state in its properties or variables.
If your handler meets either of these criteria, you can mark handler as persistent (aka singleton). These handlers are created as soon as CommandService.StartAsync() is called, and will be kept in memory until Commands Service is disposed or application terminates.
To mark handler as persistent, set IsPersistent to true:
[CommandsHandler(IsPersistent = true)]
private class MyPersistentCommandsHandler
{
// use any properties or event listeners here
}
Disposable Handlers
Sometimes Commands Handlers use unmanaged resources or other disposable classes inside. If your handler needs to be disposed, just make it implement @System.IDisposable interface.
[CommandsHandler]
private class MyDisposableCommandsHandler : IDisposable
{
public void Dispose()
{
// your disposing logic here
}
}
CommandsService will automatically call Dispose() on these handlers. Non-persistent handlers will be disposed as soon as command execution finishes. Persistent handlers will be disposed when CommandsService.Dispose() is called, or application is exiting.
Multiple constructors
If your handler has multiple constructors, the commands system will attempt to use the one that it can resolve the most parameters for. If it can't resolve any of the parameters, CommandsService will log an error, and fail to execute any command from that handler.
By default, all non-static constructors in the handler will be taken into account. If you have a constructor (or multiple constructors) that you want to be used by Commands System, you can give it a [CommandsHandlerConstructor] attribute. If any of the constructors has this attribute, Commands System will ignore all constructors without this attribute.
If multiple constructors are marked with [CommandsHandlerConstructor] attribute, by default Commands System will try to find the constructor that it can resolve most attributes for - just like when none of the constructors have the attribute. You can override this by giving [CommandsHandlerConstructor] a priority value.
[CommandsHandler]
private class ExampleCommandsHandler
{
// constructor with priority of 10
[CommandsHandlerConstructor(10)]
public ExampleCommandsHandler(IWolfClient client)
{
// ...
}
}
Commands System will attempt to use constructors with higher priority before constructors with lower priority. If you don't specify priority, the constructor will have default priority of 0.
Commands methods
Commands methods are the methods that are executed when command is invoked. Commands methods:
- Cannot be static
- Should not be "async void". If you need "async" in your command, return a @System.Threading.Tasks.Task or a @System.Threading.Tasks.Task`1 instead.
- If needed, can return ICommandResult or Task<ICommandResult>
- Need to be marked as a command.
Marking method as a command
Wolfringo Commands System includes support for 2 types of commands - Standard and Regex.
Standard Commands are marked with [Command] attribute. They are most basic commands, and you might be accomodated with them if you used other bot libraries.
Regex Commands are marked with [RegexCommand] attribute. They utilize power of Regular Expressions to allow advanced text processing and matching.
[Command("standard")]
private async Task StandardCommandExampleAsync()
{
// command code
}
[RegexCommand("^regex")]
private async Task RegexCommandExampleAsync()
{
// command code
}
Command parameters
A command would be useless if it had no knowledge on what the message is, or how to reply to the user. This information is provided to commands methods by Commands System via parameters.
Commands can have following types of parameters:
CommandContext
Command context represents fundamental information about the command being executed, such as the message or wolf client instance. To use the context, use CommandContext or ICommandContext as parameter type.
[Command("example")]
private async Task ExampleAsync(CommandContext context)
{
// command code here
}
Command context provides following properties:
- Message - a ChatMessage that triggered the command. Using this property, you can also ID of the user that sent the command, whether it was a group or private message, or ID of the recipient.
- Client - a IWolfClient that received the message. You can use this client to request profiles, send messages, or communicate with WOLF server.
- Options - an instance of CommandsOptions. This will be the same options as the ones that were configured when Enabling Commands in your bot.
Command context also has a few extension methods that make building commands easier:
- GetSenderAsync() - retrieves the WolfUser that sent the command.
- GetBotProfileAsync() - retrieves the profile (WolfUser) of the client that received the message (so, profile of the bot).
- GetRecipientAsync<T>() - retrieves the recipient of the message.
If CommandContext.Message is a group message,T
should be WolfGroup.
If CommandContext.Message is a private message,T
should WolfUser, and will work the same as GetBotProfileAsync().
If you provide wrong generic type, this method will returnnull
. - ReplyTextAsync(text) - sends a text response.
- ReplyImageAsync(text) - sends an image response.
- ReplyVoiceAsync(text) - sends a voice response.
[Command("example")]
private async Task ExampleAsync(CommandContext context)
{
await context.ReplyTextAsync("Test passed!");
}
IWolfClient
You can pass IWolfClient as parameter. It'll be the same client as CommandContext.Client property.
ChatMessage
You can pass ChatMessage as parameter. It'll be the same message as CommandContext.Message property.
Command Arguments
Any text from message after the command itself is treated as arguments. A parameter will be treated as an argument if its type isn't any of the types mentioned above, and an IArgumentConverter is registered in IArgumentConverterProvider. Types supported by default:
- @System.String
- @System.Boolean
- @System.Char
- @System.DateTime
- @System.DateTimeOffset
- @System.TimeSpan
- @System.Int16, @System.UInt16
- @System.Int32, @System.UInt32
- @System.Int64, @System.UInt64
- @System.Byte, @System.SByte
- @System.Single
- @System.Double
- @System.Decimal
- @System.Numerics.BigInteger
- WolfTimestamp
- Any enum type
Arguments splitting
The way arguments are split varies between Standard and Regex commands.
In Standard commands, arguments are split by space, unless they're grouped. Commands are grouped if they're wrapped into " "
, [ ]
or ( )
.
Example: assume that you have a command [Command("test")]
, and prefix is !
.
If user sends "!test foo bar 2 my group", there will be 5 arguments: foo
, bar
, 2
, my
and group
.
If user sends "!test foo (bar 2) [my group]", there will be 3 arguments: foo
, bar 2
and my group
.
In Regex commands, arguments will represent value of each of regex groups.
Tip
The markers (" "
, [ ]
and ( )
) can be modified by changing ArgumentsParserOptions.
In normal bots, manual creation of ArgumentsParser and providing it to Commands Service using Dependency Injection.
In bots using Wolfringo.Hosting, you can use methods like AddArgumentBlockMarker(char, char) or RemoveArgumentBlockMarker(char), or use Options Pattern.
Arguments order
Arguments are ordered as they appear in message. They'll be attempted to be converted and inserted into the method in the same order.
Assume that you have a command [Command("test")]
, and prefix is !
.
User sends "!test foo bar".
[Command("test")]
private async Task TestAsync(string argument1, string argument2)
{
Console.WriteLine(argument1); // will print "foo"
Console.WriteLine(argument2); // will print "bar"
}
Customizing error messages
When user invokes a command and argument is missing or cannot be converted, Commands System will automatically reply with a default error message. You can customize these errors using attributes:
- [MissingError(text)] to customize error when user didn't provide the argument at all;
- [ConvertingError(text)] to customize error when converting of an argument failed.
[Command("example")]
private async Task ExampleAsync(
[ConvertingError("(n) '{{Arg}}' is not a valid number!")]
[MissingError("(n) You need to provide delay value!")] int number)
{
// command code here
}
As example above shows, these custom messages can also have placeholders inside of them. These placeholders will be automatically replaced with corresponding values:
{{Arg}}
- value of the argument as provided by user sending the message. Will be replaced with empty string when used with [MissingError(text)] attribute.{{Type}}
- parameter type.{{Name}}
- parameter name.{{Message}}
- all text of the message sent by the user when invoking the command.{{SenderNickname}}
- nickname (display name) of the user that invoked the command.{{SenderID}}
- ID of the user that invoked the command.{{BotNickname}}
- nickname (display name) of the bot.{{BotID}}
- ID of the bot.{{RecipientID}}
- ID of the message recipient (bot ID for private messages, group ID for group messages).{{RecipientName}}
- name of the message recipient (bot nickname for private messages, group name for group messages).
You can also set text to null
or empty string - in such case, error response will be disabled for that command.
Tip
[ConvertingError(text)] will never be triggered if parameter type is @System.String, as arguments are handled as strings internally.
Optional arguments
To mark argument as optional, set a default value for that parameter:
[Command("example")]
private async Task ExampleAsync(string optionalArgument = null)
{
// command code here
}
Optional arguments will not cause an error if they're missing - command will still run, and parameter will simply use the default value.
Warning
Optional values disable [MissingError(text)], but they do not disable [ConvertingError(text)] - bot will still reply with an error if optional argument was provided, but converting has failed.
Catch-all
If you use an array of strings (string[]
) as a parameter type, all arguments will be inserted into it.
Warning
Argument Group markers will not be included, only the values themselves. If you want to grab raw text of the arguments, use see Arguments Text below.
Arguments Text
If you want to see all arguments as single string before they're split up, you can add a new string parameter and mark it with [ArgumentsText] attribute.
Assume that in following example, the user sends !say Hello, this is some text (with parentheses!)
:
[Prefix("!")]
[Command("say")]
private async Task ExampleAsync([ArgumentsText] string text)
{
Console.WriteLine(text); // will print "Hello, this is some text (with parentheses!)"
}
Warning
When used with Regex command, this will return entire regex match text. This is because with Regex commands, Wolfringo has no way to separate command name from the arguments. You can instead use groups in your regex, and inject Regex @System.Text.RegularExpressions.Match into your command.
ICommandOptions
By using ICommandOptions you can get the values of prefix, prefix requirement and case sensitivity for this command. These options will automatically use overrides from command's arguments (such as [Prefix]) if there are any.
CancellationToken
You can pass @System.Threading.CancellationToken as paremeter. You can then use this cancellation token in your other asynchronous calls. This cancellation token will be set to cancelled when CommandsService is being disposed - for example when the application is exiting.
[Command("example")]
private async Task ExampleAsync(CommandContext context, CancellationToken cancellationToken)
{
string fileContents = await File.ReadAllTextAsync("myfile.txt", cancellationToken);
}
Regex Match
For Regex commands, you can use Regex @System.Text.RegularExpressions.Match as one of the arguments. This will output the exact regex match for your regex command (therefore won't include prefix).
[RegexCommand("^log (.+)$")]
public void CmdLog(Match match, ILogger log)
{
log.LogInformation(match.Groups[1].Value);
}
ILogger
If you enabled logging when you were creating CommandsService (by passing a logger into constructor, or using .NET Generic Host/ASP.NET Core), you can pass in an instance of @Microsoft.Extensions.Logging.ILogger. You can then use that in your command code to log anything you want.
Dependency Injection services
Any services registered with Dependency Injection can also be used as a parameter. Please check Dependency Injection guide for more information.
ICommandInstance
Internally, all [Command] and [RegexCommand] are converted into command instance objects. For most scenarios this just a fun-fact, but sometimes (for example, when using Aliases) you may want to get access to the instance of the class. To do so, simply add a parameter of type ICommandInstance.
Commands Priorities
Unless you use ICommandResult, Commands System will execute maximum of one command method, even if multiple commands could be triggered by user's text. For example: [Command("test")]
and [Command("test2")]
.
If you want to control which command gets attempted first, use [Priority(value)] attribute. Commands with highest priority value will be checked first.
[Command("example")]
[Priority(5)]
public async Task Example1()
{
// command code here
}
[Command("example")]
[Priority(15)]
public async Task Example2()
{
// command code here
}
In the example above, command Example1 will never be executed, because Example2 has the same text, but higher priority.
Overriding Options
CommandsOptions class holds default settings for commands - you can customize them when enabling Commands System. On top of that, you can also change these settings using attributes.
These attributes can be put on the command method or on the entire handler. Commands options are checked in the following order:
- Attributes on the method.
- Attributes on the handler.
- CommandsOptions instance.
For example, in following example, Prefix for ExampleCommand1 will be !
, but for ExampleCommand2 it will be ?
:
[CommandsHandler]
[Prefix("!")]
private class ExampleCommandsHandler
{
[Command("example1")]
private void ExampleCommand1() { }
[Command("example2")]
[Prefix("?")]
private void ExampleCommand2() { }
}
Following attributes are available for use:
- [Prefix(string)] - overrides command's prefix.
- [Prefix(PrefixRequirement)] - overrides command's prefix requirement.
- [Prefix(string, PrefixRequirement)] - overrides both command's prefix and prefix requirement.
- [CaseSensitivity(bool)] - overrides command's case sensitivity.
Aliases
Wolfringo does not have [Alias]
attribute. Instead, the same method can have multiple attributes. Internally they're completely separate command instances, but because Commands System only ever runs one command at once, they'll work as if they were aliases.
[Command("alias 1")]
[Command("alias 2")]
public Task ExampleAsync()
{
// command code
}
Returning ICommandResult
By default, Wolfringo's CommandsService will consider command successful if it ran fully, or failed if any exception was thrown. In either of these cases, the Commands System will stop processing the received message, so no further [Command] attribute or [RegexCommand] attribute will be attempted.
However if you wish, you can mark the command as skipped instead - in such case, Wolfringo will attempt to execute other commands if they match. To do so, you can return ICommandResult or Task<ICommandResult> and set the Status property to Skip. You can return any type of ICommandResult, but the built-in CommandExecutionResult is sufficient for most use cases.
using TehGM.Wolfringo.Commands.Results;
[Command("example")]
[Priority(5)]
public async Task<ICommandResult> Example1()
{
// command code here
return CommandExecutionResult.Skip;
}
[Command("example")]
[Priority(15)]
public async Task Example2()
{
// because Example1 returned result CommandResultStatus.Skip, this method will run as well!
// command code here
}
Tip
For internal purposes, CommandExecutionResult can have Exception property. However it is recommended to not use it for that purpose - simply throw the exception instead!
What's next?
Examples
Example projects include example Commands Handlers:
- Check ExampleTransientCommandsHandler.cs for more examples on creating commands methods.
- Check ExamplePersistentCommandsHandler.cs for example on Persistent (singleton) handler.
Moving further
Now you are able to create new commands using Wolfringo Commands System, but the system's possibilities do not end here.
If you want to check how to avoid repetitive code even further, check out Commands Requirements guide.
Make sure to also check out Dependency Injection guide to learn how to let commands use other systems in your bot!