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.

No comments: