jemfinch
07-23-2002, 04:37 PM
Supybot version 0.3 is nearing release...I've got a few more hours of coding to do to work out some kinks, and then I'm going to need testers galore (especially because I don't actually have an internet connection on the box I develop on, and don't have Python on the box I use the internet from :))
This latest version does all the things right that I've not done right in any released code before (a lot of the ideas are gleaned from Ocamlbot version 0.2, but I never released that). Most importantly, it actually has a working configuration system that anyone who knows rudimentary Python syntax (lists, strings, and ints are enough) can manipulated. Additionally, it's decently well documented and commented, so writing plugins should really be cake (even without documentation, the interface for writing plugins is just awesome :))
I've got a few things to clear up before I can release; most notably I have to write an actual network driver for the bot. I've tons of experience with asyncore/asynchat, though, and can probably whip one up in about 20 minutes. I won't have much of a way to test that, though, so some brave souls will have to test and post exceptions for me to fix on the fly one night.
Things that won't be implemented before the initial release include dynamic reloading of modules and configuration, a test suite, a Twisted (http://www.twistedmatrix.com/) network driver, and a few callbacks that I need to re-implement from my Ocamlbot (quotes database, factoid database, things like that). Things that will be implemented are bot/user/channel control, user and channel permissions, multiple servers/channels, and several "fun" callbacks that are good for testing the robustness of the bot. (Btw, if you've ever had to configure user/channel permissions for any other bot, you'll love how it's done with this bot :))
In the far future, I plan to write a distributed user/channel configuration database, which should be easy to do with Twisted.
Like all my stuff, it's 2-clause BSDL'ed, so you're welcome to steal the code for whatever purpose you want. You're also welcome to implement and submit any new callbacks or whatnot that you'd like to see included, and I'll be happy to throw them in (maybe with slight modifications to maintain consistent style throughout the bot). Writing new callbacks is really cake for anyone who understands Python.
The bot can run from the command line, btw -- if you know the IRC protocol, you can talk to it over a little shell I wrote for testing purposes, so even if you don't want to use the bot online, you can easily use it over the shell. This could also come in useful if the bot ever gets to be as useful as, say, "apt" from #debian on OPN, because then you can just use it as a little intelligent helper on the command line.
Oh, and don't tell anyone you know is an Arstechnica fan about it -- I'm not quite ready for the bot to "go public" there, because it's going to replace my Ocamlbot that runs #linux on irc.arstechnica.com, and I don't want to have any pressure to finish it faster or push it into service before it's ready.
(In case you're curious, it's "Supybot 0.3" because it's my third rewrite of a bot in Python. "Ocamlbot 0.2" was my second rewrite of a bot in O'Caml, obviously :) Supybot 0.[12] and Ocamlbot 0.1 definitely taught me a lot about writing bots, and I think it'll show through in this bot.)
Anyway, it's just a warning -- keep your eye on this thread, I'll probably post the bot sometime later this week.
Jeremy
GnuVince
07-23-2002, 05:10 PM
Speaking of your O'Caml bot, could I see the source please?
inkedmn
07-23-2002, 05:58 PM
jeremy
if you want to post the source, i'd be happy to play with it a bit :)
jemfinch
07-23-2002, 09:55 PM
Originally posted by GnuVince
Speaking of your O'Caml bot, could I see the source please?
If I ever get back 0.2 (which is languishing on a UFS filesystem right now while I'm in XP) I'll be happy to give it to you. I'd rather not publish 0.1, though, because it's embarrasingly ugly and rather kludgy in general.
Keep in mind that 0.1 grew out of my desire to translate numerous Python modules into O'Caml. 0.2 grew out of what I learned from 0.1 :)
Originally posted by inkedmn
jeremy
if you want to post the source, i'd be happy to play with it a bit :)
Well, that's what this post is about :) I'm adding some final touches, improving the code a bit, and posting it here in this thread soon.
It's really amazing, however, what's possible with a mere 1100 lines of Python code. By the time I post the code, I'm sure it'll be up to 1500, but it's still amazing how much can be done.
Jeremy
jemfinch
07-24-2002, 10:21 AM
I coded most of the things I needed to code before release last night. Now I've downloaded two ircds (hopefully one of which will work in Cygwin) and I hope to be able to do some testing tonight.
By the way, if any of you aren't using PyChecker, you should start now. It's absolutely the only way I can write my code without fixing/testing/debugging every other statement. It's been saving me throughout my coding of this project.
(the codebase, btw, it up to about 1500 lines of code now, but that's including comments and blank lines.)
Jeremy
jemfinch
07-25-2002, 07:32 PM
Here's from the EXAMPLE file for the bot:
Here's an example of how to code a few callbacks for SupyBot.
Let's say you want to make an annoying "Mimic" callback that repeats everything
anyone says to the bot or on the channels the bot is in. Here's what it looks
like:
class AnnoyingMimic(irclib.IrcCallback):
def doPrivmsg(self, irc, msg):
irc.queueMsg(ircmsgs.privmsg(msg.args[0], msg.args[1]))
Almost every callback will inherit from irclib.IrcCallback somewhere in their
class hierarchy. irclib.IrcCallback does a lot of the basic stuff that call-
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 "ChannelJoiner" function in the callbacks module
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
see what kind of interface it provides, read the class definition in irclib.
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
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:
('mycallbacks.AnnoyingMimic', (), {})
Since it doesn't have an __init__, there are no arguments or keyword arguments
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
annoying. Now, you only want to mimic people *you* find annoying. The easiest
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.
class AnnoyingMimic(irclib.IrcCallback):
def __init__(self, nicksToAnnoy):
self.nicksToAnnoy = nicksToAnnoy
def doPrivmsg(self, irc, msg):
if msg.nick() in self.nicksToAnnoy:
irc.queueMsg(ircmsgs.privmsg(msg.args[0], msg.args[1]))
(Now, really, to make this efficient, you'd want a slightly different version
that turned the nicksToAnnoy argument into a dictionary so nick lookups would
be O(1) instead of O(n) in the length of the list of nicks to annoy, but that
would obfuscate the problem. I'll leave that as an exercise left up to the
reader.)
So now your AnnoyingMimic class has an __init__ function that accepts a list
of nicks to annoy, but how do you pass it those nicks? Simple! Change the
configuration slightly:
('mycallbacks.AnnoyingMimic', (['jemfinch', 'gnuvince'],), {})
That's the wonder of this configuration system -- you can use all the Python
syntax you want, so you have practically unlimited flexibility.
(Note that since the 'arguments' member of that tuple is a single-member tuple,
you'll have to stick a comma after the first (only) element because otherwise
Python wouldn't believe it's a tuple.)
So, again, you choose to make your AnnoyingMimic less annoying -- really, you
decide to make it not annoying at all by making it only mimic people who ask to
be repeated. You want to make a class that has an "echo" command that repeats
the message to those who ask it. You want people to be able to tell the bot,
"echo The quick brown fox jumps over the lazy dog!" and have the bot say right
back, "The quick brown fox jumps over the lazy dog!". That's easy! Here's the
code:
reply = callbacks.reply
class Echo(callbacks.Privmsg):
def echo(self, irc, msg, args):
"<text>"
text = self.getArgs(args)
irc.queueMsg(reply(msg, text))
So that seemed pretty simple there, too. Let's explain what's going on:
callbacks.Privmsg is an easy way to create "commands" which are simple named
functions that use a universal scheme for delimiting arguments -- basically,
they'll all act the same in how they get their arguments. callbacks.Privmsg
takes a Privmsg (it has a doPrivmsg function and inherits from
irclib.IrcCallback) and first determines if it's addressed to the bot -- the
message must either be PRIVMSGed directly to the bot, or PRIVMSGed over a
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"""
['arg1', 'arg2', 'arg3']
"""'arg1 arg2 arg3' arg4""" # Note the quotes.
['arg1 arg2 arg3', 'arg4']
getArgs is a function that just a little bit of magic. It takes an optional
argument (that defaults to 1) of the number of args needed. If more than one
argument is needed, it checks that the proper number of arguments has been
given, and then returns a tuple of those arguments. So if you wanted 3 args
from a message, you'd do something like this:
(name, oldpassword, newpassword) = self.getArgs(args, 3)
See how simple that is? If getArgs only needs one argument, however, it does
something a bit magic -- first of all, it doesn't return a tuple, it just
returns the argument itself. This makes it so you can type:
text = self.getArgs(args)
Instead of:
(text,) = self.getArgs(args)
It just makes things easier that way. Also, however, if *only* one argument
is needed, it does something a bit more magical. A lot of commands take only
one argument and then do some processing on it -- for example, look at the
privmsgs module, the "FunCommands" callback, at the commands 'leet' and
'rot13'. This is all great, but because of the way args are normally parsed
by callbacks.Privmsg, you'd have to always enclose that argument in quotes.
For instance, you'd have to type this:
bot: leet "The quick brown fox jumps over the lazy dog."
From experience, I can tell you that most people will forget the quotes almost
every time they talk to the bot. Since having only one argument is such a
command case, getArgs special-cases it to string.join all the args with spaces.
Now you can say:
bot: leet The quick brown fox jumps over the lazy dog.
And it'll return the same exact thing as above. Of course, the original still
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
Instead of always having to say:
echo "foo bar baz"
Anyway, you're probably wondering how that callback works. It inherits from
callbacks.Privmsg, which as I mentioned before, has a doPrivmsg callback. So
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
that method looks like this:
def method(self, irc, msg, args):
...
Then it calls that method with the appropriate arguments. Easy, huh? Don't
worry, it gets even cooler :) So you write a command like echo and you want
to provide the user with some help using it. You were probably wondering why
the docstring to that "echo" method above looked so weird, but now you know:
it *is* the help for the command! callbacks.Privmsg has its own command, help,
which will return the *docstring* for any other command! So it's cake to write
your own commands and help.
(This, of course, means that if you *don't* write a help string for your
command, you have no excuse and are just plain lazy. So write help strings!)
There's a bit more I could tutorialize on, but it would be more esoteric, and
better a reference material than as a tutorial. I'll put that in another file.
Jeremy
jemfinch
07-25-2002, 07:33 PM
I'd like to do just a bit more testing and maybe get module reloading to work before I post the code -- but it's looking good :) Most everything I've wanted to implement before release is done; the only real reason I want to implement module reloading before release is because it makes debugging soooo much faster, and I'd like to do a lot of that before I post the code :)
(Ew, the above is sooo ugly! I wish it wouldn't do that, but there are hard newlines in the file, unfortunately.)
Jeremy
vBulletin® v3.7.0, Copyright ©2000-2009, Jelsoft Enterprises Ltd.