Enables outbound PSTN probes via TwiML webhooks with Webex result cards, status polling, and optional store CDR enrichment.
89 lines
2.5 KiB
JavaScript
89 lines
2.5 KiB
JavaScript
// routes/twilioCallTest.js
|
|
// Twilio voice + status webhooks for /calltest.
|
|
|
|
import express from 'express';
|
|
|
|
import { validateTwilioSignature } from '../integrations/twilio/client.js';
|
|
import { logger } from '../utils/logger.js';
|
|
import {
|
|
getWebhookBaseUrl,
|
|
resolveCallTestWebhookUrl,
|
|
} from '../services/callTest/config.js';
|
|
import {
|
|
handleStatusCallback,
|
|
handleVoiceWebhook,
|
|
isCallTestConfigured,
|
|
} from '../services/callTest/callTestService.js';
|
|
|
|
const router = express.Router();
|
|
|
|
function verifyTwilio(req, res, next) {
|
|
if (!isCallTestConfigured()) {
|
|
return res.status(503).send('Call test not configured');
|
|
}
|
|
|
|
const signature = req.get('X-Twilio-Signature') || req.get('x-twilio-signature');
|
|
const url = resolveCallTestWebhookUrl(req);
|
|
const params = req.body || {};
|
|
|
|
if (!url) {
|
|
return next();
|
|
}
|
|
|
|
try {
|
|
if (!validateTwilioSignature(signature, url, params)) {
|
|
logger(
|
|
'calltest:webhook',
|
|
`Invalid Twilio signature for ${req.originalUrl} ` +
|
|
`(validated as ${url}, base=${getWebhookBaseUrl()}, bodyKeys=${Object.keys(params).length})`,
|
|
'warn',
|
|
);
|
|
return res.status(403).send('Forbidden');
|
|
}
|
|
} catch (err) {
|
|
logger('calltest:webhook', `Signature check error: ${err.message}`, 'error');
|
|
return res.status(503).send('Not configured');
|
|
}
|
|
|
|
return next();
|
|
}
|
|
|
|
router.post('/twilio/calltest/voice', verifyTwilio, (req, res) => {
|
|
const testId = req.query.testId;
|
|
const mode = req.query.mode === 'store' ? 'store' : 'dial';
|
|
const step = String(req.query.step || '').trim();
|
|
|
|
if (!testId || !step) {
|
|
return res.status(400).type('text/xml').send(
|
|
'<?xml version="1.0" encoding="UTF-8"?><Response><Say>Missing test parameters.</Say><Hangup/></Response>',
|
|
);
|
|
}
|
|
|
|
logger('calltest:webhook', `Voice testId=${testId} mode=${mode} step=${step}`, 'info');
|
|
|
|
const twiml = handleVoiceWebhook(testId, mode, step);
|
|
res.type('text/xml').send(twiml);
|
|
});
|
|
|
|
router.post('/twilio/calltest/status', verifyTwilio, async (req, res) => {
|
|
const testId = req.query.testId;
|
|
if (!testId) {
|
|
return res.status(400).send('Missing testId');
|
|
}
|
|
|
|
logger(
|
|
'calltest:webhook',
|
|
`Status testId=${testId} CallStatus=${req.body?.CallStatus} CallSid=${req.body?.CallSid}`,
|
|
'info',
|
|
);
|
|
|
|
try {
|
|
await handleStatusCallback(testId, req.body || {});
|
|
res.status(200).send('OK');
|
|
} catch (err) {
|
|
logger('calltest:webhook', `Status handler error: ${err.message}`, 'error');
|
|
res.status(500).send('Error');
|
|
}
|
|
});
|
|
|
|
export default router;
|