Using Twilio to Add VOIP/Conference Calling to your Ext JS 4 Apps

I was recently tasked with automating a grant submission and approval workflow process for a non-profit corporation, as illustrated in the figure below. When there is disagreement among the reviewers as to whether a project should be funded, a conference call must take place between the reviewers in order to facilitate a consensus.

naccho1

The scope of the project initially dictated that we simply add-in the capability to schedule a call and send out meeting invitations. However, when it came time to actually build out this feature, I realized that we could take it one step further, “bring the awesome”, and use the Twilio API to integrate VOIP conference calling directly into the app.

Twilio is a cloud-based service that has both client-side and REST-based API’s for adding telephony and SMS features to your desktop and mobile apps. It can also bridge IP and regular voice communications.

There are three components to every Twilio app.

1) Generating a secure passkey to access the API (server-side REST)
2) Creating a UX to place calls (client-side Javascript)
3) Handling Twilio server callbacks with TwiML, the Twilio Markup Language (server-side REST)

app flow

Generating a secure passkey

You can generate a secure passkey by leveraging one of Twilio’s “Helper APIs” which include support for PHP, Ruby, Python, .NET, Java, Salesforce, Node, ColdFusion, and others. My app has access to both ColdFusion and .NET resources, so we decided to build out the initial Twilio handlers using ColdFusion as we feel that it’s a little faster for rapid prototyping. Ultimately, we may port this over to .NET which comprises the overwhelming bulk of the appserver code.

The Twilio ColdFusion helper library is located at the following URL:
https://github.com/jasonfill/ColdFusion-Twilio-Library

Using the helper library to generate a secure passkey is a relatively straightforward process (illustrated below):


<!--- 
 TwilioSettings.cfm contains the following settings:
 - Your Twilio Account ID
 - Your Twilio passcode
 - The Twilio API version that you intend to use
 - The Twilio endpoint (api.twilio.com)
--->
<cfinclude template="TwilioSettings.cfm" />

<!--- Create a new instance of the Twilio Lib, this can be stored in the App scope or elsewhere as a singleton... --->
<cfset REQUEST.TwilioLib = createObject("component", "twilio.TwilioLib").init(
   REQUEST.AccountSid, 
   REQUEST.AuthToken, 
   REQUEST.ApiVersion, 
   REQUEST.ApiEndpoint
) />

<!--- bind for outgoing calls  --->
<cfset cap = REQUEST.TwilioLib.getCapability() />

<cfset params = StructNew() />

<!--- the appsid is registered with Twilio from their Admin GUI --->
<!--- each app has a unique callback URL --->
<cfset appSid = "[insert twilio application id here]">

<!--- Allow the client to make outgoing calls by passing in the appsid --->
<cfset cap.allowClientOutgoing(appSid, params) />

<cfset token = cap.generateToken() />

<cfoutput>
<!DOCTYPE HTML>

<html>
<head>
    <meta charset="UTF-8">
    <title>ModelPractice</title>
<link rel="stylesheet" href="resources/MyApp-all.css"/>
<!--- output the security token --->
<script type="text/javascript">
  TwilioToken = "#token#";
</script>
<script type="text/javascript" src="app.js"></script>
<link rel="stylesheet" href="resources/css/app.css">
</head>
<body>
</body>
</html>
</cfoutput>

Creating a UX to place calls

Now that I had a valid security token accessible through the global Javascript variable TwilioToken, I could get on with the process of building out the user experience, depicted in the image below:

reviewers

Users can initiate a conference call by checking the boxes for with whom they wish to communicate. Once the selection is made, I needed to load the Twilio Javascript library and make the appropriate method calls. I also needed to provide an area that could display status messages as well as toggle the availability of a “hangup” button. To accomplish these tasks, I wrapped the Twilio library in a custom Ext JS class, illustrated below:

Ext.define('Ext.ux.Twilio', {

	config: {
		statusMsg: null, // ui component to display status
		ready: false,
		hangupButton: null // ui button to terminate call
	},

	constructor: function(config) {
		this.initConfig(config);
		this.callParent(arguments);
	},

	init: function(callback, scope, arg1, arg2) {

		if (!window['Twilio']) {

			var me = this;

			// load the twilio api and configure event listeners

			Ext.Loader.loadScript({

				url: '//static.twilio.com/libs/twiliojs/1.1/twilio.min.js',

				onLoad: function() {

					Twilio.Device.setup(TwilioToken, {
						debug: true
					});

					Twilio.Device.ready(function(device) {
						me.getStatusMsg().update('Ready to Connect');
						me.setReady(true);
						callback.call(scope, arg1, arg2);
						me.getHangupButton().hide();
					});

					Twilio.Device.offline(function(device) {
						me.getStatusMsg().update('Connection offline');
						me.getHangupButton().hide();
					});

					Twilio.Device.error(function(error) {
						me.getStatusMsg().update(error);
						console.log('twilio error', error);
						me.getHangupButton().hide();
					});

					Twilio.Device.connect(function(conn) {
						me.getStatusMsg().update("Successfully joined call");
						me.getHangupButton().show();
					});

					Twilio.Device.disconnect(function(conn) {
						me.getStatusMsg().update("Disconnected");
						me.getHangupButton().hide();
					});

				}


			}); // loadscript

		}
	},

	makeOutgoingCall: function(phoneNumber) {

		if (!this.getReady()) {
			this.init(this.makeOutgoingCall, this, phoneNumber);
			return;
		}

		Twilio.Device.connect({
			"PhoneNumber": phoneNumber,
			"tAction" : "placecall"
		});

	},

	// roomId is an arbitrary string that identifies
	// a unique virtual conference room

	// phoneNumbers is a comma-delimited string of 
	// telephone numbers to call and automatically 
	// add people to the conference line.
	
	makeConferenceCall: function(roomId, phoneNumbers) {

		if (!this.getReady()) {
			this.init(this.makeConferenceCall, this, roomId, phoneNumbers);
			return;
		}

		Twilio.Device.connect({
			"conferenceRoom": roomId,
			"phoneNumbers": phoneNumbers,
			"tAction" : "conferencecall"
		});
	}

});

