Introduction to BridgeBot
Hey guys, I thought I’d drop my first post with something potentially useful for folks out there who love to write python and happen to need protocol bridging for their chat systems. As you may or may not know, Lightcrest has a chat system in place that allows users to interact with our sales and engineering staff. Rather than purchase a third party application, we decided to build it ourselves so we could extend it in the future (also – why buy something when you can build it in four hours?).
Our chat system runs off a custom Flex app that talks to a custom ratbox IRC daemon. When the Flex app loads, it talks to our custom IRC daemon over a TCP socket and initiates, registers, and funnels messages as any other IRC client would.
Now obviously we don’t want our staff to sit around and watch their IRC terminals all day. We wanted the ability to have the chat system alert our staff efficiently from any device with an AIM client – whether it be an iPhone, Droid, or home workstation. Would you want to have your sales staff learn IRC? We wouldn’t.
Twisted Framework
So off we went to the Python Twisted Framework. If you haven’t used Twisted before, it takes a little getting used to – but once you’ve got it working you’ll never want to write a select() or poll() loop ever again. Twisted essentially abstracts all the code you have to rewrite for every single network app you build, and allows you to chain your app functionality in the form of ‘chained deferreds’, which are essentially just callbacks that are assigned to an asynchronous event.
Being a pragmatist, I’m not going to go over how Twisted actually works or what it actually is – they do it far better on the official site at twistedmatrix.com.
In this case, we wanted to bridge two protocols. How do you get two Twisted factories to talk to each other? While this may seem obvious, once you play with the framework a bit you realize it’s up to you to figure out how to bridge communications between multiple clients within the same Twisted reactor. I hope this snippet of code makes your life easier down the road (and perhaps you’ll come up with even better ways of doing it).
We decided to simply stack connection objects into a queue, and share the queues between the factories/clients.
For the Propeller Heads
So here’s the code. Thanks to ActivePython and thus Richard Stevens for the unix process handling code. If you don’t know who Richard Steven is, he’s the guy who wrote TCP/IP Illustrated Volumes 1 – 3 (a must have for any software engineer).
Please note this isn’t a tutorial. I’m going to run through the code from the top down, so if the flow of the blog is funny, my apologies in advance.
# BridgeBot - AIM <-> IRC bridge written # for Lightcrest chat interface. import os import re import sys from twisted.words.protocols import irc,oscar from twisted.internet import protocol,reactor
Nothing too interesting here other than the importing of the namespaces that contain the Oscar and IRC classes we need to leverage the respective protocols.
First, lets define the configuration parser. This function will parse a very basic config that allows you to list AIM screennames that should be alerted on any incoming message from the website.
class SetConfig: """ Set aim handles for future broadcasting""" def __init__(self,config_file): self.aim_handles = [] for line in open(config_file).read().split('\n'): if len(line) and line[0] != "#": self.aim_handles.append(line.strip())
This will let let us parse AIM SN’s out of a flat file like this:
[zfierstadt@foobar bridgebot]$ cat aimhandles.cf # This configuration file lists AIM sn's # to relay BridgeBot messages from customers # to LC staff. zachflap909 shanpors62 mhu99 nclacemel #skycrane +18888575309
Now lets define our BridgeBot class. This will be the code responsible
for the IRC side of the equation.
class BridgeBot(irc.IRCClient): def _get_nickname(self): return self.factory.nickname def _get_myqueue(self): return self.factory.myqueue def _get_aimqueue(self): return self.factory.aimqueue nickname = property(_get_nickname) myqueue = property(_get_myqueue) aimqueue = property(_get_aimqueue) def signedOn(self): self.join(self.factory.channel) print "Signed on as %s." % (self.nickname,) # add this connecton to my global queue self.myqueue.append(self) def joined(self, channel): print "Joined %s." % (channel,) def privmsg_quick(self,recip,message): self.sendLine("PRIVMSG %s :%s" % (recip,message)) def irc_PRIVMSG(self, prefix, params): """ Called when we get a message. """ user = prefix.split("!")[0] channel = params[0] message = user + ": " + params[-1] self.aimqueue[0].broadcastMessage(message) if debug: print "Received PRIVMSG: %s %s %s" %(user,channel,message)
Here is the actual Factory for BridgeBot.
class BridgeBotFactory(protocol.ClientFactory): protocol = BridgeBot def __init__(self, channel, nickname='bridgebot', myqueue=[] ,aimqueue=[] self.channel = channel self.nickname = nickname self.myqueue = myqueue self.aimqueue = aimqueue def clientConnectionLost(self, connector, reason): print "Lost connection (%s), reconnecting." % (reason,) connector.connect() def clientConnectionFailed(self, connector, reason): print "Could not connect: %s" % (reason,)
Now for the Oscar implementation. This will be the AIM side of the equation, allowing us to relay messages from the IRC connection to the AIM connection.
class BosConn(oscar.BOSConnection): capabilities = [oscar.CAP_CHAT] def initDone(self): self.requestSelfInfo().addCallback(self.gotSelfInfo) self.requestSSI().addCallback(self.gotBuddyList) # Add this connection to my global queue self.myqueue.append(self) def gotSelfInfo(self, user): if debug: print user.__dict__ self.name = user.name def gotBuddyList(self, l): if debug: print l self.activateSSI() self.setIdleTime(0) self.clientReady() def receiveMessage(self, user, multiparts, flags): if debug: print user.name, multiparts, flags if debug: print "multiparts!! ", multiparts # auto messages should not be responded to. identify them by # the string auto, found in flags[0] (sometimes). try: auto = flags[0] if auto == "auto": return except IndexError: pass self.lastUser = user.name message=self.modifyReturnMessage(multiparts) try: user = message.split(":")[0] message = message[len(user)+1:] self.ircqueue[0].privmsg_quick(user,message) except: pass def broadcastMessage(self,message): for username in self.bcast: if debug: print "Broadcasting to %s" %(username) self.sendMessage(username,message, wantAck = 1, \ autoResponse = (self.awayMessage!=None))\ addCallback(self.respondToMessage) def respondToMessage(self, (username, message)): if debug: print "in respondToMessage" pass def receiveChatInvite(self, user, message, exchange, fullName \ ,instance, shortName, inviteTime): pass def extractText(self, multiparts): message = multiparts[0][0] match = re.compile(">([^><]+?)<").search(message) if match: return match.group(1) else: return message def modifyReturnMessage(self, multiparts): if debug: print "in modifyReturnMessage" message_text = self.extractText(multiparts) multiparts[0] = (message_text,) return multiparts[0][0]
Now we subclass OscarAuthenticator and add our own interesting bits
to BOSClass so they can be accessible when the reactor starts. Here we
define the separate queues for later manipulation.
class OA(oscar.OscarAuthenticator): BOSClass = BosConn def __init__(self,username,password,deferred=None,icq=0 \ ,myqueue=[],ircqueue=[],bcast=[]): self.username=username self.password=password self.deferred=deferred self.icq=icq # Make our global queues accessible to BOS self.BOSClass.myqueue = myqueue self.BOSClass.ircqueue = ircqueue self.BOSClass.bcast = bcast
Great. So the framework is built – now we need to wrap everything up
in a daemon that can be launched and eventually throw into an init script.
Here we define the process fork, file descriptor clean up, and option parsing.
if __name__ == "__main__": # Parse options from optparse import OptionParser usage = "usage: %prog [options] arg" parser = OptionParser(usage) parser.add_option("-f", "--foreground" \ ,dest="foreground",action="store_true" \ ,help="run bridgebot in foreground") parser.add_option("-d", "--daemon" \ ,dest="daemon",action="store_true" \ ,help="fork into daemonized process") parser.add_option("-v", "--verbose", dest="verbose",action="store_true" \ ,help="print debug output to standard output") # Initialize option states debug = False background = False foreground = False (options, args) = parser.parse_args() if options.foreground and options.daemon: parser.error("options -f and -d are mutually exclusive") elif options.foreground: foreground = True elif options.daemon: background = True elif options.verbose: debug = True else: parser.print_help() sys.exit(1) # Default daemon parameters. # File mode creation mask of the daemon. UMASK = 0 # Default working directory for the daemon. WORKDIR = os.environ.get("BRIDGEBOT_DIR") # Fork child process into the background # if we're in daemon mode. # Otherwise, set pid to 0 and launch in the # foreground. if(background): pid = os.fork() else: pid = 0 if (not pid): if(background): # Change to working directory os.chdir(WORKDIR) # Give child complete control over permissions. # the parent, so we give the child complete # control over permissions. os.umask(UMASK) # Child writes PID to disk pid_fd = open(WORKDIR+"/run/bridgebot.pid","w") pid_fd.write(str(os.getpid())) pid_fd.close() # irc settings chan = "#blah" # aim settings screenname = "bridgebot" password = "RjadsDF!@#m!" hostport = ('login.oscar.aol.com',5190) icqMode = 0 # initialize object queues so we can share protocol methods # across disparate factory and protocol instances ircqueue = [] aimqueue = [] # set list of AIM handles for broadcasting config = SetConfig('aimhandles.cf') # There she blows. protocol.ClientCreator(reactor, OA, screenname, password,\ icq=icqMode,myqueue=aimqueue,ircqueue=ircqueue,\ bcast=config.aim_handles).connectTCP(*hostport) reactor.connectTCP('localhost',9666,BridgeBotFactory(chan,\ "lightcres",\ myqueue=ircqueue,aimqueue=aimqueue)) reactor.run() else: # Parent exits sys.exit(0)
And there you have it. Now we can launch our shiny bridgebot.
[zfierstadt@foobar bridgebot]$ python bridgebot.py usage: bridgebot.py [options] arg options: -h, --help show this help message and exit -f, --foreground run bridgebot in foreground -d, --daemon fork into daemonized process -v, --verbose print debug output to standard output
But before we get too excited, lets wrap this up in a nice init script – we don’t
want to create more work for our systems administrator.
[zfierstadt@foobar bridgebot]$ cat init/bridgebot #!/bin/bash # # bridgebot Script to control process bridgebot # # Author: Zach Fierstadt # # chkconfig: - 90 10 # description: Starts and stops process bridgebot # Source function library. . /etc/init.d/functions # The bridgebot working directory BRIDGEBOT_DIR=/home/zfierstadt/bridgebot # The location of the bridgebot pid file start() { action $"Starting process bridgebot: " /usr/bin/python \ $BRIDGEBOT_DIR/bridgebot.py -d } stop() { PID=`cat $BRIDGEBOT_DIR/run/bridgebot.pid` action $"Shutting down process bridgebot: " kill -9 $PID } # See how we were called. case "$1" in start) start ;; stop) stop ;; restart|reload) stop start ;; *) echo $"Usage: $0 {start|stop|restart|reload}" exit 1 esac exit 0
Now we can start and stop bridgebot as a service.
[zfierstadt@foobar init]$ ./bridgebot start Starting process bridgebot: [ OK ] [zfierstadt@foobar init]$
Now let’s see this in action. I’m going to load http://www.lightcrest.com.
Let’s send a request as a customer might over the Flex widget:
Voila! We got a message broadcast to all users in the aimhandles.cf
including me.
We can address multiple users as long as we address them with their original
nickname. So we can maintain multiple conversations in a single window as
such:
And on the client side our customer sees the message funneled in from AIM:
And there you have it. Customer service tools that allow your team to effectively sell
from any device that AIM can run on.
Until next time!