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$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.
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;
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...");The main listener loop looks something like this:
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;
});
this.socket.addEventListener(air.ProgressEvent.SOCKET_DATA,So a NetCenterPlus user installs the application via a web page (yet to be prettied up!).
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;
}
});
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) {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 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);
}
var cpnum = this.document.getElementById("callpop_number");You setup the login credentials in Air, with a single line of code:
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;
});
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 = """The contact controller "byPhone" method that the Air application uses is a very simple:
[filters]
authcBasic = org.jsecurity.web.filter.authc.BasicHttpAuthenticationFilter
authcBasic.applicationName = NetCenter API
[urls]
/api/** = authcBasic
"""
def byPhone = {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.
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 ]
}