Tutorial: Writing a Kibot Module

One of the primary design goals of kibot is make modules easy to write. The bot itself is really just a framework upon which to hang modules. This document is a tutorial showing how to use the basic tools that are available. It is not an exhaustive reference of all of the available tools. There are (or will be) other documents for that, and this tutorial refers to them when possible.

Your first legal module

A kibot module is a python class instance. When asked to load a module of name FOO, the bot will look (in the bot module load path) for a python module named FOO.py (or FOO.pyc or FOO.pyo). If it finds one, it will load that python module, and look for a class named FOO within it. An instance of that class will be created, with one argument passed in, the bot object itself. The simplest module conceivable, then, is this:

# in a file called FOO.py
class FOO:
    def __init__(self, bot):
        self.bot = bot

You might want to derive your module from the class BaseModule, which provides (among other things) a basic __init__ which does this and a few other things for you. So you could also do:

# in a file called FOO.py
import kibot.BaseModule
class FOO(kibot.BaseModule.BaseModule):
    pass

Once you have your module written, place it in the bot's module path (the module path and other configuration values are dumped to the log on startup). The standard place to put your own modules is in the modules subdirectory of the base directory. Once you've placed the module in the module path, you can ask the bot to load your new module. You do this by typing botnick: load FOO on a public channel or just load FOO via a private /msg. Be sure to watch the log. If there are any problems, the python tracebacks will be written there.

Defining commands

So, you want your module to have some commands, eh? A command is simply an instance method. These methods must take one argument, an instance of Command Let's make your first command.

class FOO(kibot.BaseModule.BaseModule):
    _hello_cperm = 1 # we'll discuss this shortly
    def hello(self, cmd):
        cmd.reply('hello')

This command will be called when anyone says hello to the bot, either by typing botnick: hello in a channel or msg-ing hello privately. The command simply responds by saying hello back to them via the same mechanism. If you prefer different reply behavior, there are other methods available. There are a few other attributes of the Command object that you may find useful:

class FOO(kibot.BaseModule.BaseModule):
    _hello_cperm = 1 # we'll discuss this shortly
    def hello(self, cmd):
        if cmd.channel: # will be None if command was /msg'ed
            cmd.reply('hello %s and the denizens of %s' 
                % (cmd.nick, cmd.channel))
        else:
            cmd.reply('hello %s' % cmd.nick)

There is also cmd.cmd, which is the command as typed, which could be hello or FOO.hello or almost anything else if an alias is defined. There is also cmd.args, which is the full string that was typed after the command. There is a convenience method .asplit(), which returns a split (on whitespace) list of the arguments.

a = cmd.asplit()
  # is equivalent to
a = string.split(cmd.args)

There's also cmd.shsplit(), which does shell-style splitting that respects quotes and backslashes. Each of these respects the maxsplit keyword.

Methods of your class that begin with an underscore are considered private, and will not be listed in the help or be callable even if you provide a _cperm attribute. The one "exception" is __call__. See the google module for an example of how to use __call__. The one problem with __call__ is that the documentation doesn't work as nicely.

Permissions

As you may have guessed, the _hello_cperm attribute defines the command permission for this command. If it is the integer 1, than anyone can execute the command. If it is 0, None or not present, the command is completely forbidden. If it is a string, the corresponding user perm will be required. It's actually quite powerful, but I won't get into the details here. There's another document that addresses it extensively.

Documentation

The bot grabs the documentation from the doc strings of both the class and methods. In each case the first line will be used as the short version of the documentation, and further lines will be printed only when you ask for help on that specific object. Example:

class FOO(kibot.BaseModule.BaseModule):
    """a silly little example
    This module doesn't do much but say hello"""

    _hello_cperm = 1 # we'll discuss this shortly
    def hello(self, cmd):
        """respond with 'hello'
        hello
        this command is really pretty dumb :)"""
        cmd.reply('hello')

