feat(fax): add fax routing, job tracking, inbox management, and T.38/UDPTL media support

This commit is contained in:
2026-04-20 20:43:42 +00:00
parent 3c010a3b1b
commit d2c18a4ebb
27 changed files with 4247 additions and 280 deletions
+56 -1
View File
@@ -1,4 +1,6 @@
import { onProxyEvent } from '../proxybridge.ts';
import { hangupCall, onProxyEvent } from '../proxybridge.ts';
import type { FaxBoxManager } from '../faxbox.ts';
import type { FaxJobManager } from '../faxjobs.ts';
import type { VoiceboxManager } from '../voicebox.ts';
import type { StatusStore } from './status-store.ts';
import type { IProviderMediaInfo, WebRtcLinkManager } from './webrtc-linking.ts';
@@ -6,6 +8,8 @@ import type { IProviderMediaInfo, WebRtcLinkManager } from './webrtc-linking.ts'
export interface IRegisterProxyEventHandlersOptions {
log: (msg: string) => void;
statusStore: StatusStore;
faxBoxManager: FaxBoxManager;
faxJobManager: FaxJobManager;
voiceboxManager: VoiceboxManager;
webRtcLinks: WebRtcLinkManager;
getBrowserDeviceIds: () => string[];
@@ -19,6 +23,8 @@ export function registerProxyEventHandlers(options: IRegisterProxyEventHandlersO
const {
log,
statusStore,
faxBoxManager,
faxJobManager,
voiceboxManager,
webRtcLinks,
getBrowserDeviceIds,
@@ -30,6 +36,7 @@ export function registerProxyEventHandlers(options: IRegisterProxyEventHandlersO
const legMediaDetails = (data: {
codec?: string | null;
mediaProtocol?: string | null;
remoteMedia?: string | null;
rtpPort?: number | null;
}): string => {
@@ -37,6 +44,9 @@ export function registerProxyEventHandlers(options: IRegisterProxyEventHandlersO
if (data.codec) {
parts.push(`codec=${data.codec}`);
}
if (data.mediaProtocol) {
parts.push(`media=${data.mediaProtocol}`);
}
if (data.remoteMedia) {
parts.push(`remote=${data.remoteMedia}`);
}
@@ -91,6 +101,14 @@ export function registerProxyEventHandlers(options: IRegisterProxyEventHandlersO
log(`[call] outbound started: ${data.call_id} -> ${data.number} via ${data.provider_id}`);
statusStore.noteOutboundCallStarted(data);
if (data.ring_browsers === false) {
faxJobManager.noteDialing(data.call_id, data.number, data.provider_id);
}
if (data.ring_browsers === false) {
return;
}
for (const deviceId of getBrowserDeviceIds()) {
sendToBrowserDevice(deviceId, {
type: 'webrtc-incoming',
@@ -110,6 +128,10 @@ export function registerProxyEventHandlers(options: IRegisterProxyEventHandlersO
log(`[call] ${data.call_id} connected`);
}
if (data.media_protocol && data.media_protocol !== 'rtp') {
return;
}
if (!data.provider_media_addr || !data.provider_media_port) {
return;
}
@@ -207,4 +229,37 @@ export function registerProxyEventHandlers(options: IRegisterProxyEventHandlersO
onProxyEvent('voicemail_error', (data) => {
log(`[voicemail] error: ${data.error} call=${data.call_id}`);
});
onProxyEvent('fax_started', (data) => {
faxJobManager.noteStarted(data);
log(`[fax] started: call=${data.call_id} leg=${data.leg_id} ${data.direction}/${data.transport} codec=${data.codec || '?'} file=${data.file_path}`);
});
onProxyEvent('fax_completed', (data) => {
faxJobManager.noteCompleted(data);
log(
`[fax] completed: call=${data.call_id} leg=${data.leg_id} success=${data.success} pagesTx=${data.stats.pages_tx} bitrate=${data.stats.bit_rate} completion=${data.completion_label || data.completion_code || 'unknown'}`,
);
if (data.direction === 'inbound' && data.success && data.fax_box_id) {
faxBoxManager.addMessage(data.fax_box_id, {
callerNumber: data.caller_number,
fileName: data.file_path,
completionCode: data.completion_code,
completionLabel: data.completion_label,
pageCount: data.stats.pages_rx || data.stats.pages_tx,
bitRate: data.stats.bit_rate,
});
}
if (data.direction === 'outbound' || data.fax_box_id) {
void hangupCall(data.call_id);
}
});
onProxyEvent('fax_failed', (data) => {
faxJobManager.noteFailed(data);
log(`[fax] failed: call=${data.call_id} leg=${data.leg_id} error=${data.error}`);
if (data.direction === 'outbound' || data.fax_box_id) {
void hangupCall(data.call_id);
}
});
}