One morning on the Underground, I began to reason that what I needed was a sort of "IRC Bot Apache" that would handle incoming IRC events of various sorts ( PRIVMSG , ACTION , etc.), and then pass them along with some connection information to my django code. I'd dispatch these messages through the regex-based patterns() system, then call view code that uses ORM objects to perform queries.

The obvious choice for implementing something like this is Twisted Python, which shows its age but remains the de facto Python library for coding state machines. I'm only occasionally familiar with the system (and the documentation is filled with distracting Java-esque Software Engineering babble for some bizarre reason), but I was able to localize the actual Twisted-using code to one function, which at the very least makes it simple to hand off to experts to tell me if I'm doing anything stupid.

Yardbird

Digging through twisted documentation I found their example LogBot and based it loosely on that pattern, subclassing irc.IRCClient and replacing the privmsg method along the following lines:

from twisted.words.protocols import irc from twisted.internet import defer, threads from django.core import urlresolvers from django.conf import settings class DjangoBot(irc.IRCClient): @defer.inlineCallbacks def privmsg(self, user, channel, msg): resolver = urlresolvers.get_resolver('.'.join( (settings.ROOT_MSGCONF, 'privmsg'))) request = dict(user=user, channel=channel, msg=msg, settings=settings) callback, args, kwargs = resolver.resolve('/' + request['msg']) response = yield threads.deferToThread(callback, request, *args, **kwargs) defer.returnValue(self.notice(response.recipient, response.data.encode('UTF-8')))

Let me go through that line by line:

@defer.inlineCallbacks def privmsg(self, user, channel, msg):

The inlineCallbacks decorator essentially catches any yield of a Twisted deferred object and schedules the next delve into the generator using standard Twisted deferred-execution mechanisms. So now yield really behaves like a scheduler yield, and you can let some more critical IRC-parsing code run between your own calls.

Next we build a Django urlresolver object so we can dispatch regexes to handler functions, using some Django settings info to determine path info:

from django.core import urlresolvers from django.conf import settings resolver = urlresolvers.get_resolver('.'.join( (settings.ROOT_MSGCONF, 'privmsg')))

Then we build a request dictionary. In normal Django this would be an HttpRequest object, containing all sorts of information about the web server and the remote client and the HTTP request itself. Since this is a quick-and-dirty example, I've reduced this to a dict for simplicity. I also passed in the settings namespace just to be lazy (so I can keep things like nickname in there):

request = dict(user=user, channel=channel, msg=msg, settings=settings)

Now we actually use our resolver to test the incoming message against all our patterns in privmsg.py and return to us the appropriate function, along with all of the anonymous and named matches that were generated by the winning regular expression. Note that we have to prepend a / to our message to appease the URL-centric resolver:

callback, args, kwargs = resolver.resolve('/' + request['msg'])

Finally we get to the deferred execution magic! We have a function, a request object, and some arguments made from textual analysis of the message. We use the threads.deferToThread method to generate a deferred object that runs in a completely separate thread, and yield it up to our inlineCallbacks decorator to be scheduled:

response = yield threads.deferToThread(callback, request, *args, **kwargs)

Our view function then runs in the background, taking as long as it likes while our bot concerns itself with answering PING replies and dispatching further events to the resolver.

We're confident that the Django code is reasonably thread-safe, as it has to handle concurrency under a variety of Web server models (such as apache's Worker MPM or a traditional Prefork model). Once the function returns a value, the thread closes and execution comes back to this method again, chucking the returned value into our response object.

We're almost done, but we still need to actually do something with this information! In ordinary HTTP Django this would be an HttpResponse object, containing all sorts of information on what template to render and what dictionary to pass in as an extra context namespace. This is a bit overkill for this example, so I've simplified it to another dict:

defer.returnValue(self.notice(response['recipient'], response['data'].encode('UTF-8')))