mirror of
https://github.com/Mikaela/Limnoria.git
synced 2024-11-30 14:59:34 +01:00
503 lines
24 KiB
Plaintext
503 lines
24 KiB
Plaintext
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
|
|
after that.
|
|
|
|
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
|
|
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
|
|
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
|
|
when you've used supybot. They're also the most featureful and
|
|
easiest to write. Commands can be nested, for instance, whereas
|
|
regexp-based callbacks can't do nesting. That doesn't mean that
|
|
you'll never want regexp-based callbacks. They offer a flexibility
|
|
that command-based callbacks don't offer; however, they don't tie into
|
|
the whole system as well. If you need to combine a command-based
|
|
callback with some regexp-based methods, you can do so by subclassing
|
|
callbacks.PrivmsgCommandAndRegexp and then adding a class-level
|
|
attribute "regexps" that is a sets.Set of methods that are regexp-
|
|
based. But you'll have to do that yourself after this wizard is
|
|
finished :)
|
|
Do you want a command-based plugin or a regexp-based plugin? [command/
|
|
regexp] command
|
|
Sometimes you'll want a callback to be threaded. If its methods
|
|
(command or regexp-based, either one) will take a signficant amount
|
|
of time to run, you'll want to thread them so they don't block
|
|
the entire bot.
|
|
|
|
Does your plugin need to be threaded? [y/n] n
|
|
Your new plugin template is in plugins/Random.py
|
|
functor%
|
|
-----
|
|
|
|
So that's what it looks like. Now let's look at the source code (if
|
|
you'd like to look at it in your programming editor, the whole plugin
|
|
is available as examples/Random.py):
|
|
|
|
-----
|
|
#!/usr/bin/env python
|
|
|
|
###
|
|
# Copyright (c) 2002, Jeremiah Fincher
|
|
# All rights reserved.
|
|
#
|
|
# Redistribution and use in source and binary forms, with or without
|
|
# modification, are permitted provided that the following conditions are met:
|
|
#
|
|
# * Redistributions of source code must retain the above copyright notice,
|
|
# this list of conditions, and the following disclaimer.
|
|
# * Redistributions in binary form must reproduce the above copyright notice,
|
|
# this list of conditions, and the following disclaimer in the
|
|
# documentation and/or other materials provided with the distribution.
|
|
# * Neither the name of the author of this software nor the name of
|
|
# contributors to this software may be used to endorse or promote products
|
|
# derived from this software without specific prior written consent.
|
|
#
|
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
|
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
# POSSIBILITY OF SUCH DAMAGE.
|
|
###
|
|
|
|
"""
|
|
Add the module docstring here. This will be used by the setup.py script.
|
|
"""
|
|
|
|
import plugins
|
|
|
|
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.
|
|
from questions import expect, anything, something, yn
|
|
onStart.append('load Random')
|
|
|
|
class Random(callbacks.Privmsg):
|
|
pass
|
|
|
|
|
|
Class = Random
|
|
|
|
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:
|
|
-----
|
|
|
|
So a few notes, before we customize it.
|
|
|
|
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
|
|
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."
|
|
|
|
Then there are the imports. The callbacks module is used (the class
|
|
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.
|
|
|
|
Now comes the meat of the plugin: the plugin class.
|
|
|
|
What you're given is a skeleton: a simple subclass of
|
|
callbacks.Privmsg for you to start with. Now let's add a command.
|
|
|
|
I don't know what you know about random number generators, but the
|
|
short of it is that they start at a certain number (a seed) and they
|
|
continue (via some somewhat complicated/unpredictable algorithm) from
|
|
there. This seed (and the rest of the sequence, really) is all nice
|
|
and packaged up in Python's random module, the Random object. So the
|
|
first thing we're going to have to do is give our plugin a Random
|
|
object.
|
|
|
|
Normally, when we want to give instances of a class an object, we'll
|
|
do so in the __init__ method. And that works great for plugins, too.
|
|
The one thing you have to be careful of is that you call the
|
|
superclass __init__ method at the end of your own __init__. So to add
|
|
this random.Random object to our plugin, we can replace the "pass"
|
|
statement with this:
|
|
|
|
def __init__(self):
|
|
self.rng = random.Random()
|
|
callbacks.Privmsg.__init__(self)
|
|
|
|
(rng is an abbreviation for "random number generator," in case you
|
|
were curious)
|
|
|
|
Do be careful not to give your __init__ any arguments (other than
|
|
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.
|
|
|
|
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
|
|
always certain that there will only be one instance -- supybot doesn't
|
|
allow us to load multiple instances of a single plugin. So instead of
|
|
adding the rng in __init__, we can just add it as a attribute to the
|
|
class itself. Like so (replacing the "pass" statement again):
|
|
|
|
rng = random.Random()
|
|
|
|
And we save two lines of code and make our code a little more clear :)
|
|
|
|
Now that we have an RNG, we need some way to get random numbers. So
|
|
first, we'll add a command that simply gets the next random number and
|
|
gives it back to the user. It takes no arguments, of course (what
|
|
would you give it?). Here's the command, and I'll follow that with the
|
|
explanation of what each part means.
|
|
|
|
def random(self, irc, msg, args):
|
|
"""takes no arguments
|
|
|
|
Returns the next random number generated by the random number
|
|
generator.
|
|
"""
|
|
irc.reply(msg, 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:
|
|
|
|
def random(self, irc, msg, args):
|
|
|
|
What that does is define a command "random". You can call it by
|
|
saying "@random" (or whatever prefix character your specific bot
|
|
uses). The arguments are a bit less obvious. Self is self-evident
|
|
(hah!). irc is the Irc object passed to the command; msg is the
|
|
original IrcMsg object. But you're really not going to have to deal
|
|
with either of these too much (with the exception of calling irc.reply
|
|
or irc.error). What you're *really* interested in is the args arg.
|
|
That if a list of all the arguments passed to your command, pre-parsed
|
|
and already evaluated (i.e., you never have to worry about nested
|
|
commands, or handling double quoted strings, or splitting on
|
|
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.
|
|
|
|
(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
|
|
of the ways that supybot uses to determine which methods in a plugin
|
|
class are commands and which aren't. And while we're talking about
|
|
naming restrictions, all your commands should be named in
|
|
all-lowercase with no underscores. Before calling a command, supybot
|
|
always converts the command name to lowercase and removes all dashes
|
|
and underscores. On the other hand, you now know an easy way to make
|
|
sure a method is never called (even if its arguments are (self, irc,
|
|
msg, args), however unlikely that may be). Just name it with an
|
|
underscore or an uppercase letter in it :))
|
|
|
|
You'll also note that the docstring is odd. The wonderful thing about
|
|
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.
|
|
|
|
'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:
|
|
|
|
irc.reply(msg, 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
|
|
"str(self.rnd.random())" instead of simply "self.rng.random()" -- we
|
|
had to give irc.reply a string.
|
|
|
|
Anyway, now that we have an RNG, we have a need for seed! Of course,
|
|
Python gives us a good seed already (it uses the current time as a
|
|
seed if we don't give it one) but users might want to be able to
|
|
repeat "random" sequences, so letting them set the seed is a good
|
|
thing. So we'll add a seed command to give the RNG a specific seed:
|
|
|
|
def seed(self, irc, msg, args):
|
|
"""<seed>
|
|
|
|
Sets the seed of the random number generator. <seed> must be
|
|
an int or a long.
|
|
"""
|
|
seed = privmsgs.getArgs(args)
|
|
try:
|
|
seed = long(seed)
|
|
except ValueError:
|
|
# It wasn't a valid long!
|
|
irc.error(msg, '<seed> must be a valid int or long.')
|
|
return
|
|
self.rng.seed(seed)
|
|
irc.reply(msg, conf.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
|
|
arguments are the same, the docstring is of the same form, so we don't
|
|
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
|
|
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
|
|
one single "seed" argument (by default, it returns one argument as a
|
|
single value; more arguments are returned in a tuple/list). Yes, we
|
|
could've just said "seed = args[0]" and gotten the first argument, but
|
|
what if the user didn't pass us an argument at all? Then we've got to
|
|
catch the IndexError from args[0] and complain to the user about it.
|
|
privmsgs.getArgs, on the other hand, handles all that for us. If the
|
|
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
|
|
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
|
|
been encountered (currently, that means it puts "Error: " at the
|
|
beginning of the message). After erroring, we return. It's important
|
|
to remember this return here; otherwise, we'll just keep going down
|
|
through the function and try to use this "seed" variable that never
|
|
got assigned. A good general rule of thumb is that any time you use
|
|
irc.error, you'll want to return immediately afterwards.
|
|
|
|
Then we set the seed -- that's a simple function on our rng object.
|
|
Assuming that succeeds (and doesn't raise an exception, which it
|
|
shouldn't, because we already read the documentation and know that it
|
|
should work) we reply to say that everything worked fine. That's what
|
|
conf.replySuccess says. By default, it has the very dry (and
|
|
appropriately robot-like) "The operation succeeded." but you're
|
|
perfectly welcome to customize it yourself -- conf.py was written to
|
|
be modified!
|
|
|
|
So that's a bit more complicated command. But we still haven't dealt
|
|
with multiple arguments. Let's do that next.
|
|
|
|
So these random numbers are useful, but they're not the kind of random
|
|
numbers we usually want in Real Life. In Real Life, we like to tell
|
|
someone to "pick a number between 1 and 10." So let's write a
|
|
function that does that. Of course, we won't hardcode the 1 or the 10
|
|
into the function, but we'll take them as arguments. First the
|
|
function:
|
|
|
|
def range(self, irc, msg, args):
|
|
"""<start> <end>
|
|
|
|
Returns a number between <start> and <end>, inclusive (i.e., the number
|
|
can be either of the endpoints.
|
|
"""
|
|
(start, end) = privmsgs.getArgs(args, required=2)
|
|
try:
|
|
end = int(end)
|
|
start = int(start)
|
|
except ValueError:
|
|
irc.error(msg, '<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)))
|
|
|
|
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
|
|
want two values, to pass a keyword parameter "needed" into
|
|
privmsgs.getArgs. Of course, privmsgs.getArgs handles all the
|
|
checking for missing arguments and whatnot so we don't have to.
|
|
|
|
The Random object we're using offers us a "sample" method that takes a
|
|
sequence and a number (we'll call it N) and returns a list of N items
|
|
taken randomly from the sequence. So I'll show you an example that
|
|
takes advantage of multiple arguments but doesn't use
|
|
privmsgs.getArgs (and thus has to handle its own errors if the number
|
|
of arguments isn't right). Here's the code:
|
|
|
|
def sample(self, irc, msg, args):
|
|
"""<number of items> [<text> ...]
|
|
|
|
Returns a sample of the <number of items> taken from the remaining
|
|
arguments. Obviously <number of items> must be less than the number
|
|
of arguments given.
|
|
"""
|
|
try:
|
|
n = int(args.pop(0))
|
|
except IndexError: # raised by .pop(0)
|
|
raise callbacks.ArgumentError
|
|
except ValueError:
|
|
irc.error(msg, '<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.')
|
|
return
|
|
sample = self.rng.sample(args, n)
|
|
irc.reply(msg, 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
|
|
than through getArgs. Since we already have the arguments in a list,
|
|
it doesn't make any sense to have privmsgs.getArgs smush them all
|
|
together into a big long string that we'll just have to re-split. But
|
|
we still want the nice error handling of privmsgs.getArgs. So what do
|
|
we do? We raise callbacks.ArgumentError! That's the secret juju
|
|
that privmsgs.getArgs is doing; now we're just doing it ourself.
|
|
Someone up our callchain knows how to handle it so a neat error
|
|
message is returned. So in this function, if .pop(0) fails, we
|
|
weren't given enough arguments and thus need to tell the user how to
|
|
call us.
|
|
|
|
So we have the args, we have the number, we do a simple call to
|
|
random.sample and then we do this funky utils.commaAndify to it.
|
|
Yeah, so I was running low on useful names :) Anyway, what it does is
|
|
take a list of strings and return a string with them joined by a
|
|
comma, the last one being joined with a comma and "and". So the list
|
|
['foo', 'bar', 'baz'] becomes "foo, bar, and baz". It's pretty useful
|
|
for showing the user lists in a useful form. We map the strings with
|
|
repr() first just to surround them with quotes.
|
|
|
|
So we have one more example. Yes, I hear your groans, but it's
|
|
pedagogically useful :) This time we're going to write a command that
|
|
makes the bot roll a die. It'll take one argument (the number of
|
|
sides on the die) and will respond with the equivalent of "/me rolls a
|
|
__" where __ is the number the bot rolled. So here's the code:
|
|
|
|
def diceroll(self, irc, msg, args):
|
|
"""[<number of sides>]
|
|
|
|
Rolls a die with <number of sides> sides. The default number
|
|
of sides is 6.
|
|
"""
|
|
try:
|
|
n = privmsgs.getArgs(args, required=0, optional=1)
|
|
if not n:
|
|
n = 6
|
|
n = int(n)
|
|
except ValueError:
|
|
irc.error(msg, '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
|
|
|
|
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:
|
|
the privmsg.getArgs call. Here we're offering a default argument in
|
|
case the user is too lazy to supply one (or just wants a nice,
|
|
standard six-sided die :)) privmsgs.getArgs supports that; we'll just
|
|
tell it that we don't *need* any arguments (via required=0) and that we
|
|
*might like* one argument (optional=1). If the user provides an
|
|
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]".
|
|
|
|
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.
|
|
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.
|
|
|
|
We've written our own plugin from scratch (well, from the boilerplate
|
|
that we got from scripts/newplugin.py :)) and survived! Now go write
|
|
more plugins for supybot, and send them to me so I can use them too :)
|