The second line of function help is normally usage (not so obvious in this case).

Logging

Kibot supports centrallized logging for the benefit of both the module author and the bot owner. The bot owner can have multiple log destinations (usually files) each with its own log threshold. The log threshold determines how much stuff gets written to it. A low log threshold means that only very important information gets written, whereas a high threshold means that tons of information can be written.

As a module auther, you must decide what to log, and at what log level to log it. A higher log level corresponds to a less important log. In case you hadn't guessed, a log message's level gets compared to a log destination's threshold to determine whether or not it should be written there.

Here is a command that does nothing but record what is passed in to the log file with a level of 2. Please use some sort of prefix for all logs. A reasonable convention is to begin all log messages with the module name in caps. Remember that all modules and core bot functions will be logging to the same place, so a log such as bad argument will not be very useful unless you provide some context.

class FOO(kibot.BaseModule.BaseModule):
    _log_cperm = 1
    def log(self, cmd):
        """write a message to the log files
        log <message>"""
        self.bot.log(2, 'FOO.log: %s' % (cmd.args, ))

Here are some rough guidelines about what level you should use for log messages. Examples of when the bot itself uses these levels are provided.

LevelDescription
0 very important message, fatal error, something is broken
1 major events (modules loaded/unloaded)
2 important but normal events (handlers and timers are set/unset)
3 the day-to-day stuff (commands, op-ing and inviting users)
5 most of the instructions the bot is given (events, timers)
7 handy information for writing modules (raw traffic to/from the server)
9 stuff that's useful only when something is wrong (entry/exit of all commands)

Communicating directly with the server

If you want to do more complex interactions, you can use attributes of the bot object. For example, lets build a kick command.

class FOO(kibot.BaseModule.BaseModule):
    _kick_cperm = 1 # probably not a good idea
    def kick(self, cmd):
        """kick one or more users
        kick [nicks]"""
        for nick in cmd.asplit():
            # bot.conn is the ServerConnection object
            bot.conn.kick(nick)

Most standard irc commands are implemented in the connection object. If we want to do some simple checking first, we can consult the bot's irc database, where it keeps track of users:

class FOO(kibot.BaseModule.BaseModule):
    _kick_cperm = 1 # probably not a good idea
    def kick(self, cmd):
        """kick one or more users
        kick [nicks]"""
        if not cmd.channel:
            cmd.reply('ask me on a channel')
            return
        
        chan_obj = cmd.bot.ircdb.channels[cmd.channel]
    
        if not chan_obj.is_oper(cmd.bot.nick):
            cmd.reply("I don't have ops on %s" % cmd.channel)
            return

        for nick in cmd.asplit():
            if chan_obj.has_user(nick):
                bot.conn.kick(nick)
            else:
                reply("there's no %s on %s" % (nick, cmd.channel))

You might be tempted to keep some of these handy objects around by saving them as self.chan_obj or something. Do not do that! The only safe object is the bot object itself. The reason is that almost any object can be reloaded dynamically. For example, the ircdb object might be reloaded, and if your module has stored a reference locally, you'll have the old version. Always access them via self.bot object. You can make convenience references for use within a single function call (as we did above) but don't keep them around.

Loading and unloading properly

You need to think about loading and unloading if you have handlers, timers or stored data. It's pretty easy. Loading is done in __init__ (duh), and when your module is un-loaded, the method _unload(self) will be called (if it's defined).

Setting handlers

Handlers are functions that will be called upon irc "events", which are typically lines received from the server. Handlers take two arguments: the connection object (which will usually be the same as bot.conn) and an Event object.

Lets say you want to welcome anyone who joins a channel. You might write the following handler:

from kibot.irclib import nm_to_n
class FOO(kibot.BaseModule.BaseModule):
    def _on_join(self, c, e):
        channel = e.target
        nick = nm_to_n(e.source)
        c.privmsg(channel, 'welcome to %s, %s' % (channel, nick))

