The Lightcrest Blog

Let's talk about fluid computing, hyperconverged infrastructure, and hybrid cloud technology.

Using Twisted for Rapid Application Development

Last update on Sept. 9, 2015.

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!