Plugins

One of the most prominent features of helga is its support for plugins and plugin architecture. At their core, plugins are essentially standalone, installable python packages. There are few small rules to follow, but creating custom plugins is an incredibly easy process.

Plugin Types

Plugins have a notion of type. This essentially means that they have predefined expectations for how they behave. At this time, there are three types of plugins:

Commands

Plugins of this type require a user to specifically ask to perform some action. For example, a command plugin behave like this:

<sduncan> helga google something
<helga> no results found for "something"

(see Command Plugins)

Matches

Plugins of this type are intended to be a form of autoresponder that aim to provide some extra meaning or context to what a user has said in a chat. For example, a match plugin could provide extra details if someone says ‘foo’:

<sduncan> I'm talking about foo in this message
<helga> sduncan just said 'foo'

(see Match Plugins)

Preprocessors

Plugins of this type generally don’t respond. However, they can modify the original message that will be received by command or match plugins.

(see Preprocessor Plugins)

Plugin Priorities

Plugins also have a notion of priority that affect the order in which the plugin manager will process them. Priorities can be any numerical value, but as a rule of thumb, the higher the number, the more important a plugin will be. More important plugins will be processed first. Note, however, that preprocessor type plugins will always run before command and match plugins. Therefore, preprocessors will only be weighted against other preprocessors. Commands and matches are weighted against other commands and matches.

The helga.plugins module has three values that may be useful for indicating the priority of a plugin:

The actual values of these priorities can be fine tuned via custom settings (see Configuring Helga). Unless specifically indicated, each plugin type assumes a default priority:

Creating Plugins with Decorators

Helga comes with an easy-to-use decorator API for writing simple plugins. For the most part, this is the preferred way of creating custom plugins. In a nutshell, there are decorators in helga.plugins that correspond to each plugin type:

Command Plugins

Command plugins are those which require you to ask in order to perform some action. For these types of plugins, you will use the @command decorator:

helga.plugins.command(command, aliases=None, help='', priority=50, shlex=False)

A decorator for creating command plugins

Parameters:
  • command – The command string, i.e. ‘search’ for a command ‘helga search foo’
  • aliases – A list of command aliases. If a command ‘search’ has an alias list [‘s’], then ‘helga search foo’ and ‘helga s foo’ will both run the command.
  • help – An optional help string for the command. This is used by the builtin help plugin.
  • priority – The priority of the plugin. Default is PRIORITY_NORMAL.
  • shlex – A boolean indicating whether to use shlex arg string parsing rather than naive whitespace splitting.

Decorated functions should follow this pattern:

func(client, channel, nick, message, cmd, args)
Parameters:
  • client – an instance of helga.comm.irc.Client or helga.comm.xmpp.Client
  • channel – The channel from which the message was received. This could be a public channel like ‘#foo’, or in the event of a private message, could be the nick of the user sending the message
  • nick – The nick of the user sending the message
  • message – The full message string received from the server
  • cmd – The parsed command string which could be the registered command or one of the command aliases
  • args – The parsed command arguments as a list, i.e. any content following the command. For example: helga foo bar baz would be ['bar', 'baz']
Returns:

String or list of strings to return via chat. None or empty string or list for no response

For example:

from helga.plugins import command

@command('foo', aliases=['f'], help='The foo command')
def foo(client, channel, nick, message, cmd, args):
    return u'You said "helga foo"'

For argument parsing, there are currently two supported behaviors. The default is to perform whitespace splitting on the argument string. For example, given a command:

helga foo bar "baz qux"

the resulting args list to the command function would be:

['bar', '"baz', 'qux"']

For some plugins, this may be less than ideal. Therefore, you can optionally pass shlex=True to the @command decorator. This changes the behavior in such a way that in the previous example, the resulting args list would be:

['bar', 'baz qux']

This behavior can also be configured globally by configuring COMMAND_ARGS_SHLEX = True in your settings file (see Default Configuration)

Important

Shlex argument parsing will become the default behavior in a future version of helga.

Match Plugins

Match plugins are those that are intended to be a form of autoresponder. They are meant to provide some extra meaning or context to what a user has said in chat. For these types of plugins, you will use the @match decorator:

helga.plugins.match(pattern, priority=25)

A decorator for creating match plugins

Parameters:
  • pattern – A regular expression string used to match against a chat message. Optionally, this argument can be a callable that accepts a chat message string as its only argument and returns a value that can be evaluated for truthiness.
  • priority – The priority of the plugin. Default is PRIORITY_LOW

Decorated match functions should follow this pattern:

func(client, channel, nick, message, matches)
Parameters:
  • client – an instance of helga.comm.irc.Client or helga.comm.xmpp.Client
  • channel – The channel from which the message was received. This could be a public channel like ‘#foo’, or in the event of a private message, could be the nick of the user sending the message
  • nick – The nick of the user sending the message
  • message – The full message string received from the server
  • matches – The result of re.findall if decorated with a regular expression, otherwise the return value of the callable passed
Returns:

String or list of strings to return via chat. None or empty string or list for no response

For example:

from helga.plugins import match

@match(r'foo')
def foo(client, channel, nick, message, matches):
    return u'{0} said foo'.format(nick)

In most cases, this decorator will have a single regular expression as its argument. However, it can also accept a callable. This callable should accept a single argument: the message contents received from the chat server. There is no explicit return value type, but the return value should be able to be evaluated for truthiness. When that return value has truth, then the decorated function will be called. For example:

import time
from helga.plugins import match

def match_even(message):
    if int(time.time()) % 2 == 0:
        return 'Even Time!'

@match(match_even)
def even(client, channel, nick, message, matches):
    # Will send 'Match: Even Time!' to the server
    return u'Match: {0}'.format(matches)

Preprocessor Plugins

Preprocessor plugins generally don’t respond. Instead, they are intended to potentially modify the original chat message that will be received by command or match plugins. For these types of plugins, you will use the @preprocessor decorator:

helga.plugins.preprocessor(priority=50)

A decorator for creating preprocessor plugins

Parameters:priority – The priority of the plugin. Default is PRIORITY_NORMAL

Decorated preprocessor functions should follow this pattern:

func(client, channel, nick, message, matches)
Parameters:
  • client – an instance of helga.comm.irc.Client or helga.comm.xmpp.Client
  • channel – The channel from which the message was received. This could be a public channel like ‘#foo’, or in the event of a private message, could be the nick of the user sending the message
  • nick – The nick of the user sending the message
  • message – The full message string received from the server
Returns:

a three-tuple (channel, nick, message) containing any modifications

For example:

from helga.plugins import preprocessor

@preprocessor
def foo(client, channel, nick, message):
    # Other plugins will think the message argument is what is returned
    return channel, nick, 'NOT THE ORIGINAL MESSAGE'

Decorator Chaining

The decorators for commands, matches, and preprocessors can be chained for more complex behavior. For example, should you wish to have a command that could add or remove patterns for a match, you could chain both @command and @match. Note, however, that each plugin type decorator expects that decorated functions accept a specific number of arguments. For this reason, it is best to use *args and argument length checking (this may change in the future). For example, let’s say we want a plugin that will match a dynamic set of patterns, but also gives the ability to add or remove patterns and modifies the incoming message by prepending text to indicate that it has been processed:

import re
from helga.plugins import command, match, preprocessor

PATTERNS = set()

def check(message):
    global PATTERNS
    return re.findall('({0})'.format('|'.join(PATTERNS)))

@preprocessor
@match(check)
@command('matcher', help='Usage: helga (add|remove) <pattern>')
def matcher(client, channel, nick, message, *args):
    global PATTERNS

    if len(args) == 0:
        # Preprocessor
        return channel, nick, u'[matcher] {0}'.format(message)
    elif len(args) == 1:
        # Match - args[0] is return value of check(), re.findall
        found_list = args[0]
        return u'What you said matched: {0}'.format(found_list)
    elif len(args) == 2:
        # Command: args[1] is the parsed argument string
        command, pattern = args[1][:2]
        if command == 'add':
            PATTERNS.add(pattern)
            return u'Added {0}'.format(pattern)
        else:
            PATERNS.discard(pattern)
            return u'Removed {0}'.format(pattern)

