PDA

View Full Version : It's here! (look inside for a nice little IRC bot to hack on :))


jemfinch
07-26-2002, 04:08 AM
I'm finally releasing my first official project EVER!

It's an IRC bot. After more than a year now of coding IRC bots in both Python and O'Caml, I've written one I think I can really be proud of -- in Python. It has a number of very cool capabilities already written in, but even more importantly it's written to be very understandable and modifiable. And it's not horribly documented, either, so between reading the documentation and reading the code, it should be cake to make your own callbacks/commands and additions.

I've even got an EXAMPLE file that shows how to write your own callbacks or commands.

If you have problems, this is the thread to post them in. I'll respond as soon as possible, and will try to fix any bugs almost immediately, though I don't develop on the same box that I internet from, so there's probably going to be at least a day lag between posting a bug and having the new zip file with the bugfix posted. I won't pretend that there aren't bugs in there, and I'm sure hoping some of you people find them so I can fix them :)

If you want advice, I'm all ears. If you want to give advice, I'm all ears.

wc -l tells me that there are nearly 2,400 lines of code in my .py files. Given the large amounts of documentation and blank space, there's probably only between 1,800 and 2,200 lines of actual Python code in there. And a lot of it is repetitive stuff, so don't be turned away from reading the code. I take pride in my coding style and clarity, and would love to know any places in the code where the purpose and method isn't immediately evident. Heck, if you do read the code, take a pencil and paper (or an instance of emacs or vim) and write down all the questions you have, I'll answer them gladly (and probably stick the answer in the code, too.)

I've got a TODO in there, but there's a bit more that's not in that TODO that I plan to do. I need to write a "ChannelEnforcer" callback that will enforce certain capabilities (for instance, if someone has a '#linux.!op' capability, I want ChannelEnforcer to see that an deop them as soon as they're opped in #linux.) I also need to write a bit more in detail about the programmatical interface to the users/channels databases; right now, all there is to learn from is code. I'll probably want to write some asynchronous network classes or some threaded network classes at some point so people can access the network easily and consistently.

If someone writes a "notes" callback that let users leave notes for other users, I'd be ecstatic. I'd also like to see the bot replicate the abilities of apt on #debian on OPN at some point.

As a small design issue subject to change, I'm still determining how I want to store passwords in the user database. In my former bots, I've always stored the passwords in hashed form (for instance, in Ocamlbot, I stored them as a 30bit salt hashed via md5 with the password). The problem with storing passwords in hashed form, however, is that they must then be transmitted over the network in plaintext. Storing passwords in their unhashed form gives clients the option of using, say, HMAC to do their authentication, so no passwords have to travel over the network. I'm not quite sure exactly which security risk I'd rather deal with. For now, the passwords are in plaintext, since it's easier to go from plaintext to hashes than vice-versa.

Have at!

Jeremy

GnuVince
07-26-2002, 09:57 AM
First thing I found:


[08:57:10] <GnuVince> ~help
[08:57:12] <Berbot> GnuVince: Finds help for other commands, using their __doc__ string.
[08:57:12] <Berbot> GnuVince: Finds help for other commands, using their __doc__ string.
[08:57:13] <Berbot> GnuVince: Finds help for other commands, using their __doc__ string.
[08:57:15] <Berbot> GnuVince: Finds help for other commands, using their __doc__ string.
[08:57:17] <Berbot> GnuVince: Finds help for other commands, using their __doc__ string.
[08:57:19] <Berbot> GnuVince: Finds help for other commands, using their __doc__ string.


do I need to be told 6 times?

GnuVince
07-26-2002, 10:32 AM
~list