You need to set the handler so that it gets called on "join"s. You do this by calling:

self.bot.set_handler('join', self._on_join, 5)

This sets _on_join to be called when 'join' events occur. This handler is given priority 5. Lower values mean handlers will be called sooner (if there are multiple handlers for a given event). You should not use a value lower than 0. Negative values are for internal bot handlers, like keeping the irc database updated. This rule is not enforced, but you can screw stuff up or (more likely) confuse yourself.

If a handler returns the string NO MORE or raises the exception StopHandlingEvent (defined in kibot.m_irclib) then no more handlers will be called for that event. Make sure you know what you're doing if you do this. Normal commands (like the ones we wrote above) are called from an internal priority 20 handler, so it is possible to take action before a command is received.

You can delete a handler with the command

self.bot.del_handler('join', self._on_join, 5)

The priority is ignored, but accepted. For convenience, the BaseModule class provides the functions:

def _set_handlers(self, priority=0, prefix="_on_"): ...
def _del_handlers(self, priority=0, prefix="_on_"): ...

If you call these, they will set or remove handlers for all functions with the prefix _on_. You can alternatively, provide a list of handlers by setting the class attribute _handlers. For example:

_handlers = ['join']

Be sure to delete any set handlers in _unload.

Few events are well documented. This is mildly annoying, but frankly, there's an easier way to learn about events than reading about them. The best way to program handlers is to run a bot at a high log level (between 5 and 10) and generate the event. For example, if you want to see how to deal with a DCC request, run your bot at log level 10 and send a DCC CHAT request. You should see this roll in:

FROM SERVER: :nick!~username@host.domain.com PRIVMSG botnick :DCC CHAT chat 3287712632 35715
EVENT: type: ctcp_dcc, source: nick!~username@host.domain.com, target: botnick, args: ['CHAT chat 3287712632 35715']
EVENT: type: ctcp, source: nick!~username@host.domain.com, target: botnick, args: ['DCC', 'CHAT chat 3287712632 35715']

In this case, two events are generated: one generic ctcp event, and one specific ctcp_dcc event. You can choose which you want to deal with depending on your task. The advantage of this approach is that you can see exactly what the event looks like. There is one attribute of the Event class that is not printed in the logs. There is a raw object which (in most cases) contains the raw line that generated the event. That line is shown in the logs above on the FROM SERVER: line. Some special bot-generated events, such as command_not_found, permission_denied and a few others include some other data structure in the raw attribute.

Setting timers

Timers can be used to perform delayed or repeated actions. Timers are by creating instances of the Timer class.

self.timer = Timer(seconds, func, args=(), kwargs={}, fromnow=1, repeat=None)

and registered with

self.bot.set_timer(self.timer)

The function func will be called after seconds seconds. If fromnow == 0, then seconds will be interpreted as a unix time. The function will be passed args and kwargs. If repeat is a number, func will be repeated at that interval (in seconds).

Repeating timers should return 0 or 1. A return of 1 means that the timer should continue to repeat. If it returns 0, it will be terminated. Timers that are not repeated (or returned 0) do not need to be removed, but they can be. Repeating timers should be removed on _unload just like handlers. Timers can be removed by calling

self.bot.del_timer(self.timer)

Storing data

Data can be stored easily by using stashers. A Stasher is a python object that acts like a dict, but transparently stores the data to disk. You can interact with them directly, but the easiest thing to do is to use some of the convenience methods and attributes defined in kibot.BaseModule. Here is an example that demonstrates the use of stashers for storing users' email addresses.

