Skip to content
This repository has been archived by the owner on Jan 19, 2018. It is now read-only.

Command Processing Pipeline

Joseph Walton-Rivers edited this page Jul 10, 2016 · 1 revision

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).

Detection

(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.

Examples

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.

Pre-processing (Middleware Step 1)

(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.

Examples

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.

Tokenisation

(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.

Examples

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.

Middleware

(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

Examples (CommandFixer)

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.

Invoke the command

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).

Error Handling

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.

Examples

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.