Updated EXAMPLE, finally :)

This commit is contained in:
Jeremy Fincher 2003-09-14 03:33:02 +00:00
parent 2d0671b6f6
commit 24e26dc336
2 changed files with 668 additions and 162 deletions

View File

@ -1,207 +1,541 @@
Here's an example of how to code a few callbacks for SupyBot. 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.
Let's say you want to make an annoying "Mimic" callback that repeats everything So have you used supybot? If not, you need to go use it, get a feel
anyone says to the bot or on the channels the bot is in. That for it, see how the various commands work and such.
shouldn't be too hard. First, you'll need to make a new module to
hold the plugin:
$ scripts/newplugin.py Mimic So now that we know you've used supybot, we'll start getting into
details.
That'll make the file plugins/Mimic.py, which will First, the easiest way to start writing a module is to use the wizard
provided, scripts/newplugin.py. Here's an example session:
-----
functor% scripts/newplugin.py
What should the name of the plugin be? Random
Supybot offers two major types of plugins: command-based and regexp-
based. Command-based plugins are the kind of plugins you've seen most
when you've used supybot. They're also the most featureful and
easiest to write. Commands can be nested, for instance, whereas
regexp-based callbacks can't do nesting. That doesn't mean that
you'll never want regexp-based callbacks. They offer a flexibility
that command-based callbacks don't offer; however, they don't tie into
the whole system as well. If you need to combine a command-based
callback with some regexp-based methods, you can do so by subclassing
callbacks.PrivmsgCommandAndRegexp and then adding a class-level
attribute "regexps" that is a sets.Set of methods that are regexp-
based. But you'll have to do that yourself after this wizard is
finished :)
Do you want a command-based plugin or a regexp-based plugin? [command/
regexp] command
Sometimes you't want a callback to be threaded. If its methods
(command or regexp-based, either one) will take a signficant amount
of time to run, you'll want to thread them so they don't block
the entire bot.
Does your plugin need to be threaded? [y/n] n
Your new plugin template is in plugins/Random.py
functor%
-----
So that's what it looks like. Now let's look at the source code:
-----
#!/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
[code] def configure(onStart, afterConnect, advanced):
class AnnoyingMimic(irclib.IrcCallback): # This will be called by setup.py to configure this module. onStart and
def doPrivmsg(self, irc, msg): # afterConnect are both lists. Append to onStart the commands you would
irc.queueMsg(ircmsgs.privmsg(msg.args[0], msg.args[1])) # like to be run when the bot is started; append to afterConnect the
[/code] # commands you would like to be run when the bot has finished connecting.
from questions import expect, anything, something, yn
onStart.append('load Random')
Almost every callback will inherit from irclib.IrcCallback somewhere in their example = utils.wrapLines("""
class hierarchy. irclib.IrcCallback does a lot of the basic stuff that call- Add an example IRC session using this module here.
backs have to do, and inheriting from it relieves the programmer from such """)
pedantries. All you have to do to start writing callbacks inheriting from
irclib.IrcCallback is write functions of the form "doCommand", where "Command"
is a valid ircCommand. The "ChannelLogger" plugin is a good illustrative
example of a callback being called on different commands.
The "irc" argument there is the irc object that is calling the callback. To class Random(callbacks.Privmsg):
see what kind of interface it provides, read the class definition in irclib. pass
Really, you only need to know a few methods if you're writing simple callbacks:
'queueMsg', which queues a message to be sent later, and 'sendMsg' which tries
to send it right away (well, as soon as the Irc object's driver asks for
another message to send, which is generally right away). The Irc object also
provides some attributes that might come in useful, most notably "nick" (the
nick of the bot) and "state" (an IrcState object that does various useful
things like keeping a history of the most recent irc messages.)
Irc messsages are represented by the IrcMsg class in ircmsgs. It has several
useful methods and attributes, but it's probably easier for you to read the
code than for me to tell you about it. The ircmsgs module also provides a set
of useful little commands to create IrcMsg objects that do particular little
things; for instance, ircmsgs.privmsg(recipient, msg) sends a PRIVMSG command
to a channel or user (whatever recipient turns out to be). Check out the code
to see other functions for making IrcMsg objects.
Now, that wasn't too bad. Now, however you're going to have to get it into the Class = Random
bot. Note that AnnoyingMimic doesn't have an __init__. This'll make it pretty
simple to get it into the configuration system. Look for the section of the
config file where you see all the configurations for Irc objects. These
configurations are going to be lists of (class name, args, kwargs) tuples which
contain the name of a callback class to be instantiated, a tuple of the argu-
ments to be passed to the __init__ function for that class, and a dictionary
of the keyword arguments to be passed to the __init__ function of that class.
For instance, if AnnoyingMimic was in a file 'mycallbacks.py', its config-
uration in the config file would look like this:
[code] # vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:
('mycallbacks.AnnoyingMimic', (), {}) -----
[/code]
Since it doesn't have an __init__, there are no arguments or keyword arguments So a few notes, before we customize it.
to pass to the class. Just throw something like that in a list of callbacks
that you use for your bot (you can have several lists, you'll notice later on
in the 'drivers' variable that they're used), and you're ready to go!
Now, let's say you want to make your AnnoyingMimic class a little less You'll probably want to change the copyright notice to be your name.
annoying. Now, you only want to mimic people *you* find annoying. The easiest It wouldn't stick even if you kept my name, so you might as well :)
way to do that is to make it so you tell the class who to mimic when you
instantiate it. This means adding an __init__ function, and modifying your
configuration slightly.
[code] Describe what you want the plugin to do in the docstring. This is
class AnnoyingMimic(irclib.IrcCallback): used in scripts/setup.py in order to explain to the user the purpose
def __init__(self, nicksToAnnoy): of the module. It's also returned when someone asks the bot for help
self.nicksToAnnoy = nicksToAnnoy for a given module (instead of help for a certain command). We'll
change this one to "Lots of stuff relating to random numbers."
def doPrivmsg(self, irc, msg): Then there are the imports. The utils module is used (in example,
if msg.nick() in self.nicksToAnnoy: which we'll see later). The callbacks module is used (the class
irc.queueMsg(ircmsgs.privmsg(msg.args[0], msg.args[1])) you're given subclasses callbacks.Privmsg) but the privmsgs module
[/code] 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.
(Now, really, to make this efficient, you'd want a slightly different version Then you see a "configure" function. This the function that's called
that turned the nicksToAnnoy argument into a dictionary so nick lookups would when users decide to add your module in scripts/setup.py. You'll note
be O(1) instead of O(n) in the length of the list of nicks to annoy, but that that by default it simply adds "load Example" at the bottom. For many
would obfuscate the problem. I'll leave that as an exercise left up to the plugins this is all you need; for more complex plugins, you might need
reader.) to ask questions and add commands based on the answers.
So now your AnnoyingMimic class has an __init__ function that accepts a list Then there's an example string. It's simply an example of usage of
of nicks to annoy, but how do you pass it those nicks? Simple! Change the the plugin in practice. scripts/setup.py offers to show the user an
configuration slightly: example of the module usage; this is what it shows them. You'll note
that it's wrapped for you in utils.wrapLines so you don't have to
bother with it; just paste a session directly out of your IRC client
and you'll be set.
[code] Now comes the meat of the plugin: the plugin class.
('mycallbacks.AnnoyingMimic', (['jemfinch', 'GnuVince'],), {})
[/code]
That's the wonder of this configuration system -- you can use all the Python What you're given is a skeleton: a simple subclass of
syntax you want, so you have practically unlimited flexibility. callbacks.Privmsg for you to start with. Now let's add a command.
(Note that since the 'arguments' member of that tuple is a single-member tuple, I don't know what you know about random number generators, but the
you'll have to stick a comma after the first (only) element because otherwise short of it is that they start at a certain number (a seed) and they
Python wouldn't believe it's a tuple.) continue (via some somewhat complicated/unpredictable algorithm) from
there. This seed (and the rest of the sequence, really) is all nice
and packaged up in Python's random module, the Random object. So the
first thing we're going to have to do is give our plugin a Random
object.
So, again, you choose to make your AnnoyingMimic less annoying -- really, you Normally, when we want to give instances of a class an object, we'll
decide to make it not annoying at all by making it only mimic people who ask to do so in the __init__ method. And that works great for plugins, too.
be repeated. You want to make a class that has an "echo" command that repeats The one thing you have to be careful of is that you call the
the message to those who ask it. You want people to be able to tell the bot, superclass __init__ method at the end of your own __init__. So to add
"echo The quick brown fox jumps over the lazy dog!" and have the bot say right this random.Random object to our plugin, we can replace the "pass"
back, "The quick brown fox jumps over the lazy dog!". That's easy! Here's the statement with this:
code:
[code] def __init__(self):
reply = callbacks.reply self.rng = random.Random()
callbacks.Privmsg.__init__(self)
class Echo(callbacks.Privmsg): (rng is an abbreviation for "random number generator," in case you
def echo(self, irc, msg, args): were curious)
"<text>"
text = self.getArgs(args)
self.reply(text)
[/code]
So that seemed pretty simple there, too. Let's explain what's going on: Do be careful not to give your __init__ any arguments (other than
self, of course). There's no way anything will ever get to them! If
you have some sort of initial values you need to get to your plugin
before it can do anything interesting, add a command that gets those
values. By convention, those commands begin with "start" -- check out
the Relay and Enforcer plugins for examples of such commands.
callbacks.Privmsg is an easy way to create "commands" which are simple named There's an easier way to get our plugin to have its own rng than to
functions that use a universal scheme for delimiting arguments -- basically, define an __init__. Plugins are unique among classes because we're
they'll all act the same in how they get their arguments. callbacks.Privmsg always certain that there will only be one instance -- supybot doesn't
takes a Privmsg (it has a doPrivmsg function and inherits from allow us to load multiple instances of a single plugin. So instead of
irclib.IrcCallback) and first determines if it's addressed to the bot -- the adding the rng in __init__, we can just add it as a attribute to the
message must either be PRIVMSGed directly to the bot, or PRIVMSGed over a class itself. Like so (replacing the "pass" statement again):
channel the bot is in and either start with a character in conf.prefixchars or
start with the bot's name. Don't worry, callbacks.Privmsg almost always does
The Right Thing. After deciding that the bot has been addressed,
callbacks.Privmsg then parses the text of the message into a list of strings.
Here are a few examples of what it would do:
"""arg1 arg2 arg3""" rng = random.Random()
['arg1', 'arg2', 'arg3']
"""'arg1 arg2 arg3' arg4""" # Note the quotes. And we save two lines of code and make our code a little more clear :)
['arg1 arg2 arg3', 'arg4']
getArgs is a function that just a little bit of magic. It takes an optional Now that we have an RNG, we need some way to get random numbers. So
argument (that defaults to 1) of the number of args needed. If more than one first, we'll add a command that simply gets the next random number and
argument is needed, it checks that the proper number of arguments has been gives it back to the user. It takes no arguments, of course (what
given, and then returns a tuple of those arguments. So if you wanted 3 args would you give it?) Here's the command, and I'll follow that with the
from a message, you'd do something like this: explanation of what each part means.
(name, oldpassword, newpassword) = self.getArgs(args, 3) def random(self, irc, msg, args):
"""takes no arguments
See how simple that is? If getArgs only needs one argument, however, it does Returns the next random number generated by the random number
something a bit magic -- first of all, it doesn't return a tuple, it just generator.
returns the argument itself. This makes it so you can type: """
irc.reply(msg, str(self.rng.random()))
text = self.getArgs(args) And that's it! Pretty simple, huh? Anyway, you're probably wondering
what all that *means*. We'll start with the def statement:
Instead of: def random(self, irc, msg, args):
(text,) = self.getArgs(args) What that does is define a command "random". You can call it by
saying "@random" (or whatever prefix character your specific bot
uses). The arguments are a bit less obvious. Self is self-evident
(hah!). Irc is the Irc object passed to the command; Msg is the
original IrcMsg object. But you're really not going to have to deal
with either of these too much (with the exception of calling irc.reply
or irc.error). What you're *really* interested in is the args arg.
That if a list of all the arguments passed to your command, pre-parsed
and already evaluated (i.e., you never have to worry about nested
commands, or handling double quoted strings, or splitting on
whitespace -- the work has already been done for you). You can read
about the Irc object in irclib.py (you won't find .reply or .error
there, though, because you're actually getting an IrcObjectProxy, but
that's beyond the level we wanna describe here :)). You can read
about the msg object in ircmsgs.py. But again, aside from calling
irc.reply or irc.error, you'll very rarely be using these objects.
It just makes things easier that way. Also, however, if *only* one argument (In case you're curious, the answer is yes, you *must* name your
is needed, it does something a bit more magical. A lot of commands take only arguments (self, irc, msg, args). The names of those arguments is one
one argument and then do some processing on it -- for example, look at the of the ways that supybot uses to determine which methods in a plugin
privmsgs module, the "FunCommands" callback, at the commands 'leet' and class are commands and which aren't. And while we're talking about
'rot13'. This is all great, but because of the way args are normally parsed naming restrictions, all your commands should be named in
by callbacks.Privmsg, you'd have to always enclose that argument in quotes. all-lowercase with no underscores. Before calling a command, supybot
For instance, you'd have to type this: always converts the command name to lowercase and removes all dashes
and underscores. On the other hand, you now know an easy way to make
sure a method is never called (even if its arguments are (self, irc,
msg, args), however unlikely that may be). Just name it with an
underscore or an uppercase letter in it :))
bot: leet "The quick brown fox jumps over the lazy dog." You'll also note that the docstring is odd. The wonderful thing about
the supybot framework is that it's easy to write complete commands
with help and everything: the docstring *IS* the help! Given the
docstring, this is what a supybot does:
From experience, I can tell you that most people will forget the quotes almost <angryman> jemfinch: random takes no arguments (for more help
every time they talk to the bot. Since having only one argument is such a use the morehelp command)
command case, getArgs special-cases it to string.join all the args with spaces. <jemfinch> $morehelp random
Now you can say: <angryman> jemfinch: Returns the next random number from the
current random number generator.
bot: leet The quick brown fox jumps over the lazy dog. Help <command> replies with 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:
And it'll return the same exact thing as above. Of course, the original still irc.reply(msg, str(self.rng.random()))
works, but since people forget the quotes so often, it's good to go easy on
them :) We're actually using that behavior with our callback above: by using
getArgs, now our users can say:
echo foo bar baz Irc.reply takes two arguments, an IrcMsg (like the one passed into
your function) and a string. The IrcMsg is used to determine who the
reply should go to and whether or not it should be sent in private
message (commands sent in private are replied to in private). The
string is the reply to be sent. Don't worry about length restrictions
or anything -- if the string you want to send is too big for an IRC
message (and oftentimes that turns out to be the case :)) the supybot
framework handles that entirely transparently to you. Do make sure,
however, that you give irc.reply a string. It doesn't take anything
else (sometimes even unicode fails!). That's why we have
"str(self.rnd.random())" instead of simply "self.rng.random()" -- we
had to give irc.reply a string.
Instead of always having to say: 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:
echo "foo bar baz" def seed(self, irc, msg, args):
"""<seed>
Anyway, you're probably wondering how that callback works. It inherits from Sets the seed of the random number generator. <seed> must be
callbacks.Privmsg, which as I mentioned before, has a doPrivmsg callback. So an int or a long.
when callbacks.Privmsg receives a PRIVMSG command, it parses it and then tries """
to find if it has a method by the same name as the command -- if it does, and seed = privmsgs.getArgs(args)
that method looks like this: 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)
def method(self, irc, msg, args): 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.
Then it calls that method with the appropriate arguments. Easy, huh? Don't privmsgs.getArgs is a function you're going to be seeing a lot of when
worry, it gets even cooler :) So you write a command like echo and you want you write plugins for supybot. What it does is basically give you the
to provide the user with some help using it. You were probably wondering why right number of arguments for your comamnd. In this case, we want one
the docstring to that "echo" method above looked so weird, but now you know: argument. But we might have been given any number of arguments by the
it *is* the help for the command! callbacks.Privmsg has its own command, help, user. So privmsgs.getArgs joins them appropriately, leaving us with
which will return the *docstring* for any other command! So it's cake to write one single "seed" argument (by default, it returns one argument as a
your own commands and help. single value; more arguments are returned in a tuple/list). Yes, we
could've just said "seed = args[0]" and gotten the first argument, but
what if the user didn't pass us an argument at all? Then we've got to
catch the IndexError from args[0] and complain to the user about it.
Privmsgs.getArgs, on the other hand, handles all that for us. If the
user didn't give us enough arguments, it'll reply with the help string
for the command, thus saving us the effort.
(This, of course, means that if you *don't* write a help string for your So we have the seed from privmsgs.getArgs. But it's a string. The
command, you have no excuse and are just plain lazy. So write help strings!) next three lines is pretty darn obvious: we're just converting the
string to a int of some sort. But if it's not, that's when we're
going to call irc.error. It has the same interface as we saw before
in irc.reply, but it makes sure to remind the user that an error has
been encountered (currently, that means it puts "Error: " at the
beginning of the message). After erroring, we return. It's important
to remember this return here; otherwise, we'll just keep going down
through the function and try to use this "seed" variable that never
got assigned. A good general rule of thumb is that any time you use
irc.error, you'll want to return immediately afterwards.
There's a bit more I could tutorialize on, but it would be more esoteric, and Then we set the seed -- that's a simple function on our rng object.
better a reference material than as a tutorial. I'll put that in another file. Assuming that succeeds (and doesn't raise an exception, which it
shouldn't, because we already read the documentation and know that it
should work) we reply to say that everything worked fine. That's what
conf.replySuccess says. By default, it has the very dry (and
appropriately robot-like) "The operation succeeded." but you're
perfectly welcome to customize it yourself -- conf.py was written to
be modified!
So that's a bit more complicated command. But we still haven't dealt
with multiple arguments. Let's do that next.
So these random numbers are useful, but they're not the kind of random
numbers we usually want in Real Life. In Real Life, we like to tell
someone to "pick a number between 1 and 10." So let's write a
function that does that. Of course, we won't hardcode the 1 or the 10
into the function, but we'll take them as arguments. First the
function:
def range(self, irc, msg, args):
"""<start> <end>
Returns a number between <start> and <end>, inclusive (i.e., the number
can be either of the endpoints.
"""
(start, end) = privmsgs.getArgs(args, 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)))
Pretty simple. This is becoming old hat by now. The only new thing
here is the call to privmsgs.getArgs. We have to make sure, since we
want two values, to pass a keyword parameter "needed" into
privmsgs.getArgs. Of course, privmsgs.getArgs handles all the
checking for missing arguments and whatnot so we don't have to.
The Random object we're using offers us a "sample" method that takes a
sequence and a number (we'll call it N) and returns a list of N items
taken randomly from the sequence. So I'll show you an example that
takes advantage of multiple arguments but doesn't use
privmsgs.getArgs (and thus has to handle its own errors if the number
of arguments isn't right). Here's the code:
def sample(self, irc, msg, args):
"""<number of items> [<text> ...]
Returns a sample of the <number of items> taken from the remaining
arguments. Obviously <number of items> must be less than the number
of arguments given.
"""
try:
n = int(args.pop(0))
except IndexError: # raised by .pop(0)
raise callbacks.ArgumentError
except ValueError:
irc.error(msg, '<number of items> must be an integer.')
return
if n > len(args):
irc.error(msg, '<number of items> must be less than the number '
'of arguments.')
return
sample = self.rng.sample(args, n)
irc.reply(msg, utils.commaAndify(map(repr, sample)))
Most everything here is familiar. The difference between this and the
previous examples is that we're dealing with args directly, rather
than through getArgs. Since we already have the arguments in a list,
it doesn't make any sense to have privmsgs.getArgs smush them all
together into a big long string that we'll just have to re-split. But
we still want the nice error handling of privmsgs.getArgs. So what do
we do? We raise callbacks.ArgumentError! That's the secret juju
that privmsgs.getArgs is doing; now we're just doing it ourself.
Someone up our callchain knows how to handle it so a neat error
message is returned. So in this function, if .pop(0) fails, we
weren't given enough arguments and thus need to tell the user how to
call us.
So we have the args, we have the number, we do a simple call to
random.sample and then we do this funky utils.commaAndify to it.
Yeah, so I was running low on useful names :) Anyway, what it does is
take a list of strings and return a string with them joined by a
comma, the last one being joined with a comma and "and". So the list
['foo', 'bar', 'baz'] becomes "foo, bar, and baz". It's pretty useful
for showing the user lists in a useful form. We map the strings with
repr() first just to surround them with quotes.
So we have one more example. Yes, I hear your groans, but it's
pedagogically useful :) This time we're going to write a command that
makes the bot roll a die. It'll take one argument (the number of
sides on the die) and will respond with the equivalent of "/me rolls a
__" where __ is the number the bot rolled. So here's the code:
def diceroll(self, irc, msg, args):
"""[<number of sides>]
Rolls a die with <number of sides> sides. The default number
of sides is 6.
"""
try:
n = privmsgs.getArgs(args, 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
There's a lot of stuff you haven't seen before in there. The most
important, though, is the first thing you'll notice that's different:
the privmsg.getArgs call. Here we're offering a default argument in
case the user is too lazy to supply one (or just wants a nice,
standard six-sided die :)) Privmsgs.getArgs supports that; we'll just
tell it that we don't *need* any arguments (via needed=0) and that we
*might like* one argument (optional=1). If the user provides an
argument, we'll get it -- if they don't, we'll just get an empty
string. Hence the "if not n: n = 6", where we provide the default.
Later, though, you'll see something other than irc.reply. This is
irc.queueMsg, the general interface for sending messages to the
server. It's what irc.reply is using under the covers. It takes an
IrcMsg object. Fortunately, that's exactly what's returned by
ircmsgs.action. An action message, just in case you don't know, is a
/me kind of message. ircmsgs.action is a helper function that takes a
target (a place to send the message, either a channel or a person) and
a payload (the thing to /me) and returns the appropriate IrcMsg
object. ircutils.replyTo simply takes an IrcMsg and returns where we
should reply to; if the message was originally sent to a channel,
we'll reply to there, if it was originally sent to us privately, we'll
reply in private.
At the end, you might be surprised by the "raise
callbacks.CannotNest". That's used simply because at the moment you
can't nest actions (just like you can't nest anything that doesn't go
through irc.reply). That raise just makes sure the user finds this
out if he tries to nest this like "@rot13 [diceroll]".
So that's our plugin. 5 commands, each building in complexity. You
should now be able to write most anything you want to do in Supybot.
Except regexp-based plugins, but that's a story for another day (and
those aren't nearly as cool as these command-based callbacks anyway
:)). Now we need to flesh it out to make it a full-fledged plugin.
Let's take a look at that configure function newplugin.py made for
us. Here it is, in case you've forgotten:
def configure(onStart, afterConnect, advanced):
# This will be called by setup.py to configure this module. onStart and
# afterConnect are both lists. Append to onStart the commands you would
# like to be run when the bot is started; append to afterConnect the
# commands you would like to be run when the bot has finished connecting.
from questions import expect, anything, something, yn
onStart.append('load Random')
You remember when you first started running supybot and ran
scripts/setup.py and it asked you all those questions? Well, now's
your chance to ask other users some questions of your own. In our
case, with our Random plugin, it might be nice to offer the user the
ability to specify a seed to use whenever the plugin is loaded. So
let's ask him if he wants to do that, and if so, let's ask him what
the seed should be.
def configure(onStart, afterConnect, advanced):
# This will be called by setup.py to configure this module. onStart and
# afterConnect are both lists. Append to onStart the commands you would
# like to be run when the bot is started; append to afterConnect the
# commands you would like to be run when the bot has finished connecting.
from questions import expect, anything, something, yn
onStart.append('load Random')
if yn('Do you want to specify a seed to be used for the RNG')=='y':
seed = something('What seed? It must be an int or long.')
while not seed.isdigit():
print 'That\'s not a valid seed.'
seed = something('What seed?')
onStart.append('seed %s' % seed)
As you can see, what the questions module does is fairly self-evident:
yn returns either 'y' or 'n'; something returns *something* (but
nothing; for nothing, you'd want anything). So basically we ask some
questions until we get a good seed. Then we do this
"onStart.append('seed %s' % seed)" doohickey. onStart is a list of
the commands to run when the bot starts; we're just throwing our
little piece into it. These commands will then be written into the
template scripts/setup.py creates for the bot.
Now the only thing missing from our plugin is an example. Here, I'll
make one really quickly:
<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
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
scripts/newplugin.py :)) and survived! Now go write more plugins for
supybot, and send them to me so I can use them too :)

172
examples/Random.py Normal file
View File

@ -0,0 +1,172 @@
#!/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.
###
"""
Lots of stuff relating to random numbers.
"""
from baseplugin import *
import random
import conf
import utils
import ircmsgs
import ircutils
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')
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)
example = utils.wrapLines("""
<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
""")
class Random(callbacks.Privmsg):
rng = random.Random()
def random(self, irc, msg, args):
"""takes no arguments
Returns the next random number from the random number
generator.
"""
irc.reply(msg, str(self.rng.random()))
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)
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)))
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)))
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)
irc.queueMsg(ircmsgs.action(ircutils.replyTo(msg), s))
raise callbacks.CannotNest
Class = Random
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78: