feat(call, voicemail, ivr): add voicemail and IVR call flows with DTMF handling, prompt playback, recording, and dashboard management

This commit is contained in:
2026-04-10 08:54:46 +00:00
parent 6ecd3f434c
commit e6bd64a534
25 changed files with 3892 additions and 10 deletions

View File

@@ -13,6 +13,7 @@ import https from 'node:https';
import { WebSocketServer, WebSocket } from 'ws';
import type { CallManager } from './call/index.ts';
import { handleWebRtcSignaling } from './webrtcbridge.ts';
import type { VoiceboxManager } from './voicebox.ts';
const CONFIG_PATH = path.join(process.cwd(), '.nogit', 'config.json');
@@ -84,6 +85,7 @@ async function handleRequest(
onHangupCall: (callId: string) => boolean,
onConfigSaved?: () => void,
callManager?: CallManager,
voiceboxManager?: VoiceboxManager,
): Promise<void> {
const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
const method = req.method || 'GET';
@@ -242,6 +244,8 @@ async function handleRequest(
}
}
if (updates.contacts !== undefined) cfg.contacts = updates.contacts;
if (updates.voiceboxes !== undefined) cfg.voiceboxes = updates.voiceboxes;
if (updates.ivr !== undefined) cfg.ivr = updates.ivr;
fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2) + '\n');
log('[config] updated config.json');
@@ -252,6 +256,50 @@ async function handleRequest(
}
}
// API: voicemail - list messages.
const vmListMatch = url.pathname.match(/^\/api\/voicemail\/([^/]+)$/);
if (vmListMatch && method === 'GET' && voiceboxManager) {
const boxId = vmListMatch[1];
return sendJson(res, { ok: true, messages: voiceboxManager.getMessages(boxId) });
}
// API: voicemail - unheard count.
const vmUnheardMatch = url.pathname.match(/^\/api\/voicemail\/([^/]+)\/unheard$/);
if (vmUnheardMatch && method === 'GET' && voiceboxManager) {
const boxId = vmUnheardMatch[1];
return sendJson(res, { ok: true, count: voiceboxManager.getUnheardCount(boxId) });
}
// API: voicemail - stream audio.
const vmAudioMatch = url.pathname.match(/^\/api\/voicemail\/([^/]+)\/([^/]+)\/audio$/);
if (vmAudioMatch && method === 'GET' && voiceboxManager) {
const [, boxId, msgId] = vmAudioMatch;
const audioPath = voiceboxManager.getMessageAudioPath(boxId, msgId);
if (!audioPath) return sendJson(res, { ok: false, error: 'not found' }, 404);
const stat = fs.statSync(audioPath);
res.writeHead(200, {
'Content-Type': 'audio/wav',
'Content-Length': stat.size.toString(),
'Accept-Ranges': 'bytes',
});
fs.createReadStream(audioPath).pipe(res);
return;
}
// API: voicemail - mark as heard.
const vmHeardMatch = url.pathname.match(/^\/api\/voicemail\/([^/]+)\/([^/]+)\/heard$/);
if (vmHeardMatch && method === 'POST' && voiceboxManager) {
const [, boxId, msgId] = vmHeardMatch;
return sendJson(res, { ok: voiceboxManager.markHeard(boxId, msgId) });
}
// API: voicemail - delete message.
const vmDeleteMatch = url.pathname.match(/^\/api\/voicemail\/([^/]+)\/([^/]+)$/);
if (vmDeleteMatch && method === 'DELETE' && voiceboxManager) {
const [, boxId, msgId] = vmDeleteMatch;
return sendJson(res, { ok: voiceboxManager.deleteMessage(boxId, msgId) });
}
// Static files.
const file = staticFiles.get(url.pathname);
if (file) {
@@ -288,6 +336,7 @@ export function initWebUi(
onHangupCall: (callId: string) => boolean,
onConfigSaved?: () => void,
callManager?: CallManager,
voiceboxManager?: VoiceboxManager,
): void {
const WEB_PORT = 3060;
@@ -303,12 +352,12 @@ export function initWebUi(
const cert = fs.readFileSync(certPath, 'utf8');
const key = fs.readFileSync(keyPath, 'utf8');
server = https.createServer({ cert, key }, (req, res) =>
handleRequest(req, res, getStatus, log, onStartCall, onHangupCall, onConfigSaved, callManager).catch(() => { res.writeHead(500); res.end(); }),
handleRequest(req, res, getStatus, log, onStartCall, onHangupCall, onConfigSaved, callManager, voiceboxManager).catch(() => { res.writeHead(500); res.end(); }),
);
useTls = true;
} catch {
server = http.createServer((req, res) =>
handleRequest(req, res, getStatus, log, onStartCall, onHangupCall, onConfigSaved, callManager).catch(() => { res.writeHead(500); res.end(); }),
handleRequest(req, res, getStatus, log, onStartCall, onHangupCall, onConfigSaved, callManager, voiceboxManager).catch(() => { res.writeHead(500); res.end(); }),
);
}