diff --git a/docs/DocBook/Makefile b/docs/DocBook/Makefile new file mode 100644 index 000000000..fb1f24895 --- /dev/null +++ b/docs/DocBook/Makefile @@ -0,0 +1,8 @@ +JADE=/usr/bin/jade +STYLESHEET=/usr/share/sgml/docbook/stylesheet/dsssl/modular/html/docbook.dsl + +example: example.sgml + $(JADE) -t xml -d $(STYLESHEET) $< + +clean: + rm -f *.html diff --git a/docs/DocBook/example.sgml b/docs/DocBook/example.sgml new file mode 100644 index 000000000..bfee0ca8f --- /dev/null +++ b/docs/DocBook/example.sgml @@ -0,0 +1,744 @@ + + +
+ + + + Jeremiah + Fincher + + + Daniel + DiPaolo + DocBook translator + + + Supybot plugin author example + + + 0.1 + 13 Sep 2003 + Initial revision + + + 0.2 + 14 Sep 2003 + Converted to DocBook + + + + + Introduction + + Ok, so you want to write a callback for supybot. Good, then this + is the place to be. We're going to start from the top (the + highest level, where supybot code does the most work for you) and + move lower after that. + + + So have you used supybot? If not, you need to go use it, get a + feel for it, see how the various commands work and such. + + + So now that we know you've used supybot, we'll start getting into + details. + + + + + Creating your own plugin + + + Using <application>scripts/newplugin.py</application> + + + 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'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% + + + So that's what it looks like. Now let's look at the source + code (if you'd like to look at it in your programming editor, + the whole plugin is available as + examples/Random.py): + + +#!/usr/bin/env python + +### +# Copyright (c) 2002, Jeremiah Fincher +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions, and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions, and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the author of this software nor the name of +# contributors to this software may be used to endorse or promote products +# derived from this software without specific prior written consent. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +### + +""" +Add the module docstring here. This will be used by the setup.py script. +""" + +from baseplugin import * + +import utils +import privmsgs +import callbacks + + +def configure(onStart, afterConnect, advanced): + # This will be called by setup.py to configure this module. onStart and + # afterConnect are both lists. Append to onStart the commands you would + # like to be run when the bot is started; append to afterConnect the + # commands you would like to be run when the bot has finished connecting. + from questions import expect, anything, something, yn + onStart.append('load Random') + +example = utils.wrapLines(""" +Add an example IRC session using this module here. +""") + +class Random(callbacks.Privmsg): + pass + + +Class = Random + +# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78: + + + Customizing the boilerplate code + + So a few notes, before we customize it. + + + You'll probably want to change the copyright notice to be your + name. It wouldn't stick even if you kept my name, so you + might as well :) + + + Describe what you want the plugin to do in the docstring. + This is used in scripts/setup.py in + order to explain to the user the purpose of the module. It's + also returned when someone asks the bot for help for a given + module (instead of help for a certain command). We'll change + this one to "Lots of stuff relating to random + numbers." + + + Then there are the imports. The utils + module is used (in example, which we'll see later). The + callbacks module is used (the class you're + given subclasses callbacks.Privmsg) but the + privmsgs module isn't used. That's + alright; we can almost guarantee you'll use it, so we go ahead + and add the import to the template. + + + Then you see a configure function. This + the function that's called when users decide to add your + module in scripts/setup.py. You'll + note that by default it simply adds "load + Example" (where 'Example' is the name you provided + as the name of your plugin, so in our case it is + "load Random") 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. + + + Then there's an example string. It's simply an example of + usage of the plugin in practice. + scripts/setup.py offers to show the + user an example of the module usage; this is what it shows + them. You'll note that it's wrapped for you in + 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. + + + Digging in: customizing the plugin class + + Now comes the meat of the plugin: the plugin class. + + + What you're given is a skeleton: a simple subclass of + callbacks.Privmsg for you to start with. + Now let's add a command. + + + I don't know what you know about random number generators, but + the short of it is that they start at a certain number (a + seed) and they continue (via some somewhat + complicated/unpredictable algorithm) from there. This seed + (and the rest of the sequence, really) is all nice and + packaged up in Python's random module, the + Random object. So the first thing we're + going to have to do is give our plugin a + Random object. + + + Normally, when we want to give instances of a class an object, + we'll do so in the __init__ method. And + that works great for plugins, too. The one thing you have to + be careful of is that you call the superclass + __init__ method at the end of your own + __init__. So to add this + random.Random object to our plugin, we can + replace the pass statement with + this: + + +def __init__(self): + self.rng = random.Random() + callbacks.Privmsg.__init__(self) + + + (rngis an abbreviation for "random number + generator," in case you were curious) + + + Do be careful not to give your __init__ + any arguments (other than self, of course). + There's no way anything will ever get to them! If you have + some sort of initial values you need to get to your plugin + before it can do anything interesting, 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. + + + There's an easier way to get our plugin to have its own rng + than to define an __init__. Plugins are + unique among classes because we're always certain that there + will only be one instance -- supybot doesn't allow us to load + multiple instances of a single plugin. So instead of adding + the rng in __init__, we can just add it + as a attribute to the class itself. Like so (replacing the + pass statement again): + + + rng = random.Random() + + + And we save two lines of code and make our code a little more + clear :) + + + Now that we have an RNG, we need some way to get random + numbers. So first, we'll add a command that simply gets the + next random number and gives it back to the user. It takes no + arguments, of course (what would you give it?). Here's the + command, and I'll follow that with the explanation of what + each part means. + + + def random(self, irc, msg, args): + """takes no arguments + + Returns the next random number generated by the random number + generator. + """ + irc.reply(msg, str(self.rng.random())) + + + And that's it! Pretty simple, huh? Anyway, you're probably + wondering what all that means. We'll + start with the def statement: + + + def random(self, irc, msg, args): + + + What that does is define a command + random. You can call it by saying + "@random" (or whatever prefix character your specific bot + uses). The arguments are a bit less obvious. + self is self-evident (hah!). + irc is the Irc object + passed to the command; msg is the original + IrcMsg object. But you're really not going + to have to deal with either of these too much (with the + exception of calling irc.reply or + irc.error). What you're + really interested in is the + args arg. That 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 want to 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. + + + (In case you're curious, the answer is yes, you + must name your arguments (self, + irc, msg, args). The names of those arguments is + one of the ways that supybot uses to determine which methods + in a plugin class are commands and which aren't. And while + we're talking about naming restrictions, all your commands + should be named in all-lowercase with no underscores. Before + calling a command, supybot always converts the command name to + lowercase and removes all dashes and underscores. On the + other hand, you now know an easy way to make sure a method is + never called (even if its arguments are (self, irc, + msg, args), however unlikely that may be). Just + name it with an underscore or an uppercase letter in it :)) + + + You'll also note that the docstring is odd. The wonderful + thing about the supybot framework is that it's easy to write + complete commands with help and everything: the docstring + is the help! Given the above docstring, + this is what a supybot does: + + + <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. + + + '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: + + + irc.reply(msg, str(self.rng.random())) + + + 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. + + + Anyway, now that we have an RNG, we have a need for seed! Of + course, Python gives us a good seed already (it uses the + current time as a seed if we don't give it one) but users + might want to be able to repeat "random" sequences, so letting + them set the seed is a good thing. So we'll add a seed + command to give the RNG a specific seed: + + + def seed(self, irc, msg, args): + """<seed> + + Sets the seed of the random number generator. <seed> must be + an int or a long. + """ + seed = privmsgs.getArgs(args) + try: + seed = long(seed) + except ValueError: + # It wasn't a valid long! + irc.error(msg, '<seed> must be a valid int or long.') + return + self.rng.seed(seed) + irc.reply(msg, conf.replySuccess) + + + So this one's a bit more complicated. But it's still pretty + simple. The method name is "seed" so that'll be the command + name. The arguments are the same, the docstring is of the + same form, so we don't need to go over that again. The body + of the function, however, is significantly different. + + + privmsgs.getArgs is a function you're + going to be seeing a lot of when you write plugins for + supybot. What it does is basically give you the right number + of arguments for your comamnd. In this case, we want one + argument. But we might have been given any number of + arguments by the user. So + privmsgs.getArgs joins them + appropriately, leaving us with one single "seed" argument (by + default, it returns one argument as a single value; more + arguments are returned in a tuple/list). Yes, we could've + just said "seed = args[0]" and gotten the first argument, but + what if the user didn't pass us an argument at all? Then + we've got to catch the IndexError from + args[0] and complain to the user about it. + privmsgs.getArgs, on the other hand, + handles all that for us. If the user didn't give us enough + arguments, it'll reply with the help string for the command, + thus saving us the effort. + + + So we have the seed from + privmsgs.getArgs. But it's a string. + The next three lines 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. + + + 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 + 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. + + + + Finishing touches + + Let's take a look at that configure + function scripts/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 not 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 :) + + + +