Friday, April 24, 2009

Incoming call screen pops with sipX, rabbitMQ, and Adobe Air

I just finished the first beta of NetCenterPlus, an Adobe Air html based tray application that presents screen pops for incoming calls on sipX systems. NetCenterPlus is part of NetCenter, a CRM/Business Productivity solution from NetServe365.

Here's a screenshot of the notification window on an incoming call.

On the backend, I implemented a solution very similar to the one I did for Integrating sipx with ejabberd. There are two database triggers installed into the SIPXCDR database, the second of which is a PostgreSQL plperlu trigger which uses Net::Stomp to send a message to our rabbitMQ server indicating the callerId of an incoming call to the user registered for the destination extension. Not many of lines of code:
CREATE FUNCTION cse_ncplus_change() RETURNS trigger AS $end$
use Net::Stomp;

my ($domain, $uid, $pwd) = @{$_TD->{args}};
my $msg = TD->{"new"}{"from_id"};

my $stomp = Net::Stomp->new({hostname=>'mq.nvizn.com', port=>'61613'});
$stomp->connect({login=>$uid, passcode=>$pwd});

my $uid = $_TD->{"new"}{"username"};
$stomp->send({destination=>"/$domain/ncplus/$uid",
body=>($msg)});

$stomp->disconnect;
return undef;
$end$
LANGUAGE plperlu;
The other plpgsql trigger looks up the destination extension and munges up a nice looking incoming call number. That exercise is left to the reader.

Now we've got a message on a per-user queue for every incoming call on our sipX system. So what next?

I wanted an easy to deploy, cross platform, tray application that would listen for incoming messages on present the screen pop. I looked at Mozilla Prism, Silverlight, and Adobe Air. Air was not my first choice to be honest, but the Prism project seems to have stagnated afaict, and Silverlight 2.0 on Linux doesn't look like it will be out anytime soon, so I went with Air. After spending some time with the product, I've definitely grown in my appreciation of its ease of use and design. It's really nice to be able to leverage existing web development skills to build these type of applications.

So what does the Air application do? First of it, I used air.Socket and javascript to implement a STOMP client.

First the connection code:
air.trace("setting up MessageQueue...");
this.socket = new air.Socket();
var self = this;

this.socket.addEventListener(air.Event.CONNECT, function(event) {
self.sendCommand("CONNECT\nlogin:guest\npasscode:" + password + "\n\n");
self.state = self.STATE.CONNECT;
});
The main listener loop looks something like this:
this.socket.addEventListener(air.ProgressEvent.SOCKET_DATA,
function(event) {

switch (self.state) {
case self.STATE.CONNECT:
self.subscribe();
break;
case self.STATE.READY:
var data = event.target.readUTFBytes(event.target.bytesAvailable);
var lines = data.split("\n");
if (lines[0] == "MESSAGE" && lines.length>5) {
msg_callback(lines[6]);
}
break;
}
});
So a NetCenterPlus user installs the application via a web page (yet to be prettied up!).



Then, the user enters their NetCenter username and password (again, this dialog needs some UI love. Did I mention I'm not a graphic artist?):



You can read and write to a local encrypted store in Air via functions like this:
readFromLocalStore = function(key, defstr) {                                
var item = air.EncryptedLocalStore.getItem(key);
if (item == null) return defstr;
return item.readUTFBytes(item.length);
}

saveToLocalStore = function(key, value) {
var bytes = new air.ByteArray();
bytes.writeUTFBytes(value);
air.EncryptedLocalStore.setItem(key, bytes);
}
When NetCenterPlus receives an incoming screen pop, we use the DOM to set the incoming call caller id, then we do an authenticated HTTP GET on the NetCenter REST based API to lookup the contact's name. The code looks something like this:
    var cpnum = this.document.getElementById("callpop_number");
cpnum.innerHTML = fnum;

var url = "http://" + server + "/api/contact/byPhone/" + callnum;
var request = new air.URLRequest(url);

var loader = new air.URLLoader();
var self = this; var loader_sucess = true;

loader.addEventListener(air.IOErrorEvent.IO_ERROR, function(error) {
air.trace("Failed to load: " + url);
});

loader.addEventListener(air.Event.COMPLETE, function(event) {
var data = new air.URLVariables(event.target.data);
var cpname = self.document.getElementById("callpop_name");
cpname.innerHTML = data.name;
});
You setup the login credentials in Air, with a single line of code:
air.URLRequestDefaults.setLoginCredentialsForHost(this.server, username, password);
The NetCenter CRM is a Grails application that exposes a REST based api via basic authentication tied into Active Directory. To set this up, I added the following lines to grails-app/conf/Config.groovy:
jsecurity.filter.config = """                                                 

[filters]
authcBasic = org.jsecurity.web.filter.authc.BasicHttpAuthenticationFilter
authcBasic.applicationName = NetCenter API

[urls]
/api/** = authcBasic
"""
The contact controller "byPhone" method that the Air application uses is a very simple:
    def byPhone = {
def contacts = Contact.withCriteria {
eq('active', true)
eq('licensee.id', session.lid)
or {
eq('workPhone', params.id)
eq('homePhone', params.id)
eq('cellPhone', params.id)
}
}

return [ 'contacts': contacts ]
}
Well that about describes how all these parts come together. We plan on adding a lot more functionality to the NetCenterPlus Air application and thus far I'm pretty pleased with the Air platform.

Tuesday, April 07, 2009

NetCenter Click to Call

I just completed adding "Click to Call" functionality to NetCenter. Since this is a bit difficult to demonstrate with screenshots, I made a YouTube video instead.



I implemented "Click to Call" using Aloha and RabbitMQ, testing the solution on sipX.

I have a grails PlaceCallService that sets up a connection to our RabbitMQ instance like this:
  static transactional = false;
ConnectionParameters connectionParameters;
ConnectionFactory connectionFactory;
ConfigObject config = ConfigurationHolder.config;

MessageQueueService() {
connectionParameters = new ConnectionParameters();
connectionParameters.setUsername(config.mq.username);
connectionParameters.setPassword(config.mq.password);
connectionFactory = new ConnectionFactory(connectionParameters);
}


The actual message publish function looks something like this:
  def publish(message) {
try {
Connection conn = connectionFactory.newConnection(config.mq.host,
AMQP.PROTOCOL.PORT);
Channel ch = conn.createChannel();

ch.queueDeclare(config.placeCall.routingKey);
ch.basicPublish("", config.placeCall.routingKey, null,
message.getBytes());

ch.close();
conn.close();
}
catch (Exception e) {
log.error("Main thread caught exception: " + e);
return false
}

return true
}


Then in a "Third Party call initiator daemon", I unpack the message and use the Aloha stack to do a
        try {
OutboundCallLegBean outboundCallLegBean = (OutboundCallLegBean)
applicationContext.getBean("outboundCallLegBean");
CallBean callBean = (CallBean)
applicationContext.getBean("callBean");
callBean.addCallListener(this);

// create two call legs
String callLegId1 =
outboundCallLegBean.createCallLeg(URI.create(callee),
URI.create(caller));
String callLegId2 =
outboundCallLegBean.createCallLeg(URI.create(caller),
URI.create(callee));

// join the call legs
System.out.println(String.format("connecting %s and %s in call...$
System.out.println(callBean.joinCallLegs(callLegId1, callLegId2))$
}


This chunk of code is based on the helpful Third Party Call sample from Aloha's subversion repository.

Anyway, it's working well, consumes minimal resources on the web server (just post message to the "/placeCall/request" queue), and only took a few days to setup and deploy into production. Many thanks to the Aloha team, RabbitMQ folks, and sipX gurus.