From 25db3c9b983116d6380a38cd40b63d13700ed7e7 Mon Sep 17 00:00:00 2001 From: Daniel DiPaolo Date: Sun, 5 Sep 2004 04:55:39 +0000 Subject: [PATCH] Moving the DocBook equivalent of EXAMPLE to match the name change to PLUGIN-EXAMPLE --- docs/DocBook/example.sgml | 714 -------------------------------------- 1 file changed, 714 deletions(-) delete mode 100644 docs/DocBook/example.sgml diff --git a/docs/DocBook/example.sgml b/docs/DocBook/example.sgml deleted file mode 100644 index 8f0987c84..000000000 --- a/docs/DocBook/example.sgml +++ /dev/null @@ -1,714 +0,0 @@ - - -
- - - - 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 - - - 0.3 - 24 Nov 2003 - - Updated to match EXAMPLE included with 0.75.0 - - - - 0.4 - 26 Feb 2004 - Converted to use Supybot DTD - - - - - 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 <script>scripts/newplugin.py</script> - - - First, the easiest way to start writing a module is to use the - wizard provided, . - 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') - -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 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 - 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 . 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. - - - - 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.rng.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, 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))) - - - 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 - "required" 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, 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 - - - 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 - required=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 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 - 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 - creates for the bot. - - - We've written our own plugin from scratch (well, from the - boilerplate that we got from - :)) and - survived! Now go write more plugins for supybot, and send - them to me so I can use them too :) - - - -