mirror of
https://github.com/Mikaela/Limnoria.git
synced 2024-12-22 10:42:55 +01:00
745 lines
32 KiB
Plaintext
745 lines
32 KiB
Plaintext
|
<!DOCTYPE article PUBLIC "-//OASIS//DTD DocBook V4.1//EN">
|
||
|
|
||
|
<article>
|
||
|
<articleinfo>
|
||
|
<authorgroup>
|
||
|
<author>
|
||
|
<firstname>Jeremiah</firstname>
|
||
|
<surname>Fincher</surname>
|
||
|
</author>
|
||
|
<editor>
|
||
|
<firstname>Daniel</firstname>
|
||
|
<surname>DiPaolo</surname>
|
||
|
<contrib>DocBook translator</contrib>
|
||
|
</editor>
|
||
|
</authorgroup>
|
||
|
<title>Supybot plugin author example</title>
|
||
|
<revhistory>
|
||
|
<revision>
|
||
|
<revnumber>0.1</revnumber>
|
||
|
<date>13 Sep 2003</date>
|
||
|
<revremark>Initial revision</revremark>
|
||
|
</revision>
|
||
|
<revision>
|
||
|
<revnumber>0.2</revnumber>
|
||
|
<date>14 Sep 2003</date>
|
||
|
<revremark>Converted to DocBook</revremark>
|
||
|
</revision>
|
||
|
</revhistory>
|
||
|
</articleinfo>
|
||
|
<sect1>
|
||
|
<title>Introduction</title>
|
||
|
<para>
|
||
|
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.
|
||
|
</para>
|
||
|
<para>
|
||
|
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.
|
||
|
</para>
|
||
|
<para>
|
||
|
So now that we know you've used supybot, we'll start getting into
|
||
|
details.
|
||
|
</para>
|
||
|
</sect1>
|
||
|
|
||
|
<sect1>
|
||
|
<title>Creating your own plugin</title>
|
||
|
<sect2>
|
||
|
<title>
|
||
|
Using <application>scripts/newplugin.py</application>
|
||
|
</title>
|
||
|
<para>
|
||
|
First, the easiest way to start writing a module is to use the
|
||
|
wizard provided,
|
||
|
<application>scripts/newplugin.py</application>. Here's an
|
||
|
example session:
|
||
|
</para>
|
||
|
<screen>
|
||
|
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%
|
||
|
</screen>
|
||
|
<para>
|
||
|
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
|
||
|
<filename>examples/Random.py</filename>):
|
||
|
</para>
|
||
|
<programlisting>
|
||
|
#!/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.
|
||
|
"""
|
||
|
|
||
|
from baseplugin import *
|
||
|
|
||
|
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')
|
||
|
|
||
|
example = utils.wrapLines("""
|
||
|
Add an example IRC session using this module here.
|
||
|
""")
|
||
|
|
||
|
class Random(callbacks.Privmsg):
|
||
|
pass
|
||
|
|
||
|
|
||
|
Class = Random
|
||
|
|
||
|
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:
|
||
|
</programlisting>
|
||
|
<sect2>
|
||
|
<title>Customizing the boilerplate code</title>
|
||
|
<para>
|
||
|
So a few notes, before we customize it.
|
||
|
</para>
|
||
|
<para>
|
||
|
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 :)
|
||
|
</para>
|
||
|
<para>
|
||
|
Describe what you want the plugin to do in the docstring.
|
||
|
This is used in <application>scripts/setup.py</application> 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 <literal>"Lots of stuff relating to random
|
||
|
numbers."</literal>
|
||
|
</para>
|
||
|
<para>
|
||
|
Then there are the imports. The <varname>utils</varname>
|
||
|
module is used (in example, which we'll see later). The
|
||
|
<varname>callbacks</varname> module is used (the class you're
|
||
|
given subclasses <varname>callbacks.Privmsg</varname>) but the
|
||
|
<varname>privmsgs</varname> 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.
|
||
|
</para>
|
||
|
<para>
|
||
|
Then you see a <function>configure</function> function. This
|
||
|
the function that's called when users decide to add your
|
||
|
module in <application>scripts/setup.py</application>. You'll
|
||
|
note that by default it simply adds <literal>"load
|
||
|
Example"</literal> (where 'Example' is the name you provided
|
||
|
as the name of your plugin, so in our case it is
|
||
|
<literal>"load Random"</literal>) 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.
|
||
|
</para>
|
||
|
<para>
|
||
|
Then there's an example string. It's simply an example of
|
||
|
usage of the plugin in practice.
|
||
|
<application>scripts/setup.py</application> offers to show the
|
||
|
user an example of the module usage; this is what it shows
|
||
|
them. You'll note that it's wrapped for you in
|
||
|
<varname>utils.wrapLines</varname> so you don't have to bother
|
||
|
with it; just paste a session directly out of your IRC client
|
||
|
and you'll be set.
|
||
|
</para>
|
||
|
<sect2>
|
||
|
<title>Digging in: customizing the plugin class</title>
|
||
|
<para>
|
||
|
Now comes the meat of the plugin: the plugin class.
|
||
|
</para>
|
||
|
<para>
|
||
|
What you're given is a skeleton: a simple subclass of
|
||
|
<varname>callbacks.Privmsg</varname> for you to start with.
|
||
|
Now let's add a command.
|
||
|
</para>
|
||
|
<para>
|
||
|
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 <varname>random</varname> module, the
|
||
|
<varname>Random</varname> object. So the first thing we're
|
||
|
going to have to do is give our plugin a
|
||
|
<varname>Random</varname> object.
|
||
|
</para>
|
||
|
<para>
|
||
|
Normally, when we want to give instances of a class an object,
|
||
|
we'll do so in the <function>__init__</function> method. And
|
||
|
that works great for plugins, too. The one thing you have to
|
||
|
be careful of is that you call the superclass
|
||
|
<function>__init__</function> method at the end of your own
|
||
|
<function>__init__</function>. So to add this
|
||
|
<varname>random.Random</varname> object to our plugin, we can
|
||
|
replace the <function>pass</function> statement with
|
||
|
this:
|
||
|
</para>
|
||
|
<programlisting>
|
||
|
def __init__(self):
|
||
|
self.rng = random.Random()
|
||
|
callbacks.Privmsg.__init__(self)
|
||
|
</programlisting>
|
||
|
<para>
|
||
|
(<varname>rng</varname>is an abbreviation for "random number
|
||
|
generator," in case you were curious)
|
||
|
</para>
|
||
|
<para>
|
||
|
Do be careful not to give your <function>__init__</function>
|
||
|
any arguments (other than <varname>self</varname>, 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.
|
||
|
</para>
|
||
|
<para>
|
||
|
There's an easier way to get our plugin to have its own rng
|
||
|
than to define an <function>__init__</function>. 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 <function>__init__</function>, we can just add it
|
||
|
as a attribute to the class itself. Like so (replacing the
|
||
|
<function>pass</function> statement again):
|
||
|
</para>
|
||
|
<programlisting>
|
||
|
rng = random.Random()
|
||
|
</programlisting>
|
||
|
<para>
|
||
|
And we save two lines of code and make our code a little more
|
||
|
clear :)
|
||
|
</para>
|
||
|
<para>
|
||
|
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.
|
||
|
</para>
|
||
|
<programlisting>
|
||
|
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()))
|
||
|
</programlisting>
|
||
|
<para>
|
||
|
And that's it! Pretty simple, huh? Anyway, you're probably
|
||
|
wondering what all that <emphasis>means</emphasis>. We'll
|
||
|
start with the <function>def</function> statement:
|
||
|
</para>
|
||
|
<programlisting>
|
||
|
def random(self, irc, msg, args):
|
||
|
</programlisting>
|
||
|
<para>
|
||
|
What that does is define a command
|
||
|
<function>random</function>. You can call it by saying
|
||
|
"@random" (or whatever prefix character your specific bot
|
||
|
uses). The arguments are a bit less obvious.
|
||
|
<varname>self</varname> is self-evident (hah!).
|
||
|
<varname>irc</varname> is the <varname>Irc</varname> object
|
||
|
passed to the command; <varname>msg</varname> is the original
|
||
|
<varname>IrcMsg</varname> object. But you're really not going
|
||
|
to have to deal with either of these too much (with the
|
||
|
exception of calling <function>irc.reply</function> or
|
||
|
<function>irc.error</function>). What you're
|
||
|
<emphasis>really</emphasis> interested in is the
|
||
|
<varname>args</varname> 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 <varname>Irc</varname> object in
|
||
|
<filename>irclib.py</filename> (you won't find
|
||
|
<function>.reply</function> or <function>.error</function>
|
||
|
there, though, because you're actually getting an
|
||
|
<varname>IrcObjectProxy</varname>, but that's beyond the level
|
||
|
we want to describe here :)). You can read about the
|
||
|
<varname>msg</varname> object in
|
||
|
<filename>ircmsgs.py</filename>. But again, aside from
|
||
|
calling <function>irc.reply</function> or
|
||
|
<function>irc.error</function>, you'll very rarely be using
|
||
|
these objects.
|
||
|
</para>
|
||
|
<para>
|
||
|
(In case you're curious, the answer is yes, you
|
||
|
<emphasis>must</emphasis> name your arguments <varname>(self,
|
||
|
irc, msg, args)</varname>. 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 <varname>(self, irc,
|
||
|
msg, args)</varname>, however unlikely that may be). Just
|
||
|
name it with an underscore or an uppercase letter in it :))
|
||
|
</para>
|
||
|
<para>
|
||
|
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
|
||
|
<emphasis>is</emphasis> the help! Given the above docstring,
|
||
|
this is what a supybot does:
|
||
|
</para>
|
||
|
<screen>
|
||
|
<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.
|
||
|
</screen>
|
||
|
<para>
|
||
|
'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:
|
||
|
</para>
|
||
|
<programlisting>
|
||
|
irc.reply(msg, str(self.rng.random()))
|
||
|
</programlisting>
|
||
|
<para>
|
||
|
<function>irc.reply</function> takes two arguments, an
|
||
|
<varname>IrcMsg</varname> (like the one passed into your
|
||
|
function) and a string. The <varname>IrcMsg</varname> 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 <function>irc.reply</function> 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
|
||
|
<function>irc.reply</function> a string.
|
||
|
</para>
|
||
|
<para>
|
||
|
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:
|
||
|
</para>
|
||
|
<programlisting>
|
||
|
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)
|
||
|
</programlisting>
|
||
|
<para>
|
||
|
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.
|
||
|
</para>
|
||
|
<para>
|
||
|
<function>privmsgs.getArgs</function> 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
|
||
|
<function>privmsgs.getArgs</function> 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 <varname>IndexError</varname> from
|
||
|
<varname>args[0]</varname> and complain to the user about it.
|
||
|
<function>privmsgs.getArgs</function>, 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.
|
||
|
</para>
|
||
|
<para>
|
||
|
So we have the seed from
|
||
|
<function>privmsgs.getArgs</function>. 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
|
||
|
<function>irc.error</function>. It has the same interface as
|
||
|
we saw before in <function>irc.reply</function>, 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 <function>return</function> 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
|
||
|
<function>irc.error</function>, you'll want to return
|
||
|
immediately afterwards.
|
||
|
</para>
|
||
|
<para>
|
||
|
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
|
||
|
<varname>conf.replySuccess</varname> says. By default, it has
|
||
|
the very dry (and appropriately robot-like) "The operation
|
||
|
succeeded." but you're perfectly welcome to customize it
|
||
|
yourself -- <filename>conf.py</filename> was written to be
|
||
|
modified!
|
||
|
</para>
|
||
|
<para>
|
||
|
So that's a bit more complicated command. But we still
|
||
|
haven't dealt with multiple arguments. Let's do that
|
||
|
next.
|
||
|
</para>
|
||
|
<para>
|
||
|
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:
|
||
|
</para>
|
||
|
<programlisting>
|
||
|
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, needed=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)))
|
||
|
</programlisting>
|
||
|
<para>
|
||
|
Pretty simple. This is becoming old hat by now. The only new
|
||
|
thing here is the call to
|
||
|
<function>privmsgs.getArgs</function>. We have to make sure,
|
||
|
since we want two values, to pass a keyword parameter "needed"
|
||
|
into <function>privmsgs.getArgs</function>. Of course,
|
||
|
<function>privmsgs.getArgs</function> handles all the checking
|
||
|
for missing arguments and whatnot so we don't have to.
|
||
|
</para>
|
||
|
<para>
|
||
|
The <varname>Random</varname> object we're using offers us a
|
||
|
"sample" method that takes a sequence and a number (we'll call
|
||
|
it <varname>N</varname>) and returns a list of
|
||
|
<varname>N</varname> items taken randomly from the sequence.
|
||
|
So I'll show you an example that takes advantage of multiple
|
||
|
arguments but doesn't use
|
||
|
<function>privmsgs.getArgs</function> (and thus has to handle
|
||
|
its own errors if the number of arguments isn't right).
|
||
|
Here's the code:
|
||
|
</para>
|
||
|
<programlisting>
|
||
|
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)))
|
||
|
</programlisting>
|
||
|
<para>
|
||
|
Most everything here is familiar. The difference between this
|
||
|
and the previous examples is that we're dealing with
|
||
|
<varname>args</varname> directly, rather than through
|
||
|
<function>getArgs</function>. Since we already have the
|
||
|
arguments in a list, it doesn't make any sense to have
|
||
|
<function>privmsgs.getArgs</function> 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
|
||
|
<function>privmsgs.getArgs</function>. So what do we do? We
|
||
|
raise <varname>callbacks.ArgumentError</varname>! That's the
|
||
|
secret juju that <function>privmsgs.getArgs</function> 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
|
||
|
<function>.pop(0)</function> fails, we weren't given enough
|
||
|
arguments and thus need to tell the user how to call us.
|
||
|
</para>
|
||
|
<para>
|
||
|
So we have the args, we have the number, we do a simple call
|
||
|
to <function>random.sample</function> and then we do this
|
||
|
funky <function>utils.commaAndify</function> 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 <function>repr()</function>
|
||
|
first just to surround them with quotes.
|
||
|
</para>
|
||
|
<para>
|
||
|
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:
|
||
|
</para>
|
||
|
<programlisting>
|
||
|
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, needed=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
|
||
|
</programlisting>
|
||
|
<para>
|
||
|
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 <function>privmsg.getArgs</function>
|
||
|
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 :)) <function>privmsgs.getArgs</function>
|
||
|
supports that; we'll just tell it that we don't
|
||
|
<emphasis>need</emphasis> any arguments (via
|
||
|
<varname>needed=0</varname>) and that we <emphasis>might
|
||
|
like</emphasis> one argument (<varname>optional=1</varname>).
|
||
|
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.
|
||
|
</para>
|
||
|
<para>
|
||
|
Later, though, you'll see something other than
|
||
|
<function>irc.reply</function>. This is
|
||
|
<function>irc.queueMsg</function>, the general interface for
|
||
|
sending messages to the server. It's what
|
||
|
<function>irc.reply</function> is using under the covers. It
|
||
|
takes an <varname>IrcMsg</varname> object. Fortunately,
|
||
|
that's exactly what's returned by
|
||
|
<function>ircmsgs.action</function>. An action message, just
|
||
|
in case you don't know, is a /me kind of message.
|
||
|
<function>ircmsgs.action</function> 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 <varname>IrcMsg</varname> object.
|
||
|
<function>ircutils.replyTo</function> simply takes an
|
||
|
<varname>IrcMsg</varname> 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.
|
||
|
</para>
|
||
|
<para>
|
||
|
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
|
||
|
<function>irc.reply</function>). That raise just makes sure
|
||
|
the user finds this out if he tries to nest this like "@rot13
|
||
|
[diceroll]".
|
||
|
</para>
|
||
|
<para>
|
||
|
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.
|
||
|
</para>
|
||
|
</sect2>
|
||
|
<sect2>
|
||
|
<title>Finishing touches</title>
|
||
|
<para>
|
||
|
Let's take a look at that <function>configure</function>
|
||
|
function <application>scripts/newplugin.py</application> made
|
||
|
for us. Here it is, in case you've forgotten:
|
||
|
</para>
|
||
|
<programlisting>
|
||
|
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')
|
||
|
</programlisting>
|
||
|
<para>
|
||
|
You remember when you first started running supybot and ran
|
||
|
<application>scripts/setup.py</application> 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
|
||
|
<varname>Random</varname> 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.
|
||
|
</para>
|
||
|
<programlisting>
|
||
|
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)
|
||
|
</programlisting>
|
||
|
<para>
|
||
|
As you can see, what the <varname>questions</varname> module
|
||
|
does is fairly self-evident: <function>yn</function> returns
|
||
|
either 'y' or 'n'; <function>something</function> returns
|
||
|
<emphasis>something</emphasis> (but not nothing; for nothing,
|
||
|
you'd want <function>anything</function>). So basically we
|
||
|
ask some questions until we get a good seed. Then we do this
|
||
|
"onStart.append('seed %s' % seed)" doohickey.
|
||
|
<varname>onStart</varname> 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
|
||
|
<application>scripts/setup.py</application> creates for the bot.
|
||
|
</para>
|
||
|
<para>
|
||
|
Now the only thing missing from our plugin is an example.
|
||
|
Here, I'll make one really quickly:
|
||
|
</para>
|
||
|
<screen>
|
||
|
<jemfinch> $list Random
|
||
|
<angryman> diceroll, random, range, sample, seed
|
||
|
<jemfinch> $random
|
||
|
<angryman> 0.478084042957
|
||
|
<jemfinch> $random
|
||
|
<angryman> 0.960634332773
|
||
|
<jemfinch> $seed 50
|
||
|
<angryman> The operation succeeded.
|
||
|
<jemfinch> $random
|
||
|
<angryman> 0.497536568759
|
||
|
<jemfinch> $seed 50
|
||
|
<angryman> The operation succeeded.
|
||
|
<jemfinch> $random
|
||
|
<angryman> 0.497536568759
|
||
|
<jemfinch> $range 1 10
|
||
|
<angryman> 3
|
||
|
<jemfinch> $range 1 10000000000000
|
||
|
<angryman> 6374111614437
|
||
|
<jemfinch> $diceroll
|
||
|
* angryman rolls a 2
|
||
|
<jemfinch> $diceroll
|
||
|
* angryman rolls a 3
|
||
|
<jemfinch> $diceroll 100
|
||
|
* angryman rolls a 97
|
||
|
</screen>
|
||
|
<para>
|
||
|
So we'll throw this into our example string (where the
|
||
|
template says to put it) and then we're done! We've written
|
||
|
our own plugin from scratch (well, from the boilerplate that
|
||
|
we got from <application>scripts/newplugin.py</application>
|
||
|
:)) and survived! Now go write more plugins for supybot, and
|
||
|
send them to me so I can use them too :)
|
||
|
</para>
|
||
|
</sect2>
|
||
|
</sect1>
|
||
|
</article>
|