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.
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.
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.
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.
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).
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.
Level | Description |
---|---|
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) |
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.
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).
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
or
raises the exception NO MORE
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
. You can
alternatively, provide a list of handlers by setting the class
attribute _on_
_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:
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.
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)
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
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
dict
s, list
s, strings, etc. are
fine. This is recommended for small, simple data sets that you
may want to view/edit by hand.
pickle
pickle
s 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
dict
. The values are pickle
d 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.
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.
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.
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.