Last active
September 17, 2016 11:53
-
-
Save ObjectIsAdvantag/0cfdb9cdf1ea6168f3cc7ef7c59832b7 to your computer and use it in GitHub Desktop.
Tropo Javascipt IVR that asks for activities from an external API, sends an SMS, picks email addresses, adds particimants to a Spark room,, and does ChatOps to Spark: from Cisco Live Vegas 2016: http://www.slideshare.net/CiscoDevNet/cisco-spark-tropo-api-workshop/2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// QUICK START GUIDE | |
// | |
// 1. Clone this gists and make it private | |
// 2. Create an incoming integratin in a Spark Room from the Spark Web client : http://web.ciscospark.com | |
// 3. Replace YOUR_INTEGRATION_SUFFIX by the integration id, example: Y2lzY29zcGFyazovL3VzL1dFQkhPT0svZjE4ZTI0MDctYzg3MS00ZTdmLTgzYzEtM2EyOGI1N2ZZZZZ | |
// 4. Create your Tropo application pointing to your gist URL, append /raw/tropodevops-sample.js to the gist URL | |
// | |
// Cisco Spark Logging Library for Tropo | |
// | |
// Factory for the Spark Logging Library, with 2 parameters | |
// - the name of the application will prefix all your logs, | |
// - the Spark Incoming integration (to which logs will be posted) | |
// To create an Incoming Integration | |
// - click integrations in the right pane of a Spark Room (Example : I create a dedicated "Tropo Logs" room) | |
// - select incoming integration | |
// - give your integration a name, it will be displayed in the members lists (Example : I personally named it "from tropo scripting") | |
// - copy your integration ID, you'll use it to initialize the SparkLibrary | |
function SparkLog(appName, incomingIntegrationID) { | |
if (!appName) { | |
appName = ""; | |
//log("SPARK_LOG : bad configuration, no application name, exiting..."); | |
//throw createError("SparkLibrary configuration error: no application name specified"); | |
} | |
this.tropoApp = appName; | |
if (!incomingIntegrationID) { | |
log("SPARK_LOG : bad configuration, no Spark incoming integration URI, exiting..."); | |
throw createError("SparkLibrary configuration error: no Spark incoming integration URI specified"); | |
} | |
this.sparkIntegration = incomingIntegrationID; | |
log("SPARK_LOG: all set for application:" + this.tropoApp + ", posting to integrationURI: " + this.sparkIntegration); | |
} | |
// This function sends the log entry to the registered Spark Room | |
// Invoke this function from the Tropo token-url with the "sparkIntegration" parameter set to the incoming Webhook ID you'll have prepared | |
// Returns true if the log entry was acknowledge by Spark (ie, got a 2xx HTTP status code) | |
SparkLog.prototype.log = function(newLogEntry) { | |
// Robustify | |
if (!newLogEntry) { | |
newLogEntry = ""; | |
} | |
var result; | |
try { | |
// Open Connection | |
var url = "https://api.ciscospark.com/v1/webhooks/incoming/" + this.sparkIntegration; | |
var connection = new java.net.URL(url).openConnection(); | |
// Set timeout to 10s | |
connection.setReadTimeout(10000); | |
connection.setConnectTimeout(10000); | |
// Method == POST | |
connection.setRequestMethod("POST"); | |
connection.setRequestProperty("Content-Type", "application/json"); | |
// TODO : check if this cannot be removed | |
connection.setRequestProperty("Content-Length", newLogEntry.length); | |
connection.setUseCaches(false); | |
connection.setDoInput(true); | |
connection.setDoOutput(true); | |
//Send Post Data | |
var bodyWriter = new java.io.DataOutputStream(connection.getOutputStream()); | |
log("SPARK_LOG: posting: " + newLogEntry + " to: " + url); | |
var contents = '{ "text": "' + this.tropoApp + ': ' + newLogEntry + '" }' | |
bodyWriter.writeBytes(contents); | |
bodyWriter.flush(); | |
bodyWriter.close(); | |
var result = connection.getResponseCode(); | |
log("SPARK_LOG: read response code: " + result); | |
if (result < 200 || result > 299) { | |
log("SPARK_LOG: could not log to Spark, message format not supported"); | |
return false; | |
} | |
} | |
catch (e) { | |
log("SPARK_LOG: could not log to Spark, socket Exception or Server Timeout"); | |
return false; | |
} | |
log("SPARK_LOG: log successfully sent to Spark, status code: " + result); | |
return true; // success | |
} | |
// | |
// Cisco Spark Client Library for Tropo | |
// | |
// Factory for the Spark Library, with 1 parameter | |
// - the Spark API token | |
function SparkClient(spark_token) { | |
if (!spark_token) { | |
log("SPARK_CLIENT : bad configuration, no API token, exiting..."); | |
throw createError("SparkClient configuration error: no API token specified"); | |
} | |
this.token = spark_token; | |
log("SPARK_CLIENT: all set; ready to invoke spark"); | |
} | |
// Returns a status code | |
SparkClient.prototype.createMemberShip = function(roomID, email) { | |
// Robustify | |
if (!roomID) { | |
return 400; | |
} | |
if (!email) { | |
return 400; | |
} | |
var result; | |
try { | |
// Open Connection | |
var url = "https://api.ciscospark.com/v1/memberships"; | |
var connection = new java.net.URL(url).openConnection(); | |
// Set timeout to 10s | |
connection.setReadTimeout(10000); | |
connection.setConnectTimeout(10000); | |
// Method == POST | |
connection.setRequestMethod("POST"); | |
connection.setRequestProperty("Content-Type", "application/json"); | |
connection.setRequestProperty("Authorization", "Bearer " + this.token); | |
// Prepare payload | |
var payload = '{ "roomId": "' + roomID + '", "personEmail": "' + email + '", "isModerator": "false" }' | |
// [TODO] Check if this cannot be removed | |
connection.setRequestProperty("Content-Length", payload.length); | |
connection.setUseCaches(false); | |
connection.setDoInput(true); | |
connection.setDoOutput(true); | |
//Send Post Data | |
var bodyWriter = new java.io.DataOutputStream(connection.getOutputStream()); | |
log("SPARK_CLIENT: posting: " + payload + " to: " + url); | |
bodyWriter.writeBytes(payload); | |
bodyWriter.flush(); | |
bodyWriter.close(); | |
result = connection.getResponseCode(); | |
log("SPARK_CLIENT: read response code: " + result); | |
} | |
catch (e) { | |
log("SPARK_CLIENT: could not log to Spark, socket Exception or Server Timeout"); | |
return 500; | |
} | |
if (result < 200 || result > 299) { | |
log("SPARK_CLIENT: could not add user with email: " + email + " to room:" + roomID); | |
} | |
else { | |
log("SPARK_CLIENT: user with email: " + email + " added to room:" + roomID); | |
} | |
return result; // success | |
} | |
// | |
// Library to send outbound API calls | |
// | |
// Returns the JSON object at URL or undefined if cannot be accessed | |
function requestJSONviaGET(requestedURL) { | |
try { | |
var connection = new java.net.URL(requestedURL).openConnection(); | |
connection.setDoOutput(false); | |
connection.setDoInput(true); | |
connection.setInstanceFollowRedirects(false); | |
connection.setRequestMethod("GET"); | |
connection.setRequestProperty("Content-Type", "application/json"); | |
connection.setRequestProperty("charset", "utf-8"); | |
connection.connect(); | |
var responseCode = connection.getResponseCode(); | |
log("JSON_LIBRARY: read response code: " + responseCode); | |
if (responseCode < 200 || responseCode > 299) { | |
log("JSON_LIBRARY: request failed"); | |
return undefined; | |
} | |
// Read stream and create response from JSON | |
var bodyReader = connection.getInputStream(); | |
// [WORKAROUND] We cannot use a byte[], not supported on Tropo | |
// var myContents= new byte[1024*1024]; | |
// bodyReader.readFully(myContents); | |
var contents = new String(org.apache.commons.io.IOUtils.toString(bodyReader)); | |
var parsed = JSON.parse(contents); | |
log("JSON_LIBRARY: JSON is " + parsed.toString()); | |
return parsed; | |
} | |
catch (e) { | |
log("JSON_LIBRARY: could not retreive contents, socket Exception or Server Timeout"); | |
return undefined; | |
} | |
} | |
// Returns the Status Code when GETting the URL | |
function requestStatusCodeWithGET(requestedURL) { | |
try { | |
var connection = new java.net.URL(requestedURL).openConnection(); | |
connection.setDoOutput(false); | |
connection.setDoInput(true); | |
connection.setInstanceFollowRedirects(false); | |
connection.setRequestMethod("GET"); | |
connection.setRequestProperty("Content-Type", "application/json"); | |
connection.setRequestProperty("charset", "utf-8"); | |
connection.connect(); | |
var responseCode = connection.getResponseCode(); | |
return responseCode; | |
} | |
catch (e) { | |
log("JSON_LIBRARY: could not retreive contents, socket Exception or Server Timeout"); | |
return 500; | |
} | |
} | |
// | |
// Script logic starts here | |
// | |
// Let's create several instances for various log levels | |
var SparkInfo = new SparkLog("", "ADD YOUR SPARK ROOM INCOMING ID HERE"); | |
var SparkDebug = new SparkLog("", "ADD YOUR SPARK ROOM INCOMING ID HERE"); | |
// info level used to get a synthetic sump up of what's happing | |
function info(logEntry) { | |
log("INFO: " + logEntry); | |
SparkInfo.log(logEntry); | |
// Uncomment if you opt to go for 2 distinct Spark Rooms for DEBUG and INFO log levels | |
SparkDebug.log(logEntry); | |
} | |
// debug level used to get detail informations | |
function debug(logEntry) { | |
log("DEBUG: " + logEntry); | |
SparkDebug.log(logEntry); | |
} | |
// returns true or false | |
function isEmail(email) { | |
// extract from http://stackoverflow.com/questions/46155/validate-email-address-in-javascript | |
var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; | |
return re.test(email); | |
} | |
// returns an email address if found in the phrase specified | |
function extractEmail(phrase) { | |
if (phrase) { | |
var parts = phrase.split(" "); | |
for (var i = 0; i < parts.length; i++) { | |
if (isEmail(parts[i])) { | |
return parts[i]; | |
} | |
} | |
} | |
return null; | |
} | |
function fetchNextActivities() { | |
var url = "https://devnetzone.cleverapps.io/api/v1/activities/next"; | |
var response = requestJSONviaGET(url); | |
if (response && response instanceof Array) { | |
return response; | |
} | |
return []; | |
} | |
// Adds the user with email to the room, | |
// Returns an HTTP status referenced here: https://developer.ciscospark.com/endpoint-memberships-post.html | |
function addUserToDevnetZoneRoom(email) { | |
var client = new SparkClient("PUT YOUR SPARK API TOKEN HERE"); | |
var result = client.createMemberShip("PUT THE ID OF THE SPARK ROOM YOU WANT TO JOIN HERE", email); | |
return result; | |
} | |
// Adds the user with email to the room, | |
// Returns an HTTP status referenced here: https://developer.ciscospark.com/endpoint-memberships-post.html | |
function addUserToAskTheBotRoom(email) { | |
var client = new SparkClient("PUT YOUR SPARK API TOKEN HERE"); | |
var result = client.createMemberShip("PUT THE ID OF THE SPARK ROOM YOU WANT TO JOIN HERE", email); | |
return result; | |
} | |
// Returns true if successfully registered | |
function registerSandbox(email) { | |
var url = "https://devnetzone.cleverapps.io/api/v1/spark/register?email=" + email + "&token=SECRET"; | |
return requestStatusCodeWithGET(url); | |
} | |
// Convert time from CET to Vegas TZ | |
// example : convertCETtoVegas(new Date().getTime()) | |
// examople : "The local time in Vegas is " + convertCETtoVegas(new Date().getTime()).toLocaleString() | |
function convertCETtoVegas(ref) { | |
var offset = -7; | |
return new Date(ref + (3600000 * offset)); | |
} | |
// Returns current time in Vegas in format HH:MM AM|PM | |
function timeInVegas() { | |
var meridian = "AM"; | |
var nowInVegas = convertCETtoVegas(Date.now()); | |
var hours = nowInVegas.getHours(); | |
if (hours > 12) { | |
meridian = "PM"; | |
hours -= 12; | |
} | |
return "" + hours + ":" + nowInVegas.getMinutes() + " " + meridian; | |
} | |
var currentVoice = "Vanessa"; | |
// You may check currentCall features here : https://www.tropo.com/docs/scripting/currentcall | |
if (currentCall) { | |
if (currentCall.network == "SMS") { // SMS | |
// Check we received a valid email address | |
var input = currentCall.initialText; | |
debug("received: " + input + ", from: +" + currentCall.callerID); | |
// check email is present, | |
var extractedEmail = extractEmail(input); | |
if (!extractedEmail) { // send Welcome message | |
say("Welcome to the @CiscoDevNet Zone. Text your email to check our activities at Cisco Live Vegas #CLUS."); | |
info("sent welcome SMS to : +" + currentCall.callerID); | |
} | |
else { // register to the Sandbox Room | |
var statusCode = addUserToDevnetZoneRoom(extractedEmail); | |
switch (statusCode) { | |
case 200: | |
// [TODO] enhance with a short link: "https://web.ciscospark.com/#/rooms/6a480400-32b6-11e6-a2c1-b1ee3f4465dd" | |
say("Welcome. " + extractedEmail + " is now part of the 'DevNet Zone at CLUS2016'. Launch Cisco Spark to meet."); | |
info("" + currentCall.callerID + " added to the DevNetZone CLUS Activities team, with email: " + extractedEmail); | |
var statusCode2 = addUserToAskTheBotRoom(extractedEmail); | |
if (statusCode2 == 200) { | |
debug("added email: " + extractedEmail + " to the 'Ask the bot' room"); | |
} | |
break; | |
case 409: | |
say("You're all set: already a member of the 'DevNet Zone at CLUS2016' room. Launch Cisco Spark to meet."); | |
info("" + currentCall.callerID + " already added to team with email: " + extractedEmail); | |
var statusCode2 = addUserToAskTheBotRoom(extractedEmail); | |
if (statusCode2 == 200) { | |
debug("added email: " + extractedEmail + " to the 'Ask the bot' room"); | |
} | |
break; | |
default: | |
say("Sorry but we could not add " + extractedEmail + " to the 'DevNetZone Activities' Spark room."); | |
debug("" + currentCall.callerID + " could not be added to team, status:" + statusCode + " with email: " + extractedEmail); | |
break; | |
} | |
} | |
// End of SMS custom logic | |
} | |
else { // Voice | |
// Speak a welcome message | |
debug("incoming call from: " + currentCall.callerID); | |
wait(1000); | |
say("Welcome to the DevNet Zone. It is now " + timeInVegas() + " in Vegas.", { | |
voice: currentVoice | |
}); | |
info("spoke the welcome message to: +" + currentCall.callerID); | |
// Checking session list | |
var listOfActivities = fetchNextActivities(); | |
debug("retreived " + listOfActivities.length + " activities after time in Vegas: " + timeInVegas()); | |
var nbActivities = listOfActivities.length; | |
if (nbActivities == 0) { | |
say("Sorry, we did not find any upcoming activity. Good bye.", { | |
voice: currentVoice | |
}); | |
info("no upcoming sessions for: +" + currentCall.callerID); | |
wait(1000); | |
hangup(); | |
throw createError("no upcoming activity, exiting"); | |
} | |
// Pick a maximum of 10 sessions | |
var MAX = 10; | |
if (nbActivities > MAX) { | |
debug("more than " + MAX + " activities after: " + timeInVegas()) | |
nbActivities = MAX; | |
} | |
say("Here are the next 10 activities.", { | |
voice: currentVoice | |
}); | |
wait(500); | |
// Propose MENU, removed option 0 for now | |
var inviteIVR = "Dial Zero to join the DevNet Zone, One to receive more details by SMS, Two for next activity, and Three for previous activity."; | |
var num = 0; | |
var safeguard = 0; // to avoid loops on the scripting platform | |
while (num < nbActivities && num >= 0) { | |
debug("speaking activity number: " + (num+1)); | |
safeguard++; | |
if (safeguard > 50) { | |
debug("safeguard activated for: +" + currentCall.callerID); | |
hangup(); | |
throw createError("safeguard activated"); | |
} | |
var currentActivity = listOfActivities[num]; | |
var event = ask("" + currentActivity.category + ". " + currentActivity.title + " this " + currentActivity.beginDay + " at " + currentActivity.beginTime + ". " + inviteIVR, { | |
//ask("<speak> " + currentActivity.title + " <break time='300ms'/> this " + currentActivity.day + " <break time='300ms'/> at " + currentActivity.begin + " <break time='300ms'/> by " + currentActivity.speaker + " <break time='500ms'/>" + inviteIVR + "</speak>", { | |
voice: currentVoice, | |
//choices: "1(inscrire), 2(detail), 3(suivant)", recognizer: "fr-fr", mode: 'any', // DTMF + VOICE | |
//choices: "1(One,Suscribe),2(Two,Details),3(Three,Next)", recognizer: "en-us", mode: 'any', | |
choices: "0,1,2,3", | |
mode: 'dtmf', // DTMF only | |
attempts: 1, | |
timeout: 3, | |
bargein: true, // Take action immediately when a Dial Tone is heard | |
onEvent: function(event) { | |
event.onTimeout(function() { | |
debug("choice timeout for user: +" + currentCall.callerID); | |
say("Sorry but I did not receive your answer", { | |
voice: currentVoice | |
}); | |
}); | |
event.onBadChoice(function() { | |
debug("bad choice for user: +" + currentCall.callerID); | |
say("Sorry I did not understand your answer", { | |
voice: currentVoice | |
}); | |
}); | |
event.onHangup(function() { | |
debug("user has hanged up +" + currentCall.callerID); | |
}); | |
} | |
}); | |
// Take action corresponding to user choice | |
if (event.name == 'choice') { | |
debug("user: " + currentCall.callerID + " chose " + event.value); | |
var selected = parseInt(String(event.value)); | |
switch (selected) { | |
case 0: | |
debug("0: Infos to join the Spark Room for: +" + currentCall.callerID); | |
say("You can join by texting your email address to this number. You will then be added to the Cisco Spark room and discover upcoming activities happening in the DevNet Zone.", { | |
voice: currentVoice | |
}); | |
break; | |
case 1: | |
debug("1: Details for activity: " + currentActivity.title + " for: +" + currentCall.callerID); | |
// Send SMS in a new session | |
var forkedCall = call(currentCall.callerID, { | |
network: "SMS" | |
}); | |
forkedCall.value.say("" + currentActivity.beginDay + ", " + currentActivity.beginTime + | |
": '" + currentActivity.title + | |
"' at '" + currentActivity.location + | |
"' "); | |
say("Got it. Sending you more details by SMS.", { | |
voice: currentVoice | |
}); | |
// Send 2nd SMS | |
forkedCall.value.say("Text your email address to this number to join the 'DevNet Zone #CLUS Activities' . See you in the Cisco Spark room says the @CiscoDevNet bot"); | |
forkedCall.value.hangup(); | |
info("sms sent for: " + currentActivity.title + " to: +" + currentCall.callerID); | |
// Then move to next session | |
wait(500); | |
if (num == (nbActivities - 1)) { | |
say("Sorry, we do not have any activity after this one", { | |
voice: currentVoice | |
}); | |
wait(500); | |
} | |
else { | |
say("Moving to next activity", { | |
voice: currentVoice | |
}); | |
wait(500); | |
num++; | |
} | |
break; | |
case 2: | |
debug("2: Next from activity: " + currentActivity.title + " for: +" + currentCall.callerID); | |
if (num == (nbActivities - 1)) { | |
say("Sorry, no more activity after this one", { | |
voice: currentVoice | |
}); | |
wait(500); | |
} | |
else { | |
say("Got it, moving forward to next activity", { | |
voice: currentVoice | |
}); | |
wait(500); | |
num++; | |
} | |
break; | |
case 3: | |
debug("2: Previous from activity: " + currentActivity.title + " for: +" + currentCall.callerID); | |
if (num == 0) { | |
say("Sorry we do not have any activity before this one", { | |
voice: currentVoice | |
}); | |
wait(500); | |
} | |
else { | |
say("Got it, going back to previous activity", { | |
voice: currentVoice | |
}); | |
wait(500); | |
num--; | |
} | |
break; | |
default: | |
debug("unexpected choice from: " + currentCall.callerID); | |
hangup(); | |
throw createError("unexpected choice, exiting"); | |
} | |
} | |
else { // No choice was made, pick next session | |
debug("X: no choice, picking next activity: " + currentActivity.title + " for: +" + currentCall.callerID); | |
say("Moving forward to next activity ", { | |
voice: currentVoice | |
}); | |
wait(500); | |
num++; | |
} | |
} | |
debug("no more activity for +" + currentCall.callerID); | |
say("<speak>thank you for joing the DevNet zone <break time='500ms'/> Good bye</speak>", { | |
voice: currentVoice | |
}); | |
wait(100); | |
hangup(); | |
} | |
} | |
else { | |
debug("new API request"); | |
// Checking current time | |
var now = new Date(Date.now()); | |
debug("it is now " + (now.getHours() - 7) + " hours and " + now.getMinutes() + " minutes in Vegas"); | |
debug("local time in Vegas is now " + convertCETtoVegas(Date.now()).toLocaleString()); | |
debug("time in Vegas is now " + timeInVegas()); | |
// Checking session list | |
var listOfActivities = fetchNextActivities(); | |
debug("retreived: " + listOfActivities.length + " activity"); | |
if (listOfActivities & listOfActivities.length > 0) { | |
debug("Showing first among next activity: " + listOfActivities[0]); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment