Enables outbound PSTN probes via TwiML webhooks with Webex result cards, status polling, and optional store CDR enrichment.
91 lines
2.8 KiB
JavaScript
91 lines
2.8 KiB
JavaScript
// services/callTest/twiml.js
|
|
// Store entry steps + shared core steps for /calltest Twilio voice webhooks.
|
|
|
|
import {
|
|
buildVoiceWebhookUrl,
|
|
} from './config.js';
|
|
|
|
const CORE_STEPS = new Set(['intro', 'listen', 'outro', 'done']);
|
|
const STORE_ENTRY_STEPS = new Set(['greeting', 'dtmf', 'wait']);
|
|
|
|
function xmlEscape(text) {
|
|
return String(text || '')
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
function redirect(testId, mode, step) {
|
|
const url = xmlEscape(buildVoiceWebhookUrl(testId, mode, step));
|
|
return `<Redirect method="POST">${url}</Redirect>`;
|
|
}
|
|
|
|
/**
|
|
* @param {object} opts
|
|
* @param {'store'|'dial'} opts.mode
|
|
* @param {string} opts.step
|
|
* @param {string} opts.testId
|
|
* @param {object} opts.config
|
|
*/
|
|
export function buildTwiml({ mode, step, testId, config }) {
|
|
const c = config;
|
|
let body = '';
|
|
|
|
if (mode === 'store' && STORE_ENTRY_STEPS.has(step)) {
|
|
switch (step) {
|
|
case 'greeting':
|
|
body = `<Pause length="${c.greetingPauseSec}"/>\n${redirect(testId, mode, 'dtmf')}`;
|
|
break;
|
|
case 'dtmf': {
|
|
const digits = String(c.dtmf || '1').replace(/[^0-9*#w]/gi, '');
|
|
body = `<Play digits="wwww${xmlEscape(digits)}"/>\n${redirect(testId, mode, 'wait')}`;
|
|
break;
|
|
}
|
|
case 'wait':
|
|
body = `<Pause length="${c.answerWaitSec}"/>\n${redirect(testId, mode, 'intro')}`;
|
|
break;
|
|
default:
|
|
body = `<Say>Unknown entry step.</Say>\n<Hangup/>`;
|
|
}
|
|
} else if (CORE_STEPS.has(step)) {
|
|
switch (step) {
|
|
case 'intro':
|
|
body = `<Say voice="Polly.Joanna">${xmlEscape(c.introTts)}</Say>\n${redirect(testId, mode, 'listen')}`;
|
|
break;
|
|
case 'listen':
|
|
body = `<Pause length="${c.listenSec}"/>\n${redirect(testId, mode, 'outro')}`;
|
|
break;
|
|
case 'outro':
|
|
body = `<Say voice="Polly.Joanna">${xmlEscape(c.outroTts)}</Say>\n${redirect(testId, mode, 'done')}`;
|
|
break;
|
|
case 'done':
|
|
body = '<Hangup/>';
|
|
break;
|
|
default:
|
|
body = '<Hangup/>';
|
|
}
|
|
} else if (mode === 'dial' && step === 'intro') {
|
|
body = `<Say voice="Polly.Joanna">${xmlEscape(c.introTts)}</Say>\n${redirect(testId, mode, 'listen')}`;
|
|
} else {
|
|
body = '<Say>Invalid call test step.</Say>\n<Hangup/>';
|
|
}
|
|
|
|
return `<?xml version="1.0" encoding="UTF-8"?>\n<Response>\n${body}\n</Response>`;
|
|
}
|
|
|
|
export function initialStepForMode(mode) {
|
|
return mode === 'store' ? 'greeting' : 'intro';
|
|
}
|
|
|
|
export function isCoreStep(step) {
|
|
return CORE_STEPS.has(step);
|
|
}
|
|
|
|
export function isStoreEntryStep(step) {
|
|
return STORE_ENTRY_STEPS.has(step);
|
|
}
|
|
|
|
export const STORE_ENTRY_STEP_ORDER = ['greeting', 'dtmf', 'wait'];
|
|
export const CORE_STEP_ORDER = ['intro', 'listen', 'outro', 'done'];
|