Note, decorator chaining is only one way to create complex behavior for plugins. There is also a class-based plugin API (see Class-Based Plugins)

Handling Unicode

Plugins should try to deal exclusively with unicode as much as possible. This is important to keep in mind since all plugins that accept string arguments will receive unicode strings specifically and not byte strings. For the most part, helga’s client connection assumes a UTF-8 encoding for all incoming messages. Note, though, that plugins that don’t explicitly return unicode responses will not fail; the internal plugin manager will implicitly handle convertng all responses to the correct format (unicode or byte strings) needed by the chat server. There are also useful utilities for dealing with unicode support in plugins found in helga.util.encodings:

Accessing The Database

As mentioned in Requirements, MongoDB is highly recommended, but not required dependency. Having a MongoDB server that helga can use means that plugins can store data for use across restarts. This may be incredibly useful depending on the needs of your plugin. If your MongoDB connection is configured properly according to Core Settings, two pymongo objects in helga.db will be available for use:

Using this database connection in a plugin is very simple:

from helga.db import db

db.my_collection.insert({'foo': 'bar'})
db.my_collection.find()

For more information on using this, see the pymongo API documentation.

Note

Should helga not be configured properly for MongoDB, or should a connection to MongoDB fail, the database object helga.db.db will explicitly be None. Therefore, it may be important for plugins that depend on MongoDB to check for this condition.

Requiring Settings

In many instances, plugins may require some configurable setting in a custom helga settings file (see Custom Settings). As a general rule of thumb, configurable settings should be documented by a plugin but in no way should expect that they be present in helga.settings. Plugins should use getattr for retrieving custom settings and assume some default value:

from helga import settings

my_setting = getattr(settings, 'MY_SETTING_VALUE', 42)

Also, although not explicitly required, settings names should be prefixed with the name of the plugin. This should help in organizing custom settings. For example, if a plugin foo uses a custom setting SOME_VALUE, then try to expect a setting FOO_SOME_VALUE.

Communicating Asynchronously

In some cases, plugins may need to perform some blocking action such as communicating with an external API. If a plugin were to perform this action and directly return a string response, this may block other plugins from processing. To get around this concern, plugins can, instead of returning a response, raise ResponseNotReady. This will indicate to helga’s plugin manager that a response may be sent at some point in the future. In this instance, helga will continue to process other plugins, unless configured to only return first response, in which case no other plugins will be processed (see Default Configuration). For example:

from helga.plugins import command, ResponseNotReady

@command('foo')
def foo(client, channel, nick, message, cmd, args):
    # Run some async action
    raise ResonseNotReady

In order to actually invoke some asynchronous action, most plugins can and should utilize the fact that helga is built using Twisted by calling twisted.internet.reactor.callLater. For example:

from twisted.internet import reactor

def do_something(arg, kwarg=None):
    print arg or kwarg

# Have the event loop run `do_something` in 30 seconds
reactor.callLater(30, do_something, None, kwarg='foo')

For more details on this see the Twisted Documentation. To revisit the previous plugin example:

from twisted.internet import reactor
from helga.plugins import command, ResponseNotReady

def foo_async(client, channel, args):
    client.msg(channel, 'someone ran the foo command with args: {0}'.format(args))

@command('foo')
def foo(client, channel, nick, message, cmd, args):
    reactor.callLater(5, foo_async, client, channel, args)
    raise ResonseNotReady

Notice above that the callback function foo_async takes the client connection as an argument. Should a plugin need to respond asynchronously to the server, it is generally a good idea for deferred callbacks to accept at a minimum the client and the channel of the message. In addition, there are several useful methods of both helga.comm.irc.Client and helga.comm.xmpp.Client that can be used for asynchronous communication:

Signals/Notifications of Helga Events

