Skip to content

Instantly share code, notes, and snippets.

@ObjectIsAdvantag
Last active June 28, 2016 20:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ObjectIsAdvantag/f827d2e1beae3fee43f7bb892ef1434a to your computer and use it in GitHub Desktop.
Save ObjectIsAdvantag/f827d2e1beae3fee43f7bb892ef1434a to your computer and use it in GitHub Desktop.
An interactive IVR for the Tech2Day
// 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