:GnuVince!~vince@modemcable101.50-130-66.mtl.mc.videotron.ca PRIVMSG #hackcanada :~list
Traceback (most recent call last):
File "callbacks.py", line 177, in doPrivmsg
method(irc, msg, args)
File "callbacks.py", line 212, in list
attrs = filter(self.isCommand, attrs)
File "callbacks.py", line 104, in isCommand
return method.im_func.func_code.co_varnames[:4] ==\
AttributeError: class Help has no attribute 'im_func'
Traceback (most recent call last):
File "callbacks.py", line 177, in doPrivmsg
method(irc, msg, args)
File "callbacks.py", line 212, in list
attrs = filter(self.isCommand, attrs)
File "callbacks.py", line 104, in isCommand
return method.im_func.func_code.co_varnames[:4] ==\
AttributeError: class Help has no attribute 'im_func'
Traceback (most recent call last):
File "callbacks.py", line 177, in doPrivmsg
method(irc, msg, args)
File "callbacks.py", line 212, in list
attrs = filter(self.isCommand, attrs)
File "callbacks.py", line 104, in isCommand
return method.im_func.func_code.co_varnames[:4] ==\
AttributeError: class Help has no attribute 'im_func'
Traceback (most recent call last):
File "callbacks.py", line 177, in doPrivmsg
method(irc, msg, args)
File "callbacks.py", line 212, in list
attrs = filter(self.isCommand, attrs)
File "callbacks.py", line 104, in isCommand
return method.im_func.func_code.co_varnames[:4] ==\
AttributeError: class Help has no attribute 'im_func'
Traceback (most recent call last):
File "callbacks.py", line 177, in doPrivmsg
method(irc, msg, args)
File "callbacks.py", line 212, in list
attrs = filter(self.isCommand, attrs)
File "callbacks.py", line 104, in isCommand
return method.im_func.func_code.co_varnames[:4] ==\
AttributeError: class Help has no attribute 'im_func'
Traceback (most recent call last):
File "callbacks.py", line 177, in doPrivmsg
method(irc, msg, args)
File "callbacks.py", line 212, in list
attrs = filter(self.isCommand, attrs)
File "callbacks.py", line 104, in isCommand
return method.im_func.func_code.co_varnames[:4] ==\
AttributeError: class Help has no attribute 'im_func'
PRIVMSG #hackcanada :GnuVince: An error has occurred and has been logged.

jemfinch
07-26-2002, 10:41 AM
Grr. I hate explorer.

If you press ESC in a text box, it empties the box. Being used to using vim, you don't realize how often I type a long and use post and then type "ESC :wq" only to realize that I've deleted my WHOLE FREAKING POST. Grrr.

Anyway, here's the fix to the '6 times' problem -- I'm still curious why you're getting help's docstring and not the special-cased "help <command>" there, but oh well.


def help(self, irc, msg, args):
"Finds help for other commands, using their __doc__ string."
command = self.getArgs(args)
if command == 'help':
if 'help' in self.__dict__:
irc.queueMsg(reply(msg, 'help <command>'))
elif command in self.__dict__:
method = self.__dict__[command]
if hasattr(method, '__doc__'):
irc.queueMsg(reply(msg, '%s %s' % (command, method.__doc__)))
else:
irc.queueMsg(reply(msg, 'That command has no help.'))


Since an object's __dict__ only includes attributes of the object's class and not attributes it inherits from other classes, that should fix it.

Jeremy

GnuVince
07-26-2002, 11:25 AM
Dunno if that qualifies as a bug:


:GnuVince!~vince@modemcable101.50-130-66.mtl.mc.videotron.ca PRIVMSG #hackcanada :~eval 'isChannel("#hackcanada")'
Traceback (most recent call last):
File "privmsgs.py", line 382, in _eval
irc.queueMsg(reply(msg, repr(eval(s))))
File "<string>", line 0, in ?
NameError: name 'isChannel' is not defined
PRIVMSG #hackcanada :GnuVince: An error has occurred and has been logged.

jemfinch
07-26-2002, 11:38 AM
Is that from a string you eval'ed? It's definitely not a bug if so. It's ircutils.isChannel that you want to call.

Jeremy

jemfinch
07-26-2002, 11:38 AM
I'm gonna head out to do some work on the bot and then come back and post my changes. Keep posting bugs! Just make sure to post both the traceback and the message that caused it, and I'll try to get them fixed ASAP.

Thanks for testing!

Jeremy

kmj
07-26-2002, 01:44 PM
jemfinch: regarding pressing "ESC"; Ctrl+z unoes that, iirc. That used to happen to me all the time.

jemfinch
07-26-2002, 03:25 PM
Well, here's the newest version, containing fixes for all the bugs mentioned so far (and some for bugs not yet mentioned :)) and an extra callback or two (check out "objects" in FunCommands and "import" in OwnerCommands).

I wrote a RawLogger callback, but it's not enabled by default. At some point, I'll write a Logger callback that logs in a format somewhat more rational than raw IRC protocol, but I haven't yet.

I'm still thinking about factoids, and what database I want to use for them.

Anyway, keep the bug reports and comments flowing!

Jeremy

jemfinch
07-26-2002, 04:05 PM
There's a small bug in that zip I posted that makes it so the bot won't respond over a channel to anyone. Here's the fix:

In callbacks.py, change this:


elif ircutils.isChannel(msg.args[0]):
# Check to see if the user has the 'channel.!command'
# capability set, banning the user from calling that
# command in the channel.
if irccontrol.checkPrivmsgCapability(msg, '%s.!%s' %\
(msg.args[0],command)):
return


to this:


# Check to see if the user has the 'channel.!command'
# capability set, banning the user from calling that
# command in the channel.
elif ircutils.isChannel(msg.args[0]) and\
irccontrol.checkPrivmsgCapability(msg, '%s.!%s' %\
(msg.args[0], command)):
return


Yeah. Does anyone else see the bug? :)

Jeremy

jemfinch
07-27-2002, 11:00 AM
My next release will include several significant changes:

The irccontrol module will be renamed "ircdb" -- I dont' know what I was thinking when I named it "irccontrol", but ircdb is shorter and closer to what it does.

callbacks.Privmsg is going to have a self.reply method that deriving classes can call so they can have a more succint way of saying irc.queueMsg(reply(msg, ...)). It'll also later come in useful when I add command piping.

I'd like to have privmsgs.FactoidCommands, callbacks.Logger, and callbacks.ChannelEnforcer written by the time I release next.

I'll probably get all this out later this weekend, maybe late Sunday night/Monday morning.

Jeremy

jemfinch
07-27-2002, 03:09 PM
Well, except for the added features, there are quite a few bugfixes in this (0.33). Here's the CHANGELOG (I started keeping one).


2002-Jul-27 Jeremy Fincher <tweedgeezer@hotmail.com>

* Version 0.33 (third release)
* Bot now exits cleanly on @quit.
* Renamed former 'irccontrol' module to the much more appropriate name
'ircdb', which is also much easier to type and read.
* Changed slightly the config format for conf/users and conf/channels.
* Added a new command in ChannelCommands, 'setdefaultcapability', to
set the default capability response for a channel.
* Rewrote ircdb.checkCapability because the old version was buggy.
Hopefully this one isn't.
* Added callbacks.Privmsg.reply and .error, to make things a bit in
client code. Major caveat, though: you can't blindly "except:" any
longer. You *must* stick a "except self.reserved: raise" in there


Have at!

Jeremy

(EDIT: test with hard newlines looks much better in [code] than in [quote] :))

jemfinch
07-28-2002, 02:14 AM
Ok, the bot is nearing the "Release at Arstechnica" stage. I'm finishing up some last callbacks (ChannelEnforcer, FactoidsCommands, and Logger) and it'll be ready for consumption by the general public, features-wise.

What I'd like to do, though, is sit down with someone for a few hours on an IRC channel and beat on the capabilities system. That's some stuff that's really important to have right before releasing the software officially ("once insecure, always insecure" is the philosophy I follow with server software, and I don't want to be even "once insecure" after my Arstechnica release).

This bot will be the bot that runs #linux on irc.arstechnica.com, so my reputation (and the 'security' of the coolest #linux in existence) is really on the line :)

What I'd also really like to get working is a SourceForge project for the bot, so I can at least get it posted somewhere other than here and so I can have bugtracking/feature requests and stuff all in once place. Eventually, I'd like to have CVS working, but given my internet situation, it'll probably be more a project like the Linux kernel than like FreeBSD for awhile. I might need someone to show me how to do those things at SF.net.

Keep an eye here toward the end of the weekend for 0.4 RC1 :)

Jeremy

GnuVince
07-28-2002, 02:51 PM
Check this out:


[13:50:36] <Ismahel> ~leet Patate
[13:50:52] <GnuVince> ~leet patate
[13:50:52] <Barbotte> GnuVince: p@8
[13:51:59] <GnuVince> ~port http
[13:52:00] <Barbotte> GnuVince: 80
[13:52:07] <GnuVince> essaye donc ca
[13:52:13] <Ismahel> ~port ftp


I am a privileged user, Ismahel is not. Here's the Traceback for ~port:


:Ismahel!silver@c42.rocler.qc.ca PRIVMSG #hackcanada :~port ftp
Traceback (most recent call last):
File "irclib.py", line 205, in feedMsg
callback(self, copy.copy(msg))
File "irclib.py", line 59, in __call__
getattr(self, commandName)(irc, msg)
File "callbacks.py", line 213, in doPrivmsg
elif ircutils.isChannel(msg.args[0]) and\
File "ircdb.py", line 336, in checkPrivmsgCapability
return checkCapability(msg.prefix, capability)
File "ircdb.py", line 291, in checkCapability
c = channels.getChannel(channel)
File "ircdb.py", line 213, in getChannel
c = IrcChannel()
File "ircdb.py", line 87, in __init__
for capability in capabilities:
TypeError: iteration over non-sequence

jemfinch
07-29-2002, 03:47 AM
ah, good one...thanks for finding that bug :)

