diff --git a/endpoint/avayaVoiceChannel/README.MD b/endpoint/avayaVoiceChannel/README.MD index f043826..a8a0c8e 100644 --- a/endpoint/avayaVoiceChannel/README.MD +++ b/endpoint/avayaVoiceChannel/README.MD @@ -29,8 +29,12 @@ While creating Cognigy flow, you can use Avaya Extension nodes such as play,prom **sms** - sends the sms messages in the voice channel flow. It's not needed for sms channel. **redirect** - redirects current Cognigy flow to another flow and won't return. It requires the endpoint url of the target flow. **locale** - sets the locale language and the voice type on CPaaS so that the intended language and the voice will be spoken to the customers. +**hours** - get the business hours with the settgins of holiday and hours of operations # Deploy # -A Cognigy flow should be created and deployed with the endpoint type of Avaya voice channel and the ``Enable Input Transformer``, ``Enable Output Transformer`` and ``Enable Execution Finished Transformer`` have to be ``enabled``. - +A Cognigy flow should be created and deployed with the endpoint type of Avaya voice channel with the optional parameter ``CpaaS Token`` which authenticates CPaaS's signature, and the ``Enable Input Transformer``, ``Enable Output Transformer`` and ``Enable Execution Finished Transformer`` have to be ``enabled``. +``` + CPaaS Token + 90b196e676074f52xb4de0a2d13af4f3 +``` Next copy the endpoint url and paste it into the ``Voice Configuration`` -> ``WebLink`` of the phone number in Avaya CPaaS. Now it's ready for you to call the number to experience the Cognigy's flow. diff --git a/endpoint/avayaVoiceChannel/icon.png b/endpoint/avayaVoiceChannel/icon.png index 2d155bc..d9062fc 100644 Binary files a/endpoint/avayaVoiceChannel/icon.png and b/endpoint/avayaVoiceChannel/icon.png differ diff --git a/endpoint/avayaVoiceChannel/transformer.ts b/endpoint/avayaVoiceChannel/transformer.ts index e9632f4..bd0fcea 100644 --- a/endpoint/avayaVoiceChannel/transformer.ts +++ b/endpoint/avayaVoiceChannel/transformer.ts @@ -21,17 +21,58 @@ interface CPaaSBody { UrlBase: string; Digits: string; SpeechResult: string; + SpeechResultError: string; Body: string; SmsSid: string; + SessionId: string; } -const COGNIGY_BASE_URL = 'https://endpoint-trial.cognigy.ai/'; const DEBUG_MODE = false; const MAX_CONF_PARTIES = 2; const DEFAULT_NUM_DIGITS = 1; const DEFAULT_LANGUAGE = 'en-US'; -const DEFAULT_GATHER_TIMEOUT = 3; +const DEFAULT_GATHER_TIMEOUT = 10; const DEFAULT_VOICE = 'woman'; +const DEFAULT_CALLER_ID = '18004567890'; +const DEFAULT_API_VERSION = 'v2'; +const HTTPS = 'https://'; +const REDIRECT_PARAMS = '?PlayStatus=completed&SpeechResult=&SpeechResultError=redirect&Confidence=0'; +/** + * creates CPaaS signature + */ +function getSignature(authToken, url, params) { + const data = Object.keys(params) + .sort() + .reduce((acc, key) => acc + key + params[key], url); + return crypto + .createHmac('sha1', authToken) + .update(data) + .digest('base64'); +} +/** + * validate the signature +*/ +function validSignature(endpoint, request, url) { + const requestSignature = request['headers']['x-zang-signature']; + if (requestSignature && + (requestSignature == getSignature(endpoint.settings.cpaasToken,url,request.body)) || + (requestSignature == getSignature(endpoint.settings.cpaasToken,url+REDIRECT_PARAMS,request.body))) { + return true; + } else { + return false; + } +} +/** + * get current timestamp + */ +function getTimestamp() { + const timestamp = new Date(); + return (timestamp.getFullYear().toString() + + (timestamp.getMonth()+1).toString() + + timestamp.getDate().toString() + + timestamp.getHours().toString() + + timestamp.getMinutes().toString()); +} createRestTransformer({ @@ -58,28 +99,45 @@ createRestTransformer({ * every Endpoint, and the example above needs to be adjusted * accordingly. */ - const { body } = request as CPaaSRequest; + const { headers, body } = request; + const { host } = headers; if (DEBUG_MODE) { console.log('body='.concat(JSON.stringify(body))); } - const { From, To, CallSid, Digits, SpeechResult, SmsSid, Body } = body; + const { AccountSid, ApiVersion, Digits, CallSid, From, SpeechResult, SpeechResultError, To, UrlBase, Body, SessionId } = body; + if (CallSid && endpoint?.settings?.cpaasToken && !validSignature(endpoint,request,UrlBase)) { + response.status(401).send('Unauthorized'); + console.log('Unauthorized') + return null; + } else if (DEBUG_MODE) { + console.log('valid signature or turned off'); + } + if (ApiVersion != DEFAULT_API_VERSION) { + throw Error('wrong CPaaS API version '.concat(ApiVersion)); + } + if (SpeechResultError == 'redirect') { + return null; + } const userId = From; - const sessionId = CallSid ? CallSid : SmsSid; + const sessionId = CallSid ? CallSid : (SessionId ? SessionId : (userId+getTimestamp())); let sessionStorage = await getSessionStorage(userId,sessionId); + if (!sessionStorage.urlbase) { + sessionStorage.urlbase = host; + } sessionStorage.From = From; sessionStorage.To = To; - sessionStorage.cpaas_channel = CallSid ? 'call' : 'sms'; + sessionStorage.cpaasChannel = CallSid ? 'call' : 'sms'; sessionStorage.numberOfDigits = DEFAULT_NUM_DIGITS; - let data = {"call": false,"sms":false, "phone":From}; + let data = {"accountSid":AccountSid, "apiVersion":ApiVersion, "call": false,"sms":false, "phone":From}; const menu = sessionStorage['menu'] ? sessionStorage['menu'] : {}; let text = ''; - switch (sessionStorage.cpaas_channel) { + switch (sessionStorage.cpaasChannel) { case 'call': text = Digits ? (menu[Digits] ? menu[Digits] : Digits.replace(/\s+/g, '')) : SpeechResult; data.call = true; break; case 'sms': - text = Body; + text = Body.match(/^\d$/) ? (menu[Body] ? menu[Body] : Body) : Body; data.sms = true; break; default: @@ -127,12 +185,16 @@ createRestTransformer({ activities.forEach( (activity) => { switch (activity.name) { case 'handover': + let callerId = 'callerId="' + ((activity.activityParams.from != null && activity.activityParams.from != '') ? activity.activityParams.from : '{{To}}') + '"'; const handoverType = activity.activityParams.handoverType; if (handoverType === "phone") { const dest = activity.activityParams.destination; + if (dest == userId && callerId == '') { + callerId = 'callerId="' + DEFAULT_CALLER_ID + '"'; + } const cbUrl = (activity.activityParams.callbackUrl != "") ? (' callbackUrl="' + activity.activityParams.callbackUrl + '"') : ''; - let dial = '' + dest + ''; + let dial = '' + dest + ''; output.text = (output.text != null) ? (output.text + dial) : dial; sessionStorage.dial = true; } else if (handoverType === "sip") { @@ -140,10 +202,10 @@ createRestTransformer({ (' callbackUrl="' + activity.activityParams.callbackUrl + '"') : ''; const user = activity.activityParams.user; const domain = activity.activityParams.domain; - const userName = activity.activityParams.connection.userName; + const username = activity.activityParams.connection.username; const password = activity.activityParams.connection.password; let sipUrl = user.concat("@".concat(domain)); - let dial ='' + sipUrl + ';transport=tcp'; + let dial ='' + sipUrl + ';transport=tcp'; output.text = (output.text != null) ? (output.text + dial) : dial; sessionStorage.dial = true; } @@ -155,12 +217,12 @@ createRestTransformer({ sessionStorage['menu'] = menu; sessionStorage.numberOfDigits = DEFAULT_NUM_DIGITS; if (activity.activityParams.menuText) { - output.text = (sessionStorage.cpaas_channel == 'sms') ? activity.activityParams.menuText : (say + activity.activityParams.menuText + ''); + output.text = (sessionStorage.cpaasChannel == 'sms') ? activity.activityParams.menuText : (say + activity.activityParams.menuText + ''); } } else if (promptType === 'number') { sessionStorage.numberOfDigits = activity.activityParams.numberOfDigits; if (activity.activityParams.numberText) { - output.text = (sessionStorage.cpaas_channel == 'sms') ? activity.activityParams.numberText : (say + activity.activityParams.numberText + ''); + output.text = (sessionStorage.cpaasChannel == 'sms') ? activity.activityParams.numberText : (say + activity.activityParams.numberText + ''); } } break; @@ -168,7 +230,7 @@ createRestTransformer({ sessionStorage.hangup = true; break; case 'play': - if (sessionStorage.cpaas_channel == 'call') { + if (sessionStorage.cpaasChannel == 'call') { output.text += activity.activityParams.url ? ('' + activity.activityParams.url + '') : ''; } else { output.text = activity.activityParams.text; @@ -190,16 +252,17 @@ createRestTransformer({ break; case 'sms': sessionStorage.sms = true; - const to = activity.activityParams.to ? activity.activityParams.to : ''; - if (sessionStorage.cpaas_channel == 'call') { - output.data.sms = '' + activity.activityParams.text + ''; + const from = ' from="' + ((activity.activityParams.from != null && activity.activityParams.from != '') ? activity.activityParams.from : '{{To}}') + '"'; + const to = ' to="' + (activity.activityParams.to ? activity.activityParams.to : '') + '"'; + if (sessionStorage.cpaasChannel == 'call') { + output.data.sms = '' + activity.activityParams.text + ''; } else { output.text = activity.activityParams.text; } break; case 'redirect': sessionStorage.redirect = true; - output.text += activity.activityParams.url ? (''+ activity.activityParams.url + '') : ''; + output.text += activity.activityParams.url ? (''+ activity.activityParams.url + '') : ''; break; case 'locale': sessionStorage.language = activity.activityParams.language; @@ -209,7 +272,7 @@ createRestTransformer({ break; } }); - } else if (output.text != null && (sessionStorage.cpaas_channel == 'call')) { + } else if (output.text != null && (sessionStorage.cpaasChannel == 'call')) {             output.text = say + output.text + '';         } return output; @@ -236,15 +299,15 @@ createRestTransformer({ * correct format according to the documentation of the specific Endpoint channel. */ handleExecutionFinished: async ({ processedOutput, outputs, userId, sessionId, endpoint, response }) => { - let url = COGNIGY_BASE_URL + endpoint.URLToken; const sessionStorage = await getSessionStorage(userId, sessionId); + const url = HTTPS + sessionStorage.urlbase + '/' + endpoint.URLToken; let cpaasResponse = 'default'; - switch (sessionStorage.cpaas_channel) { + switch (sessionStorage.cpaasChannel) { case 'call': cpaasResponse = getCPaaSCallCmd(sessionStorage, url, outputs); break; case 'sms': - cpaasResponse = getCPaaSSmsCmd(sessionStorage, url, outputs); + cpaasResponse = getCPaaSSmsCmd(sessionStorage, url, sessionId, outputs); break; default: break; @@ -277,16 +340,18 @@ const getCPaaSCallCmd = (sessionStorage, url, outputs) => { let smsCmds = sms ? outputs.map((t) => {return t.data.sms}).join('\n') : ''; let prompt = outputs.map((t) => {return t.text}).join('\n'); let numDigits = 'numDigits="' + numberOfDigits + '"'; - let gather = ''+prompt+''; + let gather = ''+prompt+'' + + + '' + url + REDIRECT_PARAMS + ''; let cpaasResponse = '' + (record) + (smsCmds) + (ctrlcmd ? prompt : gather) + ''; return (cpaasResponse); }; -const getCPaaSSmsCmd = (sessionStorage, url, outputs) => { +const getCPaaSSmsCmd = (sessionStorage, url, sessionId, outputs) => { let text = outputs.map((t) => {return t.text}).join('\n'); let From = sessionStorage.To; let To = sessionStorage.From; let FromTo = ' From="'+From+'" To="'+To+'"'; - let cpaasResponse = ''+text+''; return (cpaasResponse); } \ No newline at end of file