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 ^^^^^^^^^^^^^^^ .. highlight:: text Before we can write a plugin, we need a :ref:`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 :kbd:`Control-C` or :kbd:`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) 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 ^^^^^^^^^^^^^^^^^^^^^^^ .. highlight:: python 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 *Processor*\ s. 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 :func:`@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: .. code-block:: text 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, :func:`@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 :class:`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 ``(?: )``. :mod:`ibid.utils` contains many handy helper functions. :func:`human_join() ` is the equivalent of ``u', '.join()``, with an "and" before the last item. :meth:`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 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 :attr:`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)) :class:`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: .. code-block:: ini [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 (:func:`random.choice` is good for that). Next Steps ---------- That's it, you are now more than able to write your own Ibid plugins. Please :ref:`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. .. vi: set et sta sw=3 ts=3: