-
Notifications
You must be signed in to change notification settings - Fork 2
Command Processing Pipeline
This describes the process used by the command processor to convert IRC messages into commands for the bot. The bot attempts to make life easy for Module writers by making sure that the input which arrives is always in the same format. The process by which this happens is documented below (with examples).
(see CommandListener for the code which does this)
First, the bot needs to know if you meant to address it. It does this by checking that message meets one of the following conditions:
- If the message was sent in a channel (CommandListener.onMessage called)
- The message starts with the trigger character (by default `&')
- The message starts with its name (obtained from the bot's connection).
- If the message was sent in private (CommandListener.onPrivateMessage called)
- We know that it was intended for the bot, so pass it to the next stage as-is
If none if these match, the bot discards the message and waits for the next one to check. If one of these conditions was triggered, any information used to address the bot (the trigger character or the bots name) is removed and the message is passed to the next step. This so the rest of the pipeline doesn't have to care how the bot was addressed, just that the message was intended for the bot.
assuming the bot is currently called bot on the network:
Input | Type | Matched | Resulting message |
---|---|---|---|
help test | Channel | No | N/A |
&help test | Channel | Yes | help test |
bot, help test | Channel | Yes | help test |
bot: help test | Channel | Yes | help test |
bot help test | Channel | Yes | help test |
bottest help | Channel | no | N/A |
help test | PM | Yes | help test |
&help | PM | Yes | &help |
bot, help | PM | Yes | bot, help |
As a result, it's important to remember not to use trigger keywords in private messages to the bot, as it will not remove them from the command. The bot regex will match a single character after the bot name, so bottest doesn't match but bot: and bot, will match, this allows the use of tab complete, which in most IRC clients will insert a character after the name.
(see CommandListener.extractMessage for the code which does this)
The bot cleans up the message to ensure that any formatting marks don't prevent it from recognising the command correctly or make the bot behave in strange ways. Middleware then gets a chance to examine the message before the command it processed. This hook allows the bot owner to rewrite or abort processing of commands sent to the bot before any Module sees it.
Some examples of middleware which may find this useful:
- Aliasing of commands (see
uk.co.unitycoders.pircbotx.commandprocessor.RewriteEngine
) - Removal of badwords
- Rate limiting
Middleware executes in the order defined in the configuration file, and gets passed the result of the steps above it. Therefore the order in which the Middleware is defined in your configuration file is important.
Assuming the input is the processed output of the step above and that the bot is using the default aliases in the configuration file.
Input | Result | Resulting message |
---|---|---|
help test | Nothing changed | help test |
help test | strip formatting | help test |
remember test is test message | rewritten by alias | factoid add test 'test is test message' |
Most of the examples from step 1 were condensed into `help', this makes life easier much easier for people writing Middleware as they don't have to worry about the prefixes or the way in which the command was invoked.
(See CommandProcessor.tokenise for the code which does this)
The bot understands commands in terms of arguments. Arguments are created by from the raw message by splitting the message by whitespace. The bot also has a way of dealing with arguments which need to contain whitespace: quote the argument in either single or double quotes. When extracting arguments, the bot will keep these together as one argument. The result is then encapsulated into a Message
object.
Input | help test | |
---|---|---|
Output | help | test |
Argument ID | 0 | 1 |
Input | factoid add test 'test is test message' | |||
---|---|---|---|---|
Output | factoid | add | test | test is test message |
Argument ID | 0 | 1 | 2 | 3 |
Each word becomes a separate argument, unless enclosed in quotes. When processed, the quotes are removed and the contents of the quotes are treated as a single argument.
(See CommandProcessor.invoke for the code which does this)
The second phase of Middleware processing happens after the message has been tokenised. This works similarly to the first phase but instead of working on strings, it works on the processed messages. This allows the Middleware to work at a higher level than the input level, which is more appropriate for some tasks.
The message gets passed though the Middleware in the order defined in the configuration file which can remove, alter or add arguments as required or stop processing of the message. It gives the Middleware a chance to intercept the message just before it reaches the module in the same format the module will see it.
The bot uses this hook internally to provide handling of the default
command inside the Modules via the command fixer middleware.
Some examples of middleware which would work well at this level:
- Altering requests (CommandFixerMiddleware)
- Permissions (SecurityMiddleware)
- Per module rate limiting
Assuming the command fixer middleware is enabled:
If argument 0 is not a valid module, and argument 1 is a command which exists in exactly 1 module, insert the module name:
Input | 8ball | |
---|---|---|
Output | games | 8ball |
Argument ID | 0 | 1 |
If argument 0 is a valid module, and argument 1 is not a valid command (or is null), insert default as argument 1 (this allows default commands to have arguments):
Input | help | |
---|---|---|
Output | help | default |
Argument ID | 0 | 1 |
Input | help | test | |
---|---|---|---|
Output | help | default | test |
Argument ID | 0 | 1 | 2 |
If argument 0 is a valid module, and argument 1 is a valid command in that module then don't alter it:
Input | factoid | add | test | test is test message |
---|---|---|---|---|
Output | factoid | add | test | test is test message |
Argument ID | 0 | 1 | 2 | 3 |
This ensures that all commands are of the form: module command [args]
when processed by the next stage and ultimately by the module/command. This step is very powerful step in the process and allows some really cool things to be done when processing commands.
The message has been processed, arguments extracted and system is ready to execute the command. The Command Processor gets the 0th argument from the message (the name of the module) and looks it up in the group of loaded modules. If no such module exists, the Command Processor throws a CommandNotFoundException
and the bot reports that an error occurred.
If a Module was matched, the Module's fire
method is called, which then is responsible for ensuring that the command exists and invoking the command with the message in the event it does. How this is accomplished and how commands in a module are defined depends on the Module (the simplest way of doing this is to use AnnotationModule).
If the module cannot find the command, it should throw a CommandNotFoundException
, which the bot will then deal with for it. If the user provides incorrect arguments to the command, the Module should throw a IllegalArgumentException
which the bot will handle for the Module. Any other exception thrown by the Module is treated as an Error, logged by the bot and a generic error message is reported.
Arguments are shown as a comma separated list for brevity
Input | Matched | Result |
---|---|---|
help, default, test | Yes | Passed to Module |
games, 8ball | Yes | Passed to Module |
invalid, default | No | CommandNotFoundException |
games, default | Yes | CommandNotFoundException (from module) |
games, default, invalid | Yes | CommandNotFoundException (from module) |
help, commands | Yes | IllegalArgumentException (from module) |
The only surprising thing here is that the invalid command games invalid
, will be converted by the CommandFixerMiddleware to games default invalid
as per the rules above. As games
does not provide a default
command, this will get rejected by the module, this will only happen with invalid commands (as the bot will not insert default
if argument 1 is already a valid command) and is an edge case.