mirror of
				https://github.com/Mikaela/Limnoria.git
				synced 2025-11-03 17:17:23 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			442 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
			
		
		
	
	
			442 lines
		
	
	
		
			20 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% 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
 | 
						|
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 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) 2004, 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.
 | 
						|
"""
 | 
						|
 | 
						|
__revision__ = "$Id$"
 | 
						|
__author__ = ''
 | 
						|
 | 
						|
import supybot.plugins as plugins
 | 
						|
 | 
						|
import supybot.conf as conf
 | 
						|
import supybot.utils as utils
 | 
						|
import supybot.privmsgs as privmsgs
 | 
						|
import supybot.callbacks as callbacks
 | 
						|
 | 
						|
 | 
						|
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
 | 
						|
    conf.registerPlugin('Random', True)
 | 
						|
 | 
						|
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 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."
 | 
						|
 | 
						|
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 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.
 | 
						|
 | 
						|
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, you should get those values
 | 
						|
from the registry.
 | 
						|
 | 
						|
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(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 is 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, 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:
 | 
						|
 | 
						|
    <jemfinch> @help random
 | 
						|
    <angryman> jemfinch: (random takes no arguments) -- Returns the
 | 
						|
              next random number from the random number generator.
 | 
						|
 | 
						|
Now on to the actual body of the function:
 | 
						|
 | 
						|
    irc.reply(str(self.rng.random()))
 | 
						|
 | 
						|
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.
 | 
						|
 | 
						|
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('<seed> must be a valid int or long.')
 | 
						|
            return
 | 
						|
        self.rng.seed(seed)
 | 
						|
        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
 | 
						|
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 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
 | 
						|
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
 | 
						|
irc.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 -- the registry 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('<start> and <end> must both be integers.')
 | 
						|
            return
 | 
						|
        # .randrange() doesn't include the endpoint, so we use 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
 | 
						|
want two values, to pass a keyword parameter "required" 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('<number of items> must be an integer.')
 | 
						|
            return
 | 
						|
        if n > len(args):
 | 
						|
            irc.error('<number of items> must be less than the number '
 | 
						|
                      'of arguments.')
 | 
						|
            return
 | 
						|
        sample = self.rng.sample(args, n)
 | 
						|
        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
 | 
						|
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('Dice have integer numbers of sides.  Use one.')
 | 
						|
            return
 | 
						|
        s = 'rolls a %s' % self.rng.randrange(1, n+1)
 | 
						|
        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:
 | 
						|
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.
 | 
						|
 | 
						|
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.
 | 
						|
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.
 | 
						|
 | 
						|
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
 | 
						|
more plugins for supybot, and send them to me so I can use them too :)
 |