Recently I was looking for ways to play with a simple Python-based IRC bot. Like for other network-related tasks, I turned to Twisted for the answer and was pleased to find a very nice and simple bot example in the documentation.

My adaptation of this bot is also just a sample on top of which a real bot can be built. It's a bit more versatile since it allows you to configure a lot of the connection details conveniently via the command line. It also has some debugging functionality on which I will have more to say later.

This bot connects successfully to IRC servers on the internet, but in order to make it easier to test, I was also looking for a way to run a server locally. Luckily, Twisted provides a functional IRC server, which is also integrated into the twistd daemon program, as documented here. However, I found this path problematic - my bot wouldn't connect to the server and since the server is very much self contained, I didn't have any knobs to turn that would help me figure out what's wrong.

So I went ahead and used the Twisted library to write a server explicitly. It turned out to be very simple: here's the whole thing:

import sys

from twisted.cred import checkers, portal
from twisted.internet import reactor
from twisted.internet.endpoints import TCP4ServerEndpoint
from twisted.python import log
from twisted.words import service


ROOM = 'room'
USERS = dict(
    user1='pass1',
    user2='pass2',
    user3='pass3',
    user4='pass4')


if __name__ == '__main__':
    log.startLogging(sys.stdout)

    # Initialize the Cred authentication system used by the IRC server.
    realm = service.InMemoryWordsRealm('testrealm')
    realm.addGroup(service.Group(ROOM))
    user_db = checkers.InMemoryUsernamePasswordDatabaseDontUse(**USERS)
    portal = portal.Portal(realm, [user_db])

    # IRC server factory.
    ircfactory = service.IRCFactory(realm, portal)

    # Connect a server to the TCP port 6667 endpoint and start listening.
    endpoint = TCP4ServerEndpoint(reactor, 6667)
    endpoint.listen(ircfactory)
    reactor.run()

The biggest hurdle here was getting the server's authentication to work. Although many IRC servers on the internet are very open - letting you connect with any nickname without a password, and open a new channel just by being the first one to join it - Twisted's IRCFactory provides a server that's much more strict about authentication. Therefore, most of the code deals with setting up the proper Twisted Cred authentication objects for the server to work, and this server only supports one room (group) and a predefined set of registered nicknames with passwords.

It's entirely possible that this can be avoided somehow, by either finding some sort of Cred implementation that doesn't actually do any authentication, or just mocking it out. But the server is good enough to play with, as it is.

One thing that helped me a lot while figuring out why the bot wouldn't connect to the server is implementing the lineReceived callback on my IRC client protocol. Since IRC is line based, Twisted's IRCClient subclasses protocols.basic.LineReceiver, and lineReceived thus gets invoked on every message received from the server. Overriding the callback allowed me to log it and then forward it to its correct destination (superclass's lineReceived) and made debugging very easy, since IRC return messages from a server are very detailed.

For even more visibility, with the server code above in hand, it wouldn't have been to hard to override the same method on the server's protocol class, and thus have full visibility into the communication. Well, tapping the port with Wireshark would work too ;-)