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
+66 -64
View File
@@ -88,16 +88,12 @@ export class StatusStore {
}
noteDashboardCallStarted(callId: string, number: string, providerId?: string): void {
this.activeCalls.set(callId, {
id: callId,
direction: 'outbound',
callerNumber: null,
calleeNumber: number,
providerUsed: providerId || null,
state: 'setting-up',
startedAt: Date.now(),
legs: new Map(),
});
const call = this.getOrCreateCall(callId, 'outbound');
call.direction = 'outbound';
call.callerNumber = null;
call.calleeNumber = number;
call.providerUsed = providerId || null;
call.state = 'setting-up';
}
noteProviderRegistered(data: IProviderRegisteredEvent): { wasRegistered: boolean } | null {
@@ -126,56 +122,39 @@ export class StatusStore {
}
noteIncomingCall(data: IIncomingCallEvent): void {
this.activeCalls.set(data.call_id, {
id: data.call_id,
direction: 'inbound',
callerNumber: data.from_uri,
calleeNumber: data.to_number,
providerUsed: data.provider_id,
state: 'ringing',
startedAt: Date.now(),
legs: new Map(),
});
}
noteOutboundDeviceCall(data: IOutboundCallEvent): void {
this.activeCalls.set(data.call_id, {
id: data.call_id,
direction: 'outbound',
callerNumber: data.from_device,
calleeNumber: data.to_number,
providerUsed: null,
state: 'setting-up',
startedAt: Date.now(),
legs: new Map(),
});
}
noteOutboundCallStarted(data: IOutboundCallStartedEvent): void {
this.activeCalls.set(data.call_id, {
id: data.call_id,
direction: 'outbound',
callerNumber: null,
calleeNumber: data.number,
providerUsed: data.provider_id,
state: 'setting-up',
startedAt: Date.now(),
legs: new Map(),
});
}
noteCallRinging(data: ICallRingingEvent): void {
const call = this.activeCalls.get(data.call_id);
if (call) {
const call = this.getOrCreateCall(data.call_id, 'inbound');
call.direction = 'inbound';
call.callerNumber = data.from_uri;
call.calleeNumber = data.to_number;
call.providerUsed = data.provider_id;
if (call.state === 'setting-up') {
call.state = 'ringing';
}
}
noteOutboundDeviceCall(data: IOutboundCallEvent): void {
const call = this.getOrCreateCall(data.call_id, 'outbound');
call.direction = 'outbound';
call.callerNumber = data.from_device;
call.calleeNumber = data.to_number;
call.providerUsed = null;
}
noteOutboundCallStarted(data: IOutboundCallStartedEvent): void {
const call = this.getOrCreateCall(data.call_id, 'outbound');
call.direction = 'outbound';
call.callerNumber = call.callerNumber ?? null;
call.calleeNumber = data.number;
call.providerUsed = data.provider_id;
}
noteCallRinging(data: ICallRingingEvent): void {
const call = this.getOrCreateCall(data.call_id);
call.state = 'ringing';
}
noteCallAnswered(data: ICallAnsweredEvent): boolean {
const call = this.activeCalls.get(data.call_id);
if (!call) {
return false;
}
const call = this.getOrCreateCall(data.call_id);
call.state = 'connected';
@@ -186,7 +165,12 @@ export class StatusStore {
}
leg.remoteMedia = `${data.provider_media_addr}:${data.provider_media_port}`;
if (data.sip_pt !== undefined) {
if (data.media_protocol) {
leg.mediaProtocol = data.media_protocol;
}
if (data.media_protocol === 't38-udptl') {
leg.codec = 'T.38';
} else if (data.sip_pt !== undefined) {
leg.codec = CODEC_NAMES[data.sip_pt] || `PT${data.sip_pt}`;
}
break;
@@ -216,6 +200,7 @@ export class StatusStore {
state: leg.state,
codec: leg.codec,
rtpPort: leg.rtpPort,
mediaProtocol: leg.mediaProtocol,
remoteMedia: leg.remoteMedia,
metadata: leg.metadata || {},
})),
@@ -230,10 +215,7 @@ export class StatusStore {
}
noteLegAdded(data: ILegAddedEvent): void {
const call = this.activeCalls.get(data.call_id);
if (!call) {
return;
}
const call = this.getOrCreateCall(data.call_id);
call.legs.set(data.leg_id, {
id: data.leg_id,
@@ -241,6 +223,7 @@ export class StatusStore {
state: data.state,
codec: data.codec ?? null,
rtpPort: data.rtpPort ?? null,
mediaProtocol: data.mediaProtocol ?? null,
remoteMedia: data.remoteMedia ?? null,
metadata: data.metadata || {},
});
@@ -251,10 +234,7 @@ export class StatusStore {
}
noteLegStateChanged(data: ILegStateChangedEvent): void {
const call = this.activeCalls.get(data.call_id);
if (!call) {
return;
}
const call = this.getOrCreateCall(data.call_id);
const existingLeg = call.legs.get(data.leg_id);
if (existingLeg) {
@@ -265,6 +245,9 @@ export class StatusStore {
if (data.rtpPort !== undefined) {
existingLeg.rtpPort = data.rtpPort;
}
if (data.mediaProtocol !== undefined) {
existingLeg.mediaProtocol = data.mediaProtocol;
}
if (data.remoteMedia !== undefined) {
existingLeg.remoteMedia = data.remoteMedia;
}
@@ -280,6 +263,7 @@ export class StatusStore {
state: data.state,
codec: data.codec ?? null,
rtpPort: data.rtpPort ?? null,
mediaProtocol: data.mediaProtocol ?? null,
remoteMedia: data.remoteMedia ?? null,
metadata: data.metadata || {},
});
@@ -323,4 +307,22 @@ export class StatusStore {
}
return 'webrtc';
}
private getOrCreateCall(callId: string, direction: 'inbound' | 'outbound' = 'inbound'): IActiveCall {
let call = this.activeCalls.get(callId);
if (!call) {
call = {
id: callId,
direction,
callerNumber: null,
calleeNumber: null,
providerUsed: null,
state: 'setting-up',
startedAt: Date.now(),
legs: new Map(),
};
this.activeCalls.set(callId, call);
}
return call;
}
}