When the user presses the conference call button, I aggregate the phone numbers from the selected records and invoke my custom Twilio class as follows:

(note that I have controller REFs that point to an Ext.Toolbar component containing an Ext.toolbar.TextItem component to display status messages and an Ext.button.Button to enable the caller to terminate the connection)

onConferenceCall: function(panel,grid) {
 
 if (!this.Twilio) {
    this.Twilio = Ext.create('Ext.ux.Twilio', {
        statusMsg : this.getTelStatusBar(),
        hangupButton: this.getHangupButton()
    });
 }

 var selectedRecords = grid.getSelectionModel().getSelection();

 if (selectedRecords.length === 0) {

  // join
  this.Twilio.makeConferenceCall('Room ' + this.surveyInstanceId,"");

 } else {

    // join and dial others
    var phoneNumbers = [];
    for (var i=0; i<selectedRecords.length; i++) {
        // ignore current user
        if (selectedRecords[i].get('reviewerId') != MyApp.app.userCredentials.id) {
            if (!Ext.isEmpty(selectedRecords[i].get('phone'))) {
                phoneNumbers.push(selectedRecords[i].get('phone'));
            }
        }
    }
    this.Twilio.makeConferenceCall('Room ' + this.surveyInstanceId,phoneNumbers.join(','));

 }
}

Handling Twilio REST Callbacks

When the Twilio.Device.connect() method is invoked, the Twilio service accesses a file on my server that provides additional directives via TwiML- Twilio’s XML language. Our application was relatively straightforward – we either needed to dial a single phone number (taction = placecall) or join a conference call (taction=conferencecall). In the case of joining a conference call, I also wanted the moderator to be able to have Twilio proactively call reviewers and add them to the virtual conference room. The “TwiML” application is listed below. Note that while any arguments passed to the Twilio.Device.connect() method will automatically be transmitted to your TwiML app, when you’re invoking the Twilio REST api’s custom passthrough arguments must be enumerated specifically as illustrated on line 58.


<cfsetting enablecfoutputonly="true">

<!--- your Twilio Account ID --->
<cfset accountSid = "[insert account sid here]">

<!--- your "secret" Twilio authorization token --->
<cfset authToken = "[insert auth token here]">

<!--- 
 The caller id phone number that you register 
 with Twilio
--->
<cfset callerIdNumber="[insert caller id number here]">

<cfif taction is "placecall">
  <cfsavecontent variable="out">
   <cfoutput><Number>#PhoneNumber#</Number></cfoutput>
  </cfsavecontent>
  <cfcontent type="text/xml"><cfoutput><?xml version="1.0" encoding="UTF-8"?>
    <Response>
    <Say>Placing your call now.</Say>
    <Dial callerId="#callerIdNumber#">#out#</Dial>
  </Response></cfoutput>
</cfif>


<cfif taction is "conferencecall">

	<cfcontent type="text/xml"><cfoutput><?xml version="1.0" encoding="UTF-8"?>
	<Response>
		<Say>Connecting to the Conference. You will hear music until another reviewer arrives.</Say>
		<Dial callerId="#callerIdNumber#">
		   <Conference>#ConferenceRoom#</Conference>
		</Dial>
	</Response></cfoutput>

	<cfif (isdefined("phoneNumbers"))>
	  <cfset i = 0>
	  <cfloop list="#phonenumbers#" index="thisPhoneNumber">
	      <cfset i=i+1>
		  
		  <cfthread phoneNumber="#thisPhoneNumber#" 
					name="conferenceOtherUser#i#"
					conferenceRoom = "#conferenceRoom#"
					accountSid="#accountSid#"
					authToken="#authToken#"
					callerIdNumber="#callerIdNumber#">
					
			 <cfhttp 
					url="https://api.twilio.com/2010-04-01/Accounts/#accountSid#/Calls"
					port="443"
					method="post"
					username="#accountSid#"
					password="#authtoken#">
					
					<cfhttpparam name="Caller" type="formfield" value="#callerIdNumber#">
					<cfhttpparam name="Called" type="formfield" value="#phoneNumber#">
					<cfhttpparam name="Url" type="formfield" value="http://www.naccho.org/mpdev/VoiceRequest.cfm?taction=conferencecall&conferenceRoom=#urlencodedformat(conferenceRoom)#">
					
					<cfhttpparam name="IfMachine" type="formfield" value="Hangup">
					
			  </cfhttp>	
			  
			  <cflog file="twilio1" text="#cfhttp.filecontent#">
		  </cfthread>
	  
	  </cfloop>

	</cfif>

</cfif>

So what are you waiting for? Add VOIP/Telephony/SMS capabilities to your apps and “bring the awesome!”

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s