I need to check "if capabilities is not None:" before that loop.

Jeremy

jemfinch
07-29-2002, 06:46 PM
0.34 is now released!

I'm starting a sourceforge project for this later tonight, btw.

Here's the relevant entry of the CHANGELOG (which I'm keep pretty well, just adding things as I modify/add things to the code.)


2002-Jul-29 Jeremy Fincher <tweedgeezer@hotmail.com>

* Changed FunCommands.cputime to FunCommands.cpustats.
* Moved bot.upkeep to world.upkeep (superReload can't reload __main__,
so having it in world allows it to be reloaded.)
* Added timestamp to debug message printing in irclib.
* Changed IrcMsg .{nick,user,host} to simple attributes.
* Added OwnerCommands.{set, unset} to set and unset temporary runtime
variables. As an example of their use, look at bot.upkeep.
* Added conf.timestampFormat, so timestamps can be universally of the
same form.
* Added FunCommands.netstats.
* Added callbacks.RootWarner, to warn people when they join a channel
as root.
* Added callbacks.ChannelEnforcer, to enforce certain capabilities on
channel.
* added ircdb.checkCapabilties and ircdb.checkPrivmsgCapabilities,
which check for any capability in a list of capabilities.
* added privmsgs.ChannelCommands.{voice,halfop}
* added ircdb.IrcChannel.checkBan
* Fixed bug in IrcUser.__init__


Have fun, and keep up the good testing!

Thanks,
Jeremy

inkedmn
07-29-2002, 07:57 PM
problem :)

Vince helped me through some of the initial config of the bot, i got it to connect, but i couldn't get it to respond to anything...

(Vince can vouch for me on this) :)

GnuVince
07-29-2002, 08:29 PM
Maybe we should rewrite supybot in J (www.jsoftware.com) :)

jemfinch
07-29-2002, 10:31 PM
hmm.

No exceptions, no tracebacks or anything?

Jeremy

inkedmn
07-29-2002, 11:30 PM
not that i saw, but i was kinda trying it while doing other work, so i'm not entirely sure...

jemfinch
07-29-2002, 11:47 PM
Hmm.

Well, when you know for sure, let me know :) And if there are tracebacks, go ahead and paste them into here so I can fix the bugs ASAP :)

Jeremy

jemfinch
07-30-2002, 05:11 PM
Ok, 0.35. This might be the last release here, since my SourceForge project got approved, and http://www.sf.net/projects/supybot/ has gone live :)