import kibot.BaseModule
class addressbook(kibot.BaseModule.BaseModule):
    _stash_format = 'repr'     # format for the file
    _stash_attrs = ['email']   # which attributes to load/save
    def __init__(self, bot):
        self.bot = bot
        self.email = {}
        # if the attribute wasn't in the file, or the file didn't exist,
        # then it won't do anthing.  Otherwise it will overwrite the
        # default we just set.
        self._unstash() # load attributes listed in _stash_attrs

    def _unload(self):
        self._stash()   # forces a save

    _setemail_cperm = 1
    def setemail(self, cmd):
        nick, email = cmd.asplit(1)
        self.email[nick] = email
        # self._stash() # you may want to force a save here, too

    _getemail_cperm = 1
    def getemail(self, cmd):
        try:
            email = self.email[cmd.args]
        except KeyError, e:
            cmd.reply("I don't have an email address for %s" % cmd.args)
        else:
            cmd.reply(email)

There are a number of noteworthy things going on here. First, data will be stored in a file named addressbook.repr in the bot's data directory. The name is automatically grabbed from the module name and format. There are three formats available: repr, pickle, and shelve. They are named after their respective python tools. Each has its own advantages and disadvantages.

repr
The repr format uses a text representation of the data, which makes it very easy to view and edit by hand. It is relatively large and slow becuase all data must be rewritten for each change. It is also somewhat restrictive about what types of data may be included. Class instances may not be storable, although an arbitrary nesting of dicts, lists, strings, etc. are fine. This is recommended for small, simple data sets that you may want to view/edit by hand.
pickle
This format pickles the entire structure and writes it on each save. It is the smallest format, and most flexible about data types. This is recommended for small complex data types.
shelve
This format uses a db to store the key/value pairs from the dict. The values are pickled first. This can be bulky for small data sets, but can be efficient for larger or more frequently accessed data sets because it need not rewrite the whole data set for a single modification. This is recommended for large data sets.

Note that each attribute in _stash_attrs will be one key in the Stasher object. Therefore, if your email list grows to thousands of people, you do not really gain the benefit that you'd like from shelve. To do that, you should get a Stasher object directly, and make each nick/email pair its own key/value pair in the Stasher. This is not discussed in this document, but an example can be found in kibot.ircDB.ircDB for the self.known attribute.

Managing module settings

You may need to be able to dynamically set module settings. A setting is simply an attribute of the module that can be read and written via the commands base.get and base.set. Here's an example:

import kibot.BaseModule
from kibot.Settings import Setting
class arithmetic(kibot.BaseModule.BaseModule):
    _settings = [
        Setting('base', 10, 'the base in which to return answers')
        ]

    _add_cperm = 1
    def add(self, cmd):
        nums = cmd.asplit()
        sum = 0
        for i in nums:
            try: sum += int(i)
            except: pass
        cmd.reply(self._stringify(sum))

    def _stringify(self, num):
        nums = '0123456789abcdefghijklmnopqrstuvwxyz'
        base = int(self.base)
        st = ''
        num = int(num)
        while sum != 0:
            num, rmd = num / base, num % base
            st = nums[rmd] + st
        return st

You can now give commands to the bot to set and get the current base. These commands must be given with the full path, as in set arithmetic.base 16.

As used above, the attribute self.base will always be set to a string. If you want to do something more complex, you can provide other arguments to the constructor Setting. See the Settings documentation for more details. In this case, you probably want the conv_func, which is passed the raw string as typed by the user, and returns the converted value.

class arithmetic(kibot.BaseModule.BaseModule):
    _settings = [
        Setting('base', 10, 'the base in which to return answers',
                conv_func=int)
        ]

Settings are not automatically stashed, but they are treated like any other attributes. Simply include the setting name in _stash_attrs to have them stashed.

In the examples above, we have not overridden BaseModule's __init__. If you do, you should probably do

    self._settings = init_settings(self, self._settings)
within your __init__. This is where the default values get set.

Kibot structure

If you need to do more complex things, you may need to know a little more about the structure of the bot and some of its common objects. Here is a heirarchical view of objects you're likely to need and their public attributes.

Troubleshooting

If you're having trouble getting something to work, be sure to turn logging up to at least 5 so you can see all of the events.