mirror of
https://github.com/Mikaela/Limnoria.git
synced 2024-12-22 10:42:55 +01:00
This is really example.sgml in a new life
This commit is contained in:
parent
25db3c9b98
commit
1f6f86aea7
714
docs/DocBook/plugin-example.sgml
Normal file
714
docs/DocBook/plugin-example.sgml
Normal file
@ -0,0 +1,714 @@
|
||||
<!DOCTYPE article SYSTEM "supybot.dtd">
|
||||
|
||||
<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>
|
||||
<revision>
|
||||
<revnumber>0.3</revnumber>
|
||||
<date>24 Nov 2003</date>
|
||||
<revremark>
|
||||
Updated to match EXAMPLE included with 0.75.0
|
||||
</revremark>
|
||||
</revision>
|
||||
<revision>
|
||||
<revnumber>0.4</revnumber>
|
||||
<date>26 Feb 2004</date>
|
||||
<revremark>Converted to use Supybot DTD</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 <script>scripts/newplugin.py</script>
|
||||
</title>
|
||||
<para>
|
||||
First, the easiest way to start writing a module is to use the
|
||||
wizard provided, <script>scripts/newplugin.py</script>.
|
||||
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')
|
||||
|
||||
class Random(callbacks.Privmsg):
|
||||
pass
|
||||
|
||||
|
||||
Class = Random
|
||||
|
||||
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:
|
||||
</programlisting>
|
||||
</sect2>
|
||||
<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 <script>scripts/setup.py</script> 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
|
||||
<module>callbacks</module>
|
||||
module is used (the class you're given subclasses
|
||||
<classname>callbacks.Privmsg</classname>) but the
|
||||
<module>privmsgs</module> 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 <script>scripts/setup.py</script>. 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>
|
||||
</sect2>
|
||||
<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
|
||||
<classname>callbacks.Privmsg</classname> 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 <module>random</module> 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
|
||||
<classname>random.Random</classname> object to our plugin, we
|
||||
can replace the <keyword>pass</keyword> 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 <keyword>def</keyword> 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 <classname>Irc</classname>
|
||||
object passed to the command; <varname>msg</varname> is the
|
||||
original <classname>IrcMsg</classname> 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 <classname>Irc</classname> 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
|
||||
<classname>IrcObjectProxy</classname>, 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>
|
||||
<ircsession>
|
||||
<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.
|
||||
</ircsession>
|
||||
<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
|
||||
<classname>IrcMsg</classname> (like the one passed into your
|
||||
function) and a string. The <classname>IrcMsg</classname> 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.rng.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 <classname>IndexError</classname> 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 <keyword>return</keyword> 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, 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)))
|
||||
</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
|
||||
"required" 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 <classname>Random</classname> 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 <classname>callbacks.ArgumentError</classname>! 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, 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
|
||||
</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>required=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 <classname>IrcMsg</classname> 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 <classname>IrcMsg</classname> object.
|
||||
<function>ircutils.replyTo</function> simply takes an
|
||||
<classname>IrcMsg</classname> 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 <script>scripts/newplugin.py</script> 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
|
||||
<script>scripts/setup.py</script> 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
|
||||
<plugin>Random</plugin> 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 <module>questions</module> 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
|
||||
<script>scripts/setup.py</script> creates for the bot.
|
||||
</para>
|
||||
<para>
|
||||
We've written our own plugin from scratch (well, from the
|
||||
boilerplate that we got from
|
||||
<script>scripts/newplugin.py</script> :)) and
|
||||
survived! Now go write more plugins for supybot, and send
|
||||
them to me so I can use them too :)
|
||||
</para>
|
||||
</sect2>
|
||||
</sect1>
|
||||
</article>
|
Loading…
Reference in New Issue
Block a user