-
-
Save ObjectIsAdvantag/f827d2e1beae3fee43f7bb892ef1434a to your computer and use it in GitHub Desktop.
An interactive IVR for the Tech2Day
This file contains 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 | |
} | |
// | |
// 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("", "Y2lzY29zcGFyazovL3VzL1dFQkhPT0svN2U1MzY5MDUtOWU1MS00MWMwLThiNmMtNWEyNDM0NjkyMzUy"); | |
var SparkDebug = new SparkLog("", "Y2lzY29zcGFyazovL3VzL1dFQkhPT0svYWI3YmJjNDctODYzOS00MjE2LTgwODYtODMyM2FlMzQ0ODRh"); | |
// 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 fetchNextSessions() { | |
var url = "https://tech2day2016.cleverapps.io/api/v1/sessions/next"; | |
var response = requestJSONviaGET(url); | |
if (response && response instanceof Array) { | |
return response; | |
} | |
return []; | |
} | |
// Returns true if successfully registered | |
function registerSandbox(email) { | |
var url = "https://tech2day2016.cleverapps.io/api/v1/spark/onboard?email=" + email + "&token=CiscoDevNet"; | |
return requestStatusCodeWithGET(url); | |
} | |
// 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("Bienvenue au Tech2Day 2016 ! Appelez ce numéro pour découvrir les prochaines sessions, transmettez votre email pour rejoindre la Spark Room."); | |
info("sent welcome SMS to : +" + currentCall.callerID); | |
} | |
else { // register to the Sandbox Room | |
switch (registerSandbox(extractedEmail)) { | |
case 200: | |
say("" + extractedEmail + " a rejoint l'équipe Tech2Day2016, 'Sandbox': https://web.ciscospark.com/#/rooms/6a480400-32b6-11e6-a2c1-b1ee3f4465dd"); | |
info("" + currentCall.callerID + " added to sandbox with email: " + extractedEmail); | |
break; | |
case 204: | |
say("" + extractedEmail + " fait déjà partie de l'équipe Tech2Day2016, merci de votre participation !"); | |
info("" + currentCall.callerID + " already added to sandbox with email: " + extractedEmail); | |
break; | |
default: | |
say("Désolé nous n'avons pas pu ajouter " + extractedEmail + " à l'équipe Tech2Day2016, essayez à nouveau..."); | |
debug("" + currentCall.callerID + " could not be added to sandbox, with email: " + extractedEmail); | |
break; | |
} | |
} | |
// End of SMS custom logic | |
} | |
else { // Voice | |
// Speak a welcome message | |
debug("incoming call from: " + currentCall.callerID); | |
wait(1000); | |
say("Bienvenue au TekToutDai 2016", { | |
voice: "Aurelie" | |
}); | |
wait(500); | |
var now = new Date(Date.now()); | |
say("Il est " + (now.getHours() + 2) + " heures et " + now.getMinutes() + " minutes", { | |
voice: "Aurelie" | |
}); | |
info("spoke the welcome message to: " + currentCall.callerID); | |
wait(500); | |
// Checking session list | |
var listOfSessions = fetchNextSessions(); | |
debug("Retreived " + listOfSessions.length + " sessions after GMT (Paris -2H): " + now.toString()); | |
var nbSessions = listOfSessions.length; | |
if (nbSessions == 0) { | |
say("Désolé, nous n'avons trouvé aucune session à venir. Au revoir.", { | |
voice: "Aurelie" | |
}); | |
info("no upcoming sessions for: " + currentCall.callerID); | |
wait(1000); | |
hangup(); | |
throw createError("no upcoming session, exiting"); | |
} | |
// Pick a maximum of 10 sessions | |
var MAX = 10; | |
if (nbSessions > MAX) { | |
debug("more than " + MAX + " sessions at: " + now.String()) | |
nbSessions = MAX; | |
} | |
say("<speak>Voici les " + nbSessions + " prochaines sessions</speak>", { | |
voice: "Aurelie" | |
}); | |
// Propose MENU | |
var inviteIVR = "Taper 0 pour les sessions en cours<break time='200ms'/> 1 pour recevoir plus de détails <break time='200ms'/> 2 pour suivant <break time='200ms'/> 3 pour précédent"; | |
var numero = 0; | |
var safeguard = 0; // to avoid loops on the scripting platform | |
while (numero < nbSessions && numero >= 0) { | |
debug("speaking session number: " + numero); | |
safeguard++; | |
if (safeguard > 50) { | |
debug("Safeguard activated for: " + currentCall.callerID); | |
hangup(); | |
throw createError("Safeguard activated"); | |
} | |
var courante = listOfSessions[numero]; | |
var event = ask("<speak><break time='300ms'/>" + courante.title + | |
" <break time='300ms'/> ce " + courante.day + | |
" <break time='300ms'/> à " + courante.begin + | |
" <break time='300ms'/> par " + courante.speaker + | |
" <break time='500ms'/>" + inviteIVR + | |
"</speak>", { | |
voice: "Aurelie", | |
//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: 5, | |
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("Désolé je n'ai pas reçu votre réponse", { | |
voice: "Aurelie" | |
}); | |
}); | |
event.onBadChoice(function() { | |
debug("bad choice for user " + currentCall.callerID); | |
say("Désolé je n'ai pas compris votre réponse", { | |
voice: "Aurelie" | |
}); | |
}); | |
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: Request for sessions in progress"); | |
say("Cette fonctionnalité n'est pas encore implémenté Désolé", { | |
voice: "Aurelie" | |
}); | |
break; | |
case 1: | |
debug("1: Details for session: " + courante.title + " for: +" + currentCall.callerID); | |
say("<speak>C'est noté <break time='300ms'/> Nous vous transmettons plus de détails par SMS</speak>", { | |
voice: "Aurelie" | |
}); | |
// Send an SMS in a new session | |
var forkedCall = call(currentCall.callerID, { network: "SMS" } ); | |
forkedCall.value.say("Tech2Day2016, " + courante.day + " à " + courante.begin | |
+ ", '"+ courante.title + | |
"' à '" + courante.location + | |
"' " + courante.url); | |
forkedCall.value.hangup(); | |
info("sms sent for: " + courante.title + " to: +" + currentCall.callerID); | |
// Then move to next session | |
wait(1000); | |
if (numero == (nbSessions - 1)) { | |
say("Désolé nous n'avons plus de session après celle-ci", { | |
voice: "Aurelie" | |
}); | |
wait(500); | |
} | |
else { | |
say("Session suivante", { | |
voice: "Aurelie" | |
}); | |
wait(500); | |
numero++; | |
} | |
break; | |
case 2: | |
debug("2: Next from session: " + courante.title + " for: " + currentCall.callerID); | |
if (numero == (nbSessions - 1)) { | |
say("Désolé nous n'avons plus de session après celle-ci", { | |
voice: "Aurelie" | |
}); | |
wait(500); | |
} | |
else { | |
say("OK session suivante", { | |
voice: "Aurelie" | |
}); | |
wait(500); | |
numero++; | |
} | |
break; | |
case 3: | |
debug("2: Previous from session: " + courante.title + " for: " + currentCall.callerID); | |
if (numero == 0) { | |
say("Désolé nous n'avons pas de session avant celle-ci", { | |
voice: "Aurelie" | |
}); | |
wait(500); | |
} | |
else { | |
say("OK session précédente", { | |
voice: "Aurelie" | |
}); | |
wait(500); | |
numero--; | |
} | |
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 session: " + courante.title + " for: " + currentCall.callerID); | |
say("Session suivante.", { | |
voice: "Aurelie" | |
}); | |
wait(500); | |
numero++; | |
} | |
} | |
debug("Plus de session pour " + currentCall.callerID); | |
say("<speak>Merci de votre participation au TekToutDai <break time='500ms'/> Au revoir</speak>", { | |
voice: "Aurelie" | |
}); | |
wait(100); | |
hangup(); | |
} | |
} | |
else { | |
debug("new API request, nothing there."); | |
// Checking current time | |
var now = new Date(Date.now()); | |
debug("Il est exactement " + (now.getHours() + 2) + " heures et " + now.getMinutes() + " minutes"); | |
// Checking session list | |
var listOfSessions = fetchNextSessions(); | |
debug("Retreived " + listOfSessions.length + " sessions"); | |
if (listOfSessions & listOfSessions.length > 0) { | |
debug("Showing first: " + listOfSessions[0]); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment