Files
siprouter/ts/faxjobs.ts
T

150 lines
4.2 KiB
TypeScript

import fs from 'node:fs';
import path from 'node:path';
import type { SiprouterStorage } from './storage.ts';
import type {
IFaxCompletedEvent,
IFaxFailedEvent,
IFaxStartedEvent,
} from './shared/proxy-events.ts';
export interface IFaxJob {
id: string;
callId: string;
number?: string;
providerId?: string;
direction: 'outbound' | 'inbound';
status: 'dialing' | 'started' | 'completed' | 'failed';
transport?: 'audio' | 't38';
filePath?: string;
objectKey?: string;
codec?: string;
remoteMedia?: string;
success?: boolean;
completionCode?: number | null;
completionLabel?: string | null;
error?: string;
stats?: IFaxCompletedEvent['stats'];
createdAt: number;
updatedAt: number;
}
export class FaxJobManager {
private jobs: IFaxJob[] = [];
private readonly log: (msg: string) => void;
private readonly storage: SiprouterStorage;
constructor(log: (msg: string) => void, storageArg: SiprouterStorage) {
this.log = log;
this.storage = storageArg;
}
async init(): Promise<void> {
this.jobs = await this.storage.getFaxJobs();
}
async noteDialing(callId: string, number: string, providerId: string): Promise<void> {
const jobs = this.jobs;
const now = Date.now();
const existing = jobs.find((job) => job.callId === callId);
if (existing) {
existing.number = number;
existing.providerId = providerId;
existing.updatedAt = now;
} else {
jobs.unshift({
id: callId,
callId,
number,
providerId,
direction: 'outbound',
status: 'dialing',
createdAt: now,
updatedAt: now,
});
}
await this.writeJobs();
}
async noteStarted(event: IFaxStartedEvent): Promise<void> {
const now = Date.now();
const job = this.getOrCreateJob(event.call_id, event.direction, now);
job.status = 'started';
job.transport = event.transport;
job.filePath = event.file_path;
await this.ensureOutboundFileObject(job, event.file_path);
job.codec = event.codec;
job.remoteMedia = event.remote_media;
job.updatedAt = now;
await this.writeJobs();
}
async noteCompleted(event: IFaxCompletedEvent): Promise<void> {
const now = Date.now();
const job = this.getOrCreateJob(event.call_id, event.direction, now);
job.status = 'completed';
job.transport = event.transport;
job.filePath = event.file_path;
await this.ensureOutboundFileObject(job, event.file_path);
job.codec = event.codec;
job.success = event.success;
job.completionCode = event.completion_code ?? null;
job.completionLabel = event.completion_label ?? null;
job.stats = event.stats;
job.updatedAt = now;
await this.writeJobs();
}
async noteFailed(event: IFaxFailedEvent): Promise<void> {
const now = Date.now();
const job = this.getOrCreateJob(event.call_id, event.direction, now);
job.status = 'failed';
job.transport = event.transport;
job.filePath = event.file_path;
await this.ensureOutboundFileObject(job, event.file_path);
job.error = event.error;
job.success = false;
job.updatedAt = now;
await this.writeJobs();
}
getJobs(): IFaxJob[] {
return [...this.jobs];
}
private getOrCreateJob(
callId: string,
direction: 'outbound' | 'inbound',
now: number,
): IFaxJob {
let job = this.jobs.find((entry) => entry.callId === callId);
if (!job) {
job = {
id: callId,
callId,
direction,
status: 'dialing',
createdAt: now,
updatedAt: now,
};
this.jobs.unshift(job);
}
return job;
}
private async ensureOutboundFileObject(jobArg: IFaxJob, filePathArg: string | undefined): Promise<void> {
if (jobArg.direction !== 'outbound' || jobArg.objectKey || !filePathArg) return;
const localPath = path.isAbsolute(filePathArg) ? filePathArg : path.join(process.cwd(), filePathArg);
if (!fs.existsSync(localPath)) return;
const extension = path.extname(localPath) || '.tif';
jobArg.objectKey = await this.storage.putFileObject(`fax/outbound/${jobArg.callId}${extension}`, localPath);
}
private async writeJobs(): Promise<void> {
await this.storage.writeFaxJobs(this.jobs);
this.log(`[fax] persisted ${this.jobs.length} job(s)`);
}
}