collabSupport/services/callTest/twiml.js
Joseph McQueen 7aa8c37d1d Add Twilio /calltest for store and direct-dial voice path testing.
Enables outbound PSTN probes via TwiML webhooks with Webex result cards, status polling, and optional store CDR enrichment.
2026-07-23 17:59:10 -04:00

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, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
}
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'];