ChangeLog:
2002-Jul-30 Jeremy Fincher <tweedgeezer@hotmail.com>
* Started moving some stuff out to the plugins/ subdirectory. Haven't
yet developed an adequate interface for plugin modules, though -- I'm
not entirely sure that it's needed.
* Began writing tests. At least I know the format of the test *data*,
I just haven't quite developed the framework in which to execute it :)
* Wrote ctcp.Ctcp, an example of PrivmsgRegexp use and a module to
respond to CTCP commands.
* Changed callbacks.PrivmsgRegexp to be much easier to use.
* Fixed bug in ircdb -- channels needed to have a !op capability by
default, otherwise everyone can do anything requiring op capabilities.
Found several of these type of bugs; had to revert to the old config
style (using a dictionary for capabilities instead of a list) in order
to resolve them.
* Generalized exception propogation mechanism so any exception in
conf.deadlyExceptions would be propogated, not just asyncore.ExitNow.
* Added command-line parsing and the ability to specify a configuration
file as an option.
* Added ircmsgs.IrcMsg.__len__, so calculating the (probable) length of
an IRC message doesn't require making a string of it first.
* Added code to make the Irc object ping the server every
conf.pingInterval seconds. I've always found that having the bot ping
the server leads to a more robust bot (easier detection of bad network
or a dropped connection).
* Made AsyncoreDriver capable of reconnecting.
* Moved AsynchatDriver to asyncoreDrivers.py, renamed some things, and
redesigned it so there's always an asyncore polling driver and the
individual asyncore drivers don't poll. The code is much simpler and
clearer this way.
* Fixed bug in ircutils.separateModes (tried to pop arguments on modes
that didn't take an argument.)
* Renamed the "unixstats" command in FunCommands to "progstats".
* Fixed bug in callbacks.Privmsg.help.
* Removed ircdb.checkPrivmsgCapability.


Jeremy

GnuVince
07-30-2002, 08:33 PM
I wrote my freshmeat version fetcher for your bot. I put it in provmsgs.py under FunCommands. Here's the code, tell me if it seems all right:


def version(self, irc, msg, args):
"<name of the software>"
software = self.getArgs(args).lower()
url = 'http://freshmeat.net/projects-xml/' + software
r_vers = re.compile('^\s*<latest_version>(.*)</latest_version>$',
re.IGNORECASE)
xml_doc = urllib2.urlopen(url).readlines()
version = None

for line in xml_doc:
if r_vers.match(line):
version = r_vers.match(line).groups()[0]
break

if version == None:
self.reply('%s does not exist of [fm].' % software)
else:
self.reply('latest version of %s according to [fm]: %s.' %
(software, version))

GnuVince
07-30-2002, 11:48 PM
n.ca PRIVMSG #hackcanada :~help reload
Traceback (most recent call last):
File "./callbacks.py", line 368, in doPrivmsg
method(irc, msg, args)
File "./callbacks.py", line 392, in help
command = self.mungeCommand(self.getArgsLastOptional(args))
File "./callbacks.py", line 323, in mungeCommand
return string.translate(command, world.ascii, '-_').lower()
File "/usr/lib/python2.2/string.py", line 310, in translate
return s.translate(table, deletions)
AttributeError: 'tuple' object has no attribute 'translate'
Traceback (most recent call last):
File "./callbacks.py", line 368, in doPrivmsg
method(irc, msg, args)
File "./callbacks.py", line 392, in help
command = self.mungeCommand(self.getArgsLastOptional(args))
File "./callbacks.py", line 323, in mungeCommand
return string.translate(command, world.ascii, '-_').lower()
File "/usr/lib/python2.2/string.py", line 310, in translate
return s.translate(table, deletions)
AttributeError: 'tuple' object has no attribute 'translate'
Traceback (most recent call last):
File "./callbacks.py", line 368, in doPrivmsg
method(irc, msg, args)
File "./callbacks.py", line 392, in help
command = self.mungeCommand(self.getArgsLastOptional(args))
File "./callbacks.py", line 323, in mungeCommand
return string.translate(command, world.ascii, '-_').lower()
File "/usr/lib/python2.2/string.py", line 310, in translate
return s.translate(table, deletions)
AttributeError: 'tuple' object has no attribute 'translate'
Traceback (most recent call last):
File "./callbacks.py", line 368, in doPrivmsg
method(irc, msg, args)
File "./callbacks.py", line 392, in help
command = self.mungeCommand(self.getArgsLastOptional(args))
File "./callbacks.py", line 323, in mungeCommand
return string.translate(command, world.ascii, '-_').lower()
File "/usr/lib/python2.2/string.py", line 310, in translate
return s.translate(table, deletions)
AttributeError: 'tuple' object has no attribute 'translate'
Traceback (most recent call last):
File "./callbacks.py", line 368, in doPrivmsg
method(irc, msg, args)
File "./callbacks.py", line 392, in help
command = self.mungeCommand(self.getArgsLastOptional(args))
File "./callbacks.py", line 323, in mungeCommand
return string.translate(command, world.ascii, '-_').lower()
File "/usr/lib/python2.2/string.py", line 310, in translate
return s.translate(table, deletions)
AttributeError: 'tuple' object has no attribute 'translate'
Traceback (most recent call last):
File "./callbacks.py", line 368, in doPrivmsg
method(irc, msg, args)
File "./callbacks.py", line 392, in help
command = self.mungeCommand(self.getArgsLastOptional(args))
File "./callbacks.py", line 323, in mungeCommand
return string.translate(command, world.ascii, '-_').lower()
File "/usr/lib/python2.2/string.py", line 310, in translate
return s.translate(table, deletions)
AttributeError: 'tuple' object has no attribute 'translate'
Traceback (most recent call last):
File "./callbacks.py", line 368, in doPrivmsg
method(irc, msg, args)
File "./callbacks.py", line 392, in help
command = self.mungeCommand(self.getArgsLastOptional(args))
File "./callbacks.py", line 323, in mungeCommand
return string.translate(command, world.ascii, '-_').lower()
File "/usr/lib/python2.2/string.py", line 310, in translate
return s.translate(table, deletions)
AttributeError: 'tuple' object has no attribute 'translate'

jemfinch
07-31-2002, 04:00 PM
Ok, that bug's fixed.

I'm switching over all development to SourceForge -- http://www.sf.net/projects/supybot . They have all the bug tracking stuff I need, and I expect people to use that now if they need to report bugs :) If I can find a way to lock this thread, I will :)

Jeremy

inkedmn
07-31-2002, 04:05 PM
i'm closing this thread, PM me if that's a problem for anybody...

kmj
07-31-2002, 04:49 PM
?