This commit is contained in:
Jeremy Fincher 2004-02-19 07:21:07 +00:00
parent c563559b29
commit 8b20ad77fa
1 changed files with 53 additions and 115 deletions

View File

@ -1,19 +1,19 @@
Ok, so you want to write a callback for supybot. Good, then this is
Ok, so you want to write a callback for Supybot. Good, then this is
the place to be. We're going to start from the top (the highest
level, where supybot code does the most work for you) and move lower
level, where Supybot code does the most work for you) and move lower
after that.
So have you used supybot? If not, you need to go use it, get a feel
So have you used Supybot? If not, you need to go use it, get a feel
for it, see how the various commands work and such.
So now that we know you've used supybot, we'll start getting into
So now that we know you've used Supybot, we'll start getting into
details.
First, the easiest way to start writing a module is to use the wizard
provided, scripts/newplugin.py. Here's an example session:
-----
functor% scripts/newplugin.py
functor% supybot-newplugin
What should the name of the plugin be? Random
Supybot offers two major types of plugins: command-based and regexp-
based. Command-based plugins are the kind of plugins you've seen most
@ -48,7 +48,7 @@ is available as examples/Random.py):
#!/usr/bin/env python
###
# Copyright (c) 2002, Jeremiah Fincher
# Copyright (c) 2004, Jeremiah Fincher
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
@ -82,18 +82,19 @@ Add the module docstring here. This will be used by the setup.py script.
import plugins
import conf
import utils
import privmsgs
import callbacks
def configure(onStart, afterConnect, advanced):
# This will be called by setup.py to configure this module. onStart and
# afterConnect are both lists. Append to onStart the commands you would
# like to be run when the bot is started; append to afterConnect the
# commands you would like to be run when the bot has finished connecting.
def configure(advanced):
# This will be called by setup.py to configure this module. Advanced is
# a bool that specifies whether the user identified himself as an advanced
# user or not. You should effect your configuration by manipulating the
# registry as appropriate.
from questions import expect, anything, something, yn
onStart.append('load Random')
conf.registerPlugin('Random', True)
class Random(callbacks.Privmsg):
pass
@ -110,7 +111,7 @@ You'll probably want to change the copyright notice to be your name.
It wouldn't stick even if you kept my name, so you might as well :)
Describe what you want the plugin to do in the docstring. This is
used in scripts/setup.py in order to explain to the user the purpose
used in supybot-wizard in order to explain to the user the purpose
of the module. It's also returned when someone asks the bot for help
for a given module (instead of help for a certain command). We'll
change this one to "Lots of stuff relating to random numbers."
@ -120,12 +121,12 @@ you're given subclasses callbacks.Privmsg) but the privmsgs module
isn't used. That's alright; we can almost guarantee you'll use it, so
we go ahead and add the import to the template.
Then you see a "configure" function. This the function that's called
when users decide to add your module in scripts/setup.py. You'll note
that by default it simply adds "load Example" (where 'Example' is the name you
provided as the name of your plugin, so in our case it is "load Random") at
the bottom. For many plugins this is all you need; for more complex plugins,
you might need to ask questions and add commands based on the answers.
Then you see a "configure" function. This is the function that's
called when users decide to add your module in supybot-wizard. You'll
note that by default it simply registers the plugin to be
automatically loaded on startup. For many plugins this is all you
need; for more complex plugins, you might need to ask questions and
add commands based on the answers.
Now comes the meat of the plugin: the plugin class.
@ -159,7 +160,7 @@ self, of course). There's no way anything will ever get to them! If
you have some sort of initial values you need to get to your plugin
before it can do anything interesting, add a command that gets those
values. By convention, those commands begin with "start" -- check out
the Relay and Enforcer plugins for examples of such commands.
the Relay plugin for an example of such a command.
There's an easier way to get our plugin to have its own rng than to
define an __init__. Plugins are unique among classes because we're
@ -184,7 +185,7 @@ explanation of what each part means.
Returns the next random number generated by the random number
generator.
"""
irc.reply(msg, str(self.rng.random()))
irc.reply(str(self.rng.random()))
And that's it! Pretty simple, huh? Anyway, you're probably wondering
what all that *means*. We'll start with the def statement:
@ -205,8 +206,8 @@ whitespace -- the work has already been done for you). You can read
about the Irc object in irclib.py (you won't find .reply or .error
there, though, because you're actually getting an IrcObjectProxy, but
that's beyond the level we want to describe here :)). You can read
about the msg object in ircmsgs.py. But again, aside from calling
irc.reply or irc.error, you'll very rarely be using these objects.
about the msg object in ircmsgs.py. But again, you'll very rarely be
using these objects.
(In case you're curious, the answer is yes, you *must* name your
arguments (self, irc, msg, args). The names of those arguments is one
@ -225,29 +226,21 @@ the supybot framework is that it's easy to write complete commands
with help and everything: the docstring *IS* the help! Given the
above docstring, this is what a supybot does:
<angryman> jemfinch: random takes no arguments (for more help
use the morehelp command)
<jemfinch> $morehelp random
<angryman> jemfinch: Returns the next random number from the
current random number generator.
<jemfinch> @help random
<angryman> jemfinch: (random takes no arguments) -- Returns the
next random number from the random number generator.
'help <command>' replies with the command name followed by the first line of
the command's docstring; there should be a blank line following, and then
'morehelp <command>' will reply with the remainder of the docstring. So that
explains the docstring. Now on to the actual body of the function:
Now on to the actual body of the function:
irc.reply(msg, str(self.rng.random()))
irc.reply(str(self.rng.random()))
irc.reply takes two arguments, an IrcMsg (like the one passed into
your function) and a string. The IrcMsg is used to determine who the
reply should go to and whether or not it should be sent in private
message (commands sent in private are replied to in private). The
string is the reply to be sent. Don't worry about length restrictions
or anything -- if the string you want to send is too big for an IRC
message (and oftentimes that turns out to be the case :)) the supybot
framework handles that entirely transparently to you. Do make sure,
however, that you give irc.reply a string. It doesn't take anything
else (sometimes even unicode fails!). That's why we have
irc.reply takes one simple argument: a string. The string is the
reply to be sent. Don't worry about length restrictions or anything
-- if the string you want to send is too big for an IRC message (and
oftentimes that turns out to be the case :)) the Supybot framework
handles that entirely transparently to you. Do make sure, however,
that you give irc.reply a string. It doesn't take anything else
(sometimes even unicode fails!). That's why we have
"str(self.rng.random())" instead of simply "self.rng.random()" -- we
had to give irc.reply a string.
@ -268,10 +261,10 @@ thing. So we'll add a seed command to give the RNG a specific seed:
seed = long(seed)
except ValueError:
# It wasn't a valid long!
irc.error(msg, '<seed> must be a valid int or long.')
irc.error('<seed> must be a valid int or long.')
return
self.rng.seed(seed)
irc.reply(msg, conf.replySuccess)
irc.replySuccess()
So this one's a bit more complicated. But it's still pretty simple.
The method name is "seed" so that'll be the command name. The
@ -280,7 +273,7 @@ need to go over that again. The body of the function, however, is
significantly different.
privmsgs.getArgs is a function you're going to be seeing a lot of when
you write plugins for supybot. What it does is basically give you the
you write plugins for Supybot. What it does is basically give you the
right number of arguments for your comamnd. In this case, we want one
argument. But we might have been given any number of arguments by the
user. So privmsgs.getArgs joins them appropriately, leaving us with
@ -294,7 +287,7 @@ user didn't give us enough arguments, it'll reply with the help string
for the command, thus saving us the effort.
So we have the seed from privmsgs.getArgs. But it's a string. The
next three lines is pretty darn obvious: we're just converting the
next three lines are pretty darn obvious: we're just converting the
string to a int of some sort. But if it's not, that's when we're
going to call irc.error. It has the same interface as we saw before
in irc.reply, but it makes sure to remind the user that an error has
@ -335,10 +328,10 @@ function:
end = int(end)
start = int(start)
except ValueError:
irc.error(msg, '<start> and <end> must both be integers.')
irc.error('<start> and <end> must both be integers.')
return
# .randrange() doesn't include the endpoint, so we use end+1.
irc.reply(msg, str(self.rng.randrange(start, end+1)))
irc.reply(str(self.rng.randrange(start, end+1)))
Pretty simple. This is becoming old hat by now. The only new thing
here is the call to privmsgs.getArgs. We have to make sure, since we
@ -365,14 +358,14 @@ of arguments isn't right). Here's the code:
except IndexError: # raised by .pop(0)
raise callbacks.ArgumentError
except ValueError:
irc.error(msg, '<number of items> must be an integer.')
irc.error('<number of items> must be an integer.')
return
if n > len(args):
irc.error(msg, '<number of items> must be less than the number '
'of arguments.')
irc.error('<number of items> must be less than the number '
'of arguments.')
return
sample = self.rng.sample(args, n)
irc.reply(msg, utils.commaAndify(map(repr, sample)))
irc.reply(utils.commaAndify(map(repr, sample)))
Most everything here is familiar. The difference between this and the
previous examples is that we're dealing with args directly, rather
@ -414,11 +407,10 @@ __" where __ is the number the bot rolled. So here's the code:
n = 6
n = int(n)
except ValueError:
irc.error(msg, 'Dice have integer numbers of sides. Use one.')
irc.error('Dice have integer numbers of sides. Use one.')
return
s = 'rolls a %s' % self.rng.randrange(1, n+1)
irc.queueMsg(ircmsgs.action(ircutils.replyTo(msg), s))
raise callbacks.CannotNest
irc.reply(s, action=True)
There's a lot of stuff you haven't seen before in there. The most
important, though, is the first thing you'll notice that's different:
@ -430,24 +422,9 @@ tell it that we don't *need* any arguments (via required=0) and that we
argument, we'll get it -- if they don't, we'll just get an empty
string. Hence the "if not n: n = 6", where we provide the default.
Later, though, you'll see something other than irc.reply. This is
irc.queueMsg, the general interface for sending messages to the
server. It's what irc.reply is using under the covers. It takes an
IrcMsg object. Fortunately, that's exactly what's returned by
ircmsgs.action. An action message, just in case you don't know, is a
/me kind of message. ircmsgs.action is a helper function that takes a
target (a place to send the message, either a channel or a person) and
a payload (the thing to /me) and returns the appropriate IrcMsg
object. ircutils.replyTo simply takes an IrcMsg and returns where we
should reply to; if the message was originally sent to a channel,
we'll reply to there, if it was originally sent to us privately, we'll
reply in private.
At the end, you might be surprised by the "raise callbacks.CannotNest".
That's used simply because at the moment you can't nest actions (just like
you can't nest anything that doesn't go through irc.reply). That raise just
makes sure the user finds this out if he tries to nest this like "@rot13
[diceroll]".
You'll also note that irc.reply was given a keyword argument here,
"action". This means that the reply is to be made as an action rather
than a normal reply.
So that's our plugin. 5 commands, each building in complexity. You
should now be able to write most anything you want to do in Supybot.
@ -455,47 +432,8 @@ Except regexp-based plugins, but that's a story for another day (and
those aren't nearly as cool as these command-based callbacks anyway
:)). Now we need to flesh it out to make it a full-fledged plugin.
Let's take a look at that configure function newplugin.py made for
us. Here it is, in case you've forgotten:
def configure(onStart, afterConnect, advanced):
# This will be called by setup.py to configure this module. onStart and
# afterConnect are both lists. Append to onStart the commands you would
# like to be run when the bot is started; append to afterConnect the
# commands you would like to be run when the bot has finished connecting.
from questions import expect, anything, something, yn
onStart.append('load Random')
You remember when you first started running supybot and ran
scripts/setup.py and it asked you all those questions? Well, now's
your chance to ask other users some questions of your own. In our
case, with our Random plugin, it might be nice to offer the user the
ability to specify a seed to use whenever the plugin is loaded. So
let's ask him if he wants to do that, and if so, let's ask him what
the seed should be.
def configure(onStart, afterConnect, advanced):
# This will be called by setup.py to configure this module. onStart and
# afterConnect are both lists. Append to onStart the commands you would
# like to be run when the bot is started; append to afterConnect the
# commands you would like to be run when the bot has finished connecting.
from questions import expect, anything, something, yn
onStart.append('load Random')
if yn('Do you want to specify a seed to be used for the RNG')=='y':
seed = something('What seed? It must be an int or long.')
while not seed.isdigit():
print 'That\'s not a valid seed.'
seed = something('What seed?')
onStart.append('seed %s' % seed)
As you can see, what the questions module does is fairly self-evident:
yn returns either 'y' or 'n'; something returns *something* (but not
nothing; for nothing, you'd want anything). So basically we ask some
questions until we get a good seed. Then we do this
"onStart.append('seed %s' % seed)" doohickey. onStart is a list of
the commands to run when the bot starts; we're just throwing our
little piece into it. These commands will then be written into the
template scripts/setup.py creates for the bot.
TODO: Describe the registry and how to write a proper plugin configure
function.
We've written our own plugin from scratch (well, from the boilerplate
that we got from scripts/newplugin.py :)) and survived! Now go write