feat(storage): persist siprouter data in smartdata and smartbucket

This commit is contained in:
2026-05-21 23:35:50 +00:00
parent 04e706715f
commit 3e2fee16c1
14 changed files with 2018 additions and 492 deletions
+21 -76
View File
@@ -11,19 +11,19 @@ import path from 'node:path';
import http from 'node:http';
import https from 'node:https';
import { WebSocketServer, WebSocket } from 'ws';
import { maskConfig, type IAppConfig } from './config.ts';
import type { FaxBoxManager } from './faxbox.ts';
import type { FaxJobManager } from './faxjobs.ts';
import { handleWebRtcSignaling } from './webrtcbridge.ts';
import type { VoiceboxManager } from './voicebox.ts';
const CONFIG_PATH = path.join(process.cwd(), '.nogit', 'config.json');
interface IHandleRequestContext {
getStatus: () => unknown;
getConfig: () => IAppConfig;
updateConfig: (updatesArg: any) => Promise<IAppConfig>;
log: (msg: string) => void;
onStartCall: (number: string, deviceId?: string, providerId?: string) => Promise<{ id: string } | null>;
onHangupCall: (callId: string) => boolean;
onConfigSaved?: () => void | Promise<void>;
faxBoxManager?: FaxBoxManager;
faxJobManager?: FaxJobManager;
voiceboxManager?: VoiceboxManager;
@@ -112,7 +112,7 @@ async function handleRequest(
res: http.ServerResponse,
context: IHandleRequestContext,
): Promise<void> {
const { getStatus, log, onStartCall, onHangupCall, onConfigSaved, faxBoxManager, faxJobManager, voiceboxManager } = context;
const { getStatus, getConfig, updateConfig, log, onStartCall, onHangupCall, faxBoxManager, faxJobManager, voiceboxManager } = context;
const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
const method = req.method || 'GET';
@@ -156,13 +156,16 @@ async function handleRequest(
try {
const body = await readJsonBody(req);
const number = body?.number;
const filePath = body?.filePath;
let filePath = body?.filePath;
if (!number || typeof number !== 'string') {
return sendJson(res, { ok: false, error: 'missing "number" field' }, 400);
}
if (!filePath || typeof filePath !== 'string') {
return sendJson(res, { ok: false, error: 'missing "filePath" field' }, 400);
}
if (faxBoxManager) {
filePath = await faxBoxManager.prepareOutboundFaxFile(filePath);
}
const { sendFax } = await import('./proxybridge.ts');
const callId = await sendFax(number, filePath, body?.providerId);
if (callId) {
@@ -191,7 +194,7 @@ async function handleRequest(
const faxFileMatch = url.pathname.match(/^\/api\/fax\/inboxes\/([^/]+)\/([^/]+)\/file$/);
if (faxFileMatch && method === 'GET' && faxBoxManager) {
const [, boxId, msgId] = faxFileMatch;
const filePath = faxBoxManager.getMessageFilePath(boxId, msgId);
const filePath = await faxBoxManager.getMessageFilePath(boxId, msgId);
if (!filePath) return sendJson(res, { ok: false, error: 'not found' }, 404);
const stat = fs.statSync(filePath);
res.writeHead(200, {
@@ -207,7 +210,7 @@ async function handleRequest(
const faxDeleteMatch = url.pathname.match(/^\/api\/fax\/inboxes\/([^/]+)\/([^/]+)$/);
if (faxDeleteMatch && method === 'DELETE' && faxBoxManager) {
const [, boxId, msgId] = faxDeleteMatch;
return sendJson(res, { ok: faxBoxManager.deleteMessage(boxId, msgId) });
return sendJson(res, { ok: await faxBoxManager.deleteMessage(boxId, msgId) });
}
// API: add a SIP device to a call (mid-call INVITE to desk phone).
@@ -272,10 +275,7 @@ async function handleRequest(
// API: get config (sans passwords).
if (url.pathname === '/api/config' && method === 'GET') {
try {
const raw = fs.readFileSync(CONFIG_PATH, 'utf8');
const cfg = JSON.parse(raw);
const safe = { ...cfg, providers: cfg.providers?.map((p: any) => ({ ...p, password: '••••••' })) };
return sendJson(res, safe);
return sendJson(res, maskConfig(getConfig()));
} catch (e: any) {
return sendJson(res, { ok: false, error: e.message }, 500);
}
@@ -285,65 +285,9 @@ async function handleRequest(
if (url.pathname === '/api/config' && method === 'POST') {
try {
const updates = await readJsonBody(req);
const raw = fs.readFileSync(CONFIG_PATH, 'utf8');
const cfg = JSON.parse(raw);
// Update existing providers.
if (updates.providers) {
for (const up of updates.providers) {
const existing = cfg.providers?.find((p: any) => p.id === up.id);
if (existing) {
if (up.displayName !== undefined) existing.displayName = up.displayName;
if (up.password && up.password !== '••••••') existing.password = up.password;
if (up.domain !== undefined) existing.domain = up.domain;
if (up.outboundProxy !== undefined) existing.outboundProxy = up.outboundProxy;
if (up.username !== undefined) existing.username = up.username;
if (up.registerIntervalSec !== undefined) existing.registerIntervalSec = up.registerIntervalSec;
if (up.codecs !== undefined) existing.codecs = up.codecs;
if (up.quirks !== undefined) existing.quirks = up.quirks;
}
}
}
// Add a new provider.
if (updates.addProvider) {
cfg.providers ??= [];
cfg.providers.push(updates.addProvider);
}
// Remove a provider.
if (updates.removeProvider) {
cfg.providers = (cfg.providers || []).filter((p: any) => p.id !== updates.removeProvider);
// Clean up routing references — remove routes that reference this provider.
if (cfg.routing?.routes) {
cfg.routing.routes = cfg.routing.routes.filter((r: any) =>
r.match?.sourceProvider !== updates.removeProvider &&
r.action?.provider !== updates.removeProvider
);
}
}
if (updates.devices) {
for (const ud of updates.devices) {
const existing = cfg.devices?.find((d: any) => d.id === ud.id);
if (existing && ud.displayName !== undefined) existing.displayName = ud.displayName;
}
}
if (updates.incomingNumbers !== undefined) cfg.incomingNumbers = updates.incomingNumbers;
if (updates.routing) {
if (updates.routing.routes) {
cfg.routing.routes = updates.routing.routes;
}
}
if (updates.contacts !== undefined) cfg.contacts = updates.contacts;
if (updates.faxboxes !== undefined) cfg.faxboxes = updates.faxboxes;
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');
await onConfigSaved?.();
return sendJson(res, { ok: true });
const config = await updateConfig(updates);
log('[config] updated smartdata config');
return sendJson(res, { ok: true, config: maskConfig(config) });
} catch (e: any) {
return sendJson(res, { ok: false, error: e.message }, 400);
}
@@ -367,7 +311,7 @@ async function handleRequest(
const vmAudioMatch = url.pathname.match(/^\/api\/voicemail\/([^/]+)\/([^/]+)\/audio$/);
if (vmAudioMatch && method === 'GET' && voiceboxManager) {
const [, boxId, msgId] = vmAudioMatch;
const audioPath = voiceboxManager.getMessageAudioPath(boxId, msgId);
const audioPath = await voiceboxManager.getMessageAudioPath(boxId, msgId);
if (!audioPath) return sendJson(res, { ok: false, error: 'not found' }, 404);
const stat = fs.statSync(audioPath);
res.writeHead(200, {
@@ -383,14 +327,14 @@ async function handleRequest(
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) });
return sendJson(res, { ok: await 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) });
return sendJson(res, { ok: await voiceboxManager.deleteMessage(boxId, msgId) });
}
// Static files.
@@ -428,10 +372,11 @@ export function initWebUi(
const {
port,
getStatus,
getConfig,
updateConfig,
log,
onStartCall,
onHangupCall,
onConfigSaved,
faxBoxManager,
faxJobManager,
voiceboxManager,
@@ -453,12 +398,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, faxBoxManager, faxJobManager, voiceboxManager }).catch(() => { res.writeHead(500); res.end(); }),
handleRequest(req, res, { getStatus, getConfig, updateConfig, log, onStartCall, onHangupCall, faxBoxManager, faxJobManager, voiceboxManager }).catch(() => { res.writeHead(500); res.end(); }),
);
useTls = true;
} catch {
server = http.createServer((req, res) =>
handleRequest(req, res, { getStatus, log, onStartCall, onHangupCall, onConfigSaved, faxBoxManager, faxJobManager, voiceboxManager }).catch(() => { res.writeHead(500); res.end(); }),
handleRequest(req, res, { getStatus, getConfig, updateConfig, log, onStartCall, onHangupCall, faxBoxManager, faxJobManager, voiceboxManager }).catch(() => { res.writeHead(500); res.end(); }),
);
}