mirror of
https://github.com/Mikaela/Limnoria.git
synced 2024-12-22 10:42:55 +01:00
Moving the DocBook equivalent of EXAMPLE to match the name change to
PLUGIN-EXAMPLE
This commit is contained in:
parent
8b150d33c3
commit
25db3c9b98
@ -1,714 +0,0 @@
|
|||||||
<!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