Helga makes heavy use of signals for events provided by smokesignal. In this way, plugins can receive notifications when some event occurs and perform some action such as loading data from the database or setting some preferred state. At this time, there are several included signals that fire on given events and provide callbacks with certain arguments:

started
Fired when the helga process starts. Callbacks should accept no arguments.
signon

Fired when helga successfully connects to the chat server. Callbacks should follow:

func(client)
Parameters:client – an instance of helga.comm.irc.Client or helga.comm.xmpp.Client depending on the server type
join

Fired when helga joins a channel. Callbacks should follow:

func(client, channel)
Parameters:
left

Fired when helga leaves a channel. Callbacks should follow:

func(client, channel)
Parameters:
user_joined

Fired when a user joins a channel helga is in. Callbacks should follow:

func(client, nick, channel)
Parameters:
user_left

Fired when a user leaves a channel helga is in. Callbacks should follow:

func(client, nick, channel)
Parameters:

Packaging and Distribution

If you have created a simple helga plugin, you may be asking “What now?”. Helga, rather than using plugin directories containing lots of one-off scripts, makes use of proper python packaging to manage plugin installation. This may be a bit of an advanced topic if you are new to python packaging, but for the most part, you can follow a small number of repeatable steps for simple plugins.

Basic Project Structure

For the most part, simple plugins will follow the same basic project structure:

helga_my_plugin
├── helga_my_plugin.py
├── LICENSE
├── MANIFEST.in
├── README.rst
├── setup.py
├── tests.py
└── tox.ini
helga_my_plugin.py
This is the actual plugin script. This can be named whatever you feel like naming it, but it is good practice to name this something like helga_<name of plugin>.py.
LICENSE
Since helga is dual-licensed MIT and GPL, this can be either MIT or GPL
MANIFEST.in

If you wish to include any non-python files with your plugin, you should include this file. For example, if you wish to include the README and LICENSE, the contents of this file would be:

LICENSE
README.rst
README.rst
Not required to be a reStructuredText document, but it is good practice to describe what the plugin does, how to use it, and if there are any custom settings that should be set.
setup.py
setuptools setup script (see setuptools and entry_points)
tests.py
If you write any unit tests for your plugin
tox.ini
If you write any unit tests for your plugin and use tox to run them. It is generally a good idea to use tox to run tests against python 2.6 and 2.7 since helga supports both of those versions.

setuptools and entry_points

Not only does a plugin’s setup.py file declare project information and allow it to be installed with pip, it is also how helga loads plugins at runtime. To do this, helga uses a setuptools feature known as entry_points. To understand how to use this, take the above project structure as an example. Let’s say that the contents of helga_my_plugin.py looks like this:

from helga.plugins import match

@match(r'foo')
def foo(client, channel, nick, message, matches):
    return u'{0} said foo'.format(nick)

A basic setup.py file for this project might look like:

from setuptools import setup, find_packages

setup(
    name='helga_my_plugin',
    version='0.0.1',
    description='A foo plugin',
    author="Jane Smith"
    author_email="jane.smith@example.com",
    packages=find_packages(),
    py_modules=['helga_my_plugin'],
    include_package_data=True,
    zip_safe=True,
    entry_points=dict(
        helga_plugins=[
            'my_plugin = helga_my_plugin:foo',
        ],
    ),
)

Before talking about entry_points, take note of some other important lines.

py_modules
If your plugin is a single python file, you will need include it without the ‘.py’ extension in a string list.
include_package_data
If you intend on including files specified in a MANIFEST.in file, you will need to set this to True.

Now, let’s talk about the entry_points line. The helga plugin loader will look for any installed python package that declares helga_plugins entry points. These should be list of strings of the form:

plugin_name = module.path:decorated_function

The ‘plugin_name’ portion should be a simple name for the plugin, such as ‘my_plugin’ in the ‘helga_my_plugin’ example above. The latter half must be colon delimited containing a module path and the function decorated using @command, @match, or @preprocessor. So if a file helga_my_plugin.py contains:

from helga.plugins import match

@match(r'foo')
def foo(*args):
    return 'foo'

the entry point would be helga_my_plugin:foo. For more information and details on how entry points work, see the entry_points documentation.

Distributing Plugins

The preferred distribution channel for helga plugins is PyPI so that plugins can be installed using pip. Once you have properly packaged your plugin, submit it to PyPI:

$ python setup.py sdist register upload

Using A Project Template

If you use cookiecutter for managing project templates, there is a third-party helga plugin cookiecutter template here: https://github.com/bigjust/cookiecutter-helga-plugin

Installing Plugins

If plugins are properly packaged and distributed according to Packaging and Distribution, then any new plugins for helga to use can be installed using pip. If helga has been installed into a virtualenv as mentioned in Getting Started, activate that virtualenv prior to installing the new plugin:

$ source bin/activate
$ pip install helga-my-plugin

Note, however, that you will need to full restart any running helga process in order to use the new plugins. This behavior may change in future versions of helga. If a plugin is not distributed using PyPI, but is available via some source repository, you can still install it with a little more work:

$ source bin/activate
$ git clone git@example.com:janedoe/helga-my-plugin.git src/helga-my-plugin
$ cd src/helga-my-plugin
$ python setup.py develop

Note, that installing a plugin will mean that it will be loaded when helga starts unless it is not included in the plugins whitelist helga.settings.ENABLED_PLUGINS or it is listed in the plugins blacklist helga.settings.DISABLED_PLUGINS The default behavior is that all plugins installed on the system are loaded and made available for use in IRC.

With this in mind, installed plugins are available for use, but they may not immediately be so. Helga maintains a list of plugin names that indicate which plugins should be enabled by default in a channel, which is configured via helga.settings.DEFAULT_CHANNEL_PLUGINS. If a plugin name does not appear in this list, a user in a channel will not be able to use it until it is enabled with the manager plugin:

<sduncan> !plugins enable my_plugin

Class-Based Plugins

All of the above documentation for creating plugins makes use of helga’s simple decorator API. Generally speaking, the decorator API is the preferred way of authoring plugins. However, simple decorated functions may not be robust enough for all plugins. For this reason, there is a class-based API that can be used instead. In fact, this is what is used behind the scenes for the decorator API.

Base Plugin Class

At a high level, plugin objects should be some form of a sublass of helga.plugins.Plugin:

class helga.plugins.Plugin(priority=50)

The base class for helga plugins. There are three main methods of this base class that are important for creating class-based plugins.

preprocess

Run by the plugin registry as a preprocessing mechanism. Allows plugins to modify the channel, nick, and/or message that other plugins will receive.

process

Run by the plugin registry to allow a plugin to process a chat message. This is the primary entry point for plugins according to the plugin manager, so it should either return a response or not.

run

Run internally by the plugin, generally from within the process method. This should do the actual work to generate a response. In other words, process should handle checking if the plugin should handle a message and then return whatever run returns.

preprocess(client, channel, nick, message)

A preprocessing filter for plugins. This allows a plugin to modify a received message prior to that message being handled by this plugin’s or other plugin’s process method.

Parameters:
  • client – an instance of helga.comm.irc.Client or helga.comm.xmpp.Client
  • channel – The channel from which the message was received. This could be a public channel like ‘#foo’, or in the event of a private message, could be the nick of the user sending the message
  • nick – The nick of the user sending the message
  • message – The full message string received from the server
Returns:

a three-tuple (channel, nick, message) containing any modifications

process(client, channel, nick, message)

This method of a plugin is called by helga’s plugin registry to process an incoming chat message. This should determine whether or not the plugin run method should be called. If so, it should return whatever return value run generates. If not, None should be returned.

Parameters:
  • client – an instance of helga.comm.irc.Client or helga.comm.xmpp.Client
  • channel – The channel from which the message was received. This could be a public channel like ‘#foo’, or in the event of a private message, could be the nick of the user sending the message
  • nick – The nick of the user sending the message
  • message – The full message string received from the server
Returns:

None if the plugin should not run, otherwise the return value of the run method

run(client, channel, nick, message, *args, **kwargs)

Executes this plugin with a given message to generate a response. This should run without regard to whether it should or should not for a given message. Note, that this is where the actual work for the plugin should occur. Subclasses should implement this method.

A return value of None, an empty string, or empty list implies that no response should be sent via chat. A non-empty string, list of strings, or raised ResponseNotReady implies a response to be sent.

Parameters:
  • client – an instance of helga.comm.irc.Client or helga.comm.xmpp.Client
  • channel – The channel from which the message was received. This could be a public channel like ‘#foo’, or in the event of a private message, could be the nick of the user sending the message
  • nick – The nick of the user sending the message
  • message – The full message string received from the server
Returns:

None if no response is to be sent back to the server, a non-empty string or list of strings if a response is to be returned

Plugin implementations can subclass this base class directly, but there are convenience subclasses for each plugin type that already do a lot of the heavy lifting.

Command Subclasses

To create a class-based command plugin, subclass helga.plugins.Command. For example:

from helga.plugins import Command

class FooCommand(Command):
    command = 'foo'
    aliases = ['f']
    help = 'Return the foo count. Usage: helga (foo|f)'

    def __init__(self, *args, **kwargs):
        super(FooCommand, self).__init__(*args, **kwargs)
        self.foo_count = 0

    def run(self, client, channel, nick, message, cmd, args):
        self.foo_count += 1
        return u'Foo count is {0}'.format(self.foo_count)

Match Subclasses

To create a class-based match plugin, subclass helga.plugins.Match. For example:

from helga.plugins import Match

class FooMatch(Match):
    pattern = r'foo (\w+)'

    def run(self, client, channel, nick, message, matches):
        return u"{0} said 'foo' followed by '{1}'".format(nick, matches[0])

Or in the case of using a callable as a pattern:

import time

from helga.plugins import Match

class FooMatch(Match):

    def __init__(self, *args, **kwargs):
        super(FooMatch, self).__init__(*args, **kwargs)
        self.pattern = self.match_foo

    def match_foo(self, message):
        if 'foo' in message:
            return time.time()

    def run(self, client, channel, nick, message, matches):
        return u"{0} said 'foo' at {0}".format(nick, matches)

Preprocessor Subclasses

There is no direct Plugin subclass for preprocessor plugins. Preprocessors using the decorator API are merely instances of the base Plugin class (see Base Plugin Class). However, to create a preprocessor plugin using a class-based approach:

from helga.plugins import Plugin

def FooPreprocessor(Plugin):

    def preprocess(self, client, channel, nick, message):
        # Ignore anything from nicks that start with a vowel
        if nick[0] in 'aeiou':
            return channel, nick, u''
        return channel, nick, message

Packaging Class-Based Plugins

Class-based plugins are packaged in exactly the same manner as those using the decorator API (see Packaging and Distribution). The only difference is with respect to entry points. Whereas with decorator plugins, the entry point follows a ‘module:function’ pattern, class-based plugins follow a ‘module:class’ pattern. For example, given this plugin in a file helga_foo.py:

from helga.plugins import Command

class FooCommand(Command):
    pass

The respective entry point string might look something like this:

foo = helga_foo:FooCommand

Supporting XMPP

You shouldn’t need to make any special changes to plugins if you follow the documenation above. However, remember that helga was started as an IRC bot, so things work a bit more to that favor. Plugins will still receive client, channel, nick, and message arguments.

Note, though, that values for channel will never be the full JID of a chat room. Instead, they will be the user portion of the room JID, prepended with a ‘#’. For example:

bots@conf.example.com

would become a channel named #bots and private messages from:

user@host.com

would become a channel named user.

Nick values operate in a similar manner, only using the resource portion of the JID for group chat. For example:

bots@conf.example.com/foo

would become a nick named:

foo

and a private message from:

foo@host.com

would become a nick named:

foo

For more information about how this works see helga.comm.xmpp.Client.parse_channel() and helga.comm.xmpp.Client.parse_nick().