150 lines
4.2 KiB
TypeScript
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)`);
|
|
}
|
|
}
|