Ibid Plugin Tutorial

This will guide you through the process of creating an Ibid plugin so you can add your own features.

Getting Started

Install an Ibid

Before we can write a plugin, we need a working base Ibid install.

The Testing Environment

Rather than working on a live bot and having to reload modules a lot, when developing for Ibid, we usually use ibid-plugin, a minimal, fast, testing environment.

It looks like this:

user@box ~/botdir $ ibid-plugin
... Messages about loading plugins
Query: hello
Response: Huh?
Query: help
Response: I can help you with: looking things up.
Ask me "help me with ..." for more details.
Query: help me with looking things up
Response: I use the following features for looking things up: help
Ask me "how do I use ..." for more details.

To exit, press Control-C or Control-D.

As you can see, there is almost nothing loaded. It can’t even respond to “hello”, the code for that is in the factoid module. If you want to load the factoid module, you can tell ibid-plugin to load it on startup, by adding it as a parameter:

user@box ~/botdir $ ibid-plugin factoid
... Messages about loading plugins
Query: hi
Response: good morning

Well, actually there are some administrative functions, which don’t show up in the overall help. You could have asked the bot to “load factoid”:

Query: help admin
Response: I use the following features for administrative functions:
config, core, die, help, plugins, sources and version
Ask me "how do I use ..." for more details.
Query: how do I use plugins
Response: Lists, loads and unloads plugins. You can use it like this:
  list plugins
  (load|unload|reload) <plugin|processor>
Query: load factoid
DEBUG:core.reloader:Loading Processor: factoid.Forget
DEBUG:core.reloader:Loading Processor: factoid.Get
DEBUG:core.reloader:Loading Processor: factoid.Modify
DEBUG:core.reloader:Loading Processor: factoid.Search
DEBUG:core.reloader:Loading Processor: factoid.Set
DEBUG:core.reloader:Loading Processor: factoid.StaticFactoid
DEBUG:core.reloader:Loading Processor: factoid.Utils
DEBUG:core.reloader:Loaded factoid plugin
Response: factoid reloaded
Query: hi
Response: howsit

Try talking to it too fast, it’ll start ignoring you. This makes sense for a real chat channel, but not debugging. You can tell the ignorer not to load by adding it as a parameter followed by -:

user@box ~/botdir $ ibid-plugin factoid core.Ignore-

If you want all the normal modules loaded, you can add the -c option, but it’ll take quite a bit longer to start up:

user@box ~/botdir $ ibid-plugin -c
... Screenfulls of messages
Query: hello
Response: sup

Play with that a bit. It isn’t exactly the same as a full bot, there are a few things that won’t work, but it’s good enough for testing. Some examples:

  • Karma, because it’s disabled for private conversations by default. You can switch to public mode with -p.
  • The games, because they require some advanced Twisted functionality (as well as other channel members).

Ibid Theory

Ibid is divided into two main parts (excluding the Ibid core code): Sources and Plugins.

The sources speak IRC, Jabber, e-Mail, etc. There is one source for each network that the bot is connected to. When someone says something in an IRC channel, the IRC source for that network will create an Event. The event is passed to the plugins, which each take a turn to look at it and decide if they want to do anything. If a plugin decides to reply, the event is sent back to the source to dispatch the reply.

Events are also used for, private messages from users to the bot, people joining and leaving channels, etc. but most plugins don’t need to deal with anything except message events, directed to the bot.

Ibid comes with some plugins for pre- and post-processing of events (such as logging), and some for features.

Plugin Writing Time

Processors and Handlers

Let’s see what that looks like in practice. Here’s a simple hello world plugin. Create a directory called ibid/plugins in the botdir. In that directory, create a file called tutorial.py with the following contents:

from ibid.plugins import Processor, handler

class HelloWorld(Processor):
   @handler
   def hello(self, event):
      event.addresponse(u'Hello World!')

A plugin can contain multiple Processors. Each one is a self-contained part of the event handling chain. It can register an interest in certain types of event, or a specific place in the chain, but for most plugins the defaults are sufficient.

Inside the processor, any functions decorated with @handler will get a chance to look at the event. If it choses to add a response to the event, the response will be returned to the user.

Note

Ibid uses unicode strings and to catch mistakes, you’ll get a warning if you pass a normal string as a response, so try to get in the habit of using unicode.

Test it out, anything you say to the bot should provoke a “Hello World!” response:

user@box ~/botdir $ ibid-plugin tutorial
... Messages about loading plugins
Query: hello
Response: Hello World!

Now, you could include code inside your handler to determine if you want to reply to a message or not, but must of the time you are after messages that look like something particular, so we have another decorator, @match(), to help you:

from ibid.plugins import Processor, match

class HelloWorld(Processor):
    @match(r'^hello$')
    def hello(self, event):
        event.addresponse(u'Hello World!')

Match takes a regular expression as a parameter, and will only run your handler function if the regex matches the event’s message. In this case, it’ll only fire if you say “hello”. It’ll ignore trailing punctuation and whitespace, as that’s removed by the core.Strip plugin.

Match Groups

Time for a more complex example, a multiple dice roller, you can add it as another Processor in your tutorial plugin:

from random import randint

from ibid.plugins import Processor, match
from ibid.utils import human_join

class Dice(Processor):
    @match(r'^roll\s+(\d+)\s+dic?e$')
    def multithrow(self, event, number):
        number = int(number)
        throws = [unicode(randint(1, 6)) for i in range(number)]
        event.addresponse(u'I threw %s', human_join(throws))

If you still have an ibid-plugin open you can “reload tutorial” to reload your plugin.

Any match groups you put in the regex will be passed to the handler as arguments, in this case the number of dice to throw. If you want brackets without creating a match group, you can use the non-grouping syntax (?: ).

ibid.utils contains many handy helper functions. human_join() is the equivalent of u', '.join(), with an “and” before the last item.

addresponse() takes a second argument for string substitution. If you want to substitute multiple items, use the dict syntax:

event.addresponse(u'Nobody %(verb)s the %(noun)s!', {
    'verb': u'expects',
    'noun': u'Spanish Inquisition',
})

Documentation

At the moment you’ll see that your plugin doesn’t appear in the help system. You can fix that with a little more code:

from random import randint

from ibid.plugins import Processor, match
from ibid.utils import human_join

features = {}

features['dice'] = {
    'description': u'Throws multiple dice',
    'categories': ('fun',),
}

class Dice(Processor):
    usage = u'roll <number> dice'

    features = ('dice',)

    @match(r'^roll\s+(\d+)\s+dic?e$')
    def multithrow(self, event, number):
        number = int(number)
        throws = [unicode(randint(1, 6)) for i in range(number)]
        event.addresponse(u'I threw %s', human_join(throws))

The module-level features dict specifies descriptions for features (given with the usage description) and categories to place the feature in. You can find a list of available categories in ibid.categories and if necessary add a category to it from your module.

The Processor can be linked to a feature by specifying it in the features attribute. Usage for the Processor’s functions (in BNF) goes in a usage attribute. “reload tutorial” and you should see “dice” appear in the features for “fun stuff”.

Configuration

Ibid has a configuration system that may be useful for your plugin. Configuration values can be set at runtime or by editing ibid.ini.

Let’s make the number of dice sides be configurable:

from random import randint

from ibid.config import IntOption
from ibid.plugins import Processor, match
from ibid.utils import human_join

class Dice(Processor):
    sides = IntOption('sides', 'Number of sides to each die', 6)

    @match(r'^roll\s+(\d+)\s+dic?e$')
    def multithrow(self, event, number):
        number = int(number)
        throws = [unicode(randint(1, self.sides)) for i in range(number)]
        event.addresponse(u'I threw %s', human_join(throws))

IntOption() creates a configuration value called plugins.tutorial.sides with a default value of 6. There are also configuration helpers for other data types.

If you merge the following into your ibid.ini, you can change to 21 sided dice:

[plugins]
   [[tutorial]]
      sides = 21

Style

Now that you’ve got all the basics, here are some other things you should know about writing Ibid plugins.

Error Handling

You might have noticed that we haven’t said anything about error handling. That was intentional. All exceptions in plugins are caught at the dispatcher level, and an appropriate response will be returned to the user, as well as tracebacks logged. The only time you should worry about handling errors is if you can recover gracefully or you want to return a specific response (such as an explanation).

Responses

The general Ibid style is that the bot should be something people can relate to, not too mechanical. So many Ibid responses are playful and maybe a little snarky. Also, many responses aren’t static, but rather chosen from a list of 3 or 4 at random (random.choice() is good for that).

Next Steps

That’s it, you are now more than able to write your own Ibid plugins. Please send us anything you write, it may be useful for other people too.

We wished there was more documentation we could point you at, to help you, but it hasn’t been written yet. So, read some modules to see what’s there, and stick your nose in our IRC channel for help.