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 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. 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. 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. details.
First, the easiest way to start writing a module is to use the wizard First, the easiest way to start writing a module is to use the wizard
provided, scripts/newplugin.py. Here's an example session: 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 What should the name of the plugin be? Random
Supybot offers two major types of plugins: command-based and regexp- Supybot offers two major types of plugins: command-based and regexp-
based. Command-based plugins are the kind of plugins you've seen most 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 #!/usr/bin/env python
### ###
# Copyright (c) 2002, Jeremiah Fincher # Copyright (c) 2004, Jeremiah Fincher
# All rights reserved. # All rights reserved.
# #
# Redistribution and use in source and binary forms, with or without # 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 plugins
import conf
import utils import utils
import privmsgs import privmsgs
import callbacks import callbacks
def configure(onStart, afterConnect, advanced): def configure(advanced):
# This will be called by setup.py to configure this module. onStart and # This will be called by setup.py to configure this module. Advanced is
# afterConnect are both lists. Append to onStart the commands you would # a bool that specifies whether the user identified himself as an advanced
# like to be run when the bot is started; append to afterConnect the # user or not. You should effect your configuration by manipulating the
# commands you would like to be run when the bot has finished connecting. # registry as appropriate.
from questions import expect, anything, something, yn from questions import expect, anything, something, yn
onStart.append('load Random') conf.registerPlugin('Random', True)
class Random(callbacks.Privmsg): class Random(callbacks.Privmsg):
pass 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 :) 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 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 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 for a given module (instead of help for a certain command). We'll
change this one to "Lots of stuff relating to random numbers." 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 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. we go ahead and add the import to the template.
Then you see a "configure" function. This the function that's called Then you see a "configure" function. This is the function that's
when users decide to add your module in scripts/setup.py. You'll note called when users decide to add your module in supybot-wizard. You'll
that by default it simply adds "load Example" (where 'Example' is the name you note that by default it simply registers the plugin to be
provided as the name of your plugin, so in our case it is "load Random") at automatically loaded on startup. For many plugins this is all you
the bottom. For many plugins this is all you need; for more complex plugins, need; for more complex plugins, you might need to ask questions and
you might need to ask questions and add commands based on the answers. add commands based on the answers.
Now comes the meat of the plugin: the plugin class. 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 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 before it can do anything interesting, add a command that gets those
values. By convention, those commands begin with "start" -- check out 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 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 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 Returns the next random number generated by the random number
generator. 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 And that's it! Pretty simple, huh? Anyway, you're probably wondering
what all that *means*. We'll start with the def statement: 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 about the Irc object in irclib.py (you won't find .reply or .error
there, though, because you're actually getting an IrcObjectProxy, but there, though, because you're actually getting an IrcObjectProxy, but
that's beyond the level we want to describe here :)). You can read 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 about the msg object in ircmsgs.py. But again, you'll very rarely be
irc.reply or irc.error, you'll very rarely be using these objects. using these objects.
(In case you're curious, the answer is yes, you *must* name your (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 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 with help and everything: the docstring *IS* the help! Given the
above docstring, this is what a supybot does: above docstring, this is what a supybot does:
<angryman> jemfinch: random takes no arguments (for more help <jemfinch> @help random
use the morehelp command) <angryman> jemfinch: (random takes no arguments) -- Returns the
<jemfinch> $morehelp random next random number from the random number generator.
<angryman> jemfinch: Returns the next random number from the
current random number generator.
'help <command>' replies with the command name followed by the first line of Now on to the actual body of the function:
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:
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 irc.reply takes one simple argument: a string. The string is the
your function) and a string. The IrcMsg is used to determine who the reply to be sent. Don't worry about length restrictions or anything
reply should go to and whether or not it should be sent in private -- if the string you want to send is too big for an IRC message (and
message (commands sent in private are replied to in private). The oftentimes that turns out to be the case :)) the Supybot framework
string is the reply to be sent. Don't worry about length restrictions handles that entirely transparently to you. Do make sure, however,
or anything -- if the string you want to send is too big for an IRC that you give irc.reply a string. It doesn't take anything else
message (and oftentimes that turns out to be the case :)) the supybot (sometimes even unicode fails!). That's why we have
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 "str(self.rng.random())" instead of simply "self.rng.random()" -- we
had to give irc.reply a string. 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) seed = long(seed)
except ValueError: except ValueError:
# It wasn't a valid long! # 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 return
self.rng.seed(seed) 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. 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 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. significantly different.
privmsgs.getArgs is a function you're going to be seeing a lot of when 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 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 argument. But we might have been given any number of arguments by the
user. So privmsgs.getArgs joins them appropriately, leaving us with 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. for the command, thus saving us the effort.
So we have the seed from privmsgs.getArgs. But it's a string. The 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 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 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 in irc.reply, but it makes sure to remind the user that an error has
@ -335,10 +328,10 @@ function:
end = int(end) end = int(end)
start = int(start) start = int(start)
except ValueError: except ValueError:
irc.error(msg, '<start> and <end> must both be integers.') irc.error('<start> and <end> must both be integers.')
return return
# .randrange() doesn't include the endpoint, so we use end+1. # .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 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 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) except IndexError: # raised by .pop(0)
raise callbacks.ArgumentError raise callbacks.ArgumentError
except ValueError: except ValueError:
irc.error(msg, '<number of items> must be an integer.') irc.error('<number of items> must be an integer.')
return return
if n > len(args): if n > len(args):
irc.error(msg, '<number of items> must be less than the number ' irc.error('<number of items> must be less than the number '
'of arguments.') 'of arguments.')
return return
sample = self.rng.sample(args, n) 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 Most everything here is familiar. The difference between this and the
previous examples is that we're dealing with args directly, rather 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 = 6
n = int(n) n = int(n)
except ValueError: except ValueError:
irc.error(msg, 'Dice have integer numbers of sides. Use one.') irc.error('Dice have integer numbers of sides. Use one.')
return return
s = 'rolls a %s' % self.rng.randrange(1, n+1) s = 'rolls a %s' % self.rng.randrange(1, n+1)
irc.queueMsg(ircmsgs.action(ircutils.replyTo(msg), s)) irc.reply(s, action=True)
raise callbacks.CannotNest
There's a lot of stuff you haven't seen before in there. The most 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: 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 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. 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 You'll also note that irc.reply was given a keyword argument here,
irc.queueMsg, the general interface for sending messages to the "action". This means that the reply is to be made as an action rather
server. It's what irc.reply is using under the covers. It takes an than a normal reply.
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]".
So that's our plugin. 5 commands, each building in complexity. You 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. 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 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. :)). 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 TODO: Describe the registry and how to write a proper plugin configure
us. Here it is, in case you've forgotten: function.
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.
We've written our own plugin from scratch (well, from the boilerplate We've written our own plugin from scratch (well, from the boilerplate
that we got from scripts/newplugin.py :)) and survived! Now go write that we got from scripts/newplugin.py :)) and survived! Now go write