How to write plugins

Anatomy of a plugin

Plugins are automatically discovered if they match the right pattern. They must

  • subclass csbot.plugin.Plugin, and
  • live under the package specified by csbot.core.Bot.PLUGIN_PACKAGE (csbot.plugins by default).

For example, a minimal plugin that does nothing might live in csbot/plugins/nothing.py and look like:

from csbot.plugin import Plugin

class Nothing(Plugin):
    pass

A plugin’s name is its class name in lowercase [1] and must be unique, so plugin classes should be named meaningfully. Changing a plugin name will cause it to lose access to its associated configuration and database, so try not to do that unless you’re prepared to migrate these things.

The vast majority of interaction with the outside world is through subscribing to events and registering commands.

Events

Root events are generated when the bot receives data from the IRC server, and further events may be generated while handling an event.

All events are represented by the Event class, which is a dictionary of event-related information with some additional helpful attributes. See Events for further information on the Event class and available events.

Events are hooked with the Plugin.hook() decorator. The decorated method will be called for every event that matches the specified event_type, with the event object as the only argument. For example, a basic logging plugin that prints sent and received data:

class Logger(Plugin):
    @Plugin.hook('core.raw.sent')
    def sent(self, e):
        print('<-- ' + e['message'])

    @Plugin.hook('core.raw.received')
    def received(self, e):
        print('--> ' + e['message'])

A single handler can hook more than one event:

class MessagePrinter(Plugin):
    @Plugin.hook('core.message.privmsg')
    @Plugin.hook('core.message.notice')
    def got_message(self, e):
        """Print out all messages, ignoring if they were PRIVMSG or NOTICE."""
        print(e['message'])

Commands

Registering commands provides a more structured way for users to interact with a plugin. A command can be any unique, non-empty sequence of non-whitespace characters, and are invoked when prefixed with the bot’s configured command prefix. Command events use the CommandEvent class, extending a core.message.privmsg Event and adding the arguments() method and the command and data items.

class CommandTest(Plugin):
    @Plugin.command('test')
    def hello(self, e):
        print(e['command'] + ' invoked with arguments ' + repr(e.arguments()))

A single handler can be registered for more than one command, e.g. to give aliases, and commands and hooks can be freely mixed.

class Friendly(Plugin):
    @Plugin.hook('core.channel.joined')
    @Plugin.command('hello')
    @Plugin.command('hi')
    def hello(self, e):
        e.protocol.msg(e['channel'], 'Hello, ' + nick(e['user']))

Responding: the BotProtocol object

In the above example the Event.protocol attribute was used to respond back to the IRC server. This attribute is an instance of BotProtocol, which subclasses twisted.words.protocols.irc.IRCClient for IRC protocol support. The documentation for IRCClient is the best place to find out what methods are supported when responding to an event or command.

Configuration

Basic string key/value configuration can be stored in an INI-style file. A plugin’s config attribute is a shortcut to a configuration section with the same name as the plugin. The Python 3 configparser is used instead of the Python 2 ConfigParser because it supports the mapping access protocol, i.e. it acts like a dictionary in addition to supporting its own API.

An example of using plugin configuration:

class Say(Plugin):
    @Plugin.command('say')
    def say(self, e):
        if self.config.getboolean('shout', False):
            e.protocol.msg(e['reply_to'], e['data'].upper() + '!')
        else:
            e.protocol.msg(e['reply_to'], e['data'])

For even more convenience, automatic fallback values are supported through the CONFIG_DEFAULTS attribute when using the config_get() or config_getboolean() methods instead of the corresponding methods on config. This is encouraged, since it makes it clear what configuration the plugin supports and what the default values are by looking at just one part of the plugin source code. The above example would look like this:

class Say(Plugin):
    CONFIG_DEFAULTS = {
        'shout': False,
    }

    @Plugin.command('say')
    def say(self, e):
        if self.config_getboolean('shout'):
            e.protocol.msg(e['reply_to'], e['data'].upper() + '!')
        else:
            e.protocol.msg(e['reply_to'], e['data'])

Configuration can be changed at runtime, but won’t be saved. This allows for temporary state changes, whilst ensuring the startup state of the bot reflects the configuration file. For example, the above plugin could be modified with a toggle for the “shout” mode:

class Say(Plugin):
    # ...
    @Plugin.command('toggle')
    def toggle(self, e):
        self.config['shout'] = not self.config_get('shout')

Database

The bot supports easy access to MongoDB through PyMongo. Plugins have a db attribute which is a pymongo.database.Database, unique to the plugin and created as needed. Refer to the PyMongo documentation for further guidance on using the API.

[1]This can be changed by overriding the plugin_name() class method if absolutely necessary.