154 lines
4.0 KiB
TypeScript
154 lines
4.0 KiB
TypeScript
|
|
import fs from 'node:fs';
|
||
|
|
import path from 'node:path';
|
||
|
|
|
||
|
|
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;
|
||
|
|
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 readonly basePath: string;
|
||
|
|
private readonly jobsPath: string;
|
||
|
|
private readonly log: (msg: string) => void;
|
||
|
|
|
||
|
|
constructor(log: (msg: string) => void) {
|
||
|
|
this.basePath = path.join(process.cwd(), '.nogit', 'fax');
|
||
|
|
this.jobsPath = path.join(this.basePath, 'jobs.json');
|
||
|
|
this.log = log;
|
||
|
|
}
|
||
|
|
|
||
|
|
init(): void {
|
||
|
|
fs.mkdirSync(this.basePath, { recursive: true });
|
||
|
|
if (!fs.existsSync(this.jobsPath)) {
|
||
|
|
this.writeJobs([]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
noteDialing(callId: string, number: string, providerId: string): void {
|
||
|
|
const jobs = this.loadJobs();
|
||
|
|
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,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
this.writeJobs(jobs);
|
||
|
|
}
|
||
|
|
|
||
|
|
noteStarted(event: IFaxStartedEvent): void {
|
||
|
|
const jobs = this.loadJobs();
|
||
|
|
const now = Date.now();
|
||
|
|
const job = this.getOrCreateJob(jobs, event.call_id, event.direction, now);
|
||
|
|
job.status = 'started';
|
||
|
|
job.transport = event.transport;
|
||
|
|
job.filePath = event.file_path;
|
||
|
|
job.codec = event.codec;
|
||
|
|
job.remoteMedia = event.remote_media;
|
||
|
|
job.updatedAt = now;
|
||
|
|
this.writeJobs(jobs);
|
||
|
|
}
|
||
|
|
|
||
|
|
noteCompleted(event: IFaxCompletedEvent): void {
|
||
|
|
const jobs = this.loadJobs();
|
||
|
|
const now = Date.now();
|
||
|
|
const job = this.getOrCreateJob(jobs, event.call_id, event.direction, now);
|
||
|
|
job.status = 'completed';
|
||
|
|
job.transport = event.transport;
|
||
|
|
job.filePath = 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;
|
||
|
|
this.writeJobs(jobs);
|
||
|
|
}
|
||
|
|
|
||
|
|
noteFailed(event: IFaxFailedEvent): void {
|
||
|
|
const jobs = this.loadJobs();
|
||
|
|
const now = Date.now();
|
||
|
|
const job = this.getOrCreateJob(jobs, event.call_id, event.direction, now);
|
||
|
|
job.status = 'failed';
|
||
|
|
job.transport = event.transport;
|
||
|
|
job.filePath = event.file_path;
|
||
|
|
job.error = event.error;
|
||
|
|
job.success = false;
|
||
|
|
job.updatedAt = now;
|
||
|
|
this.writeJobs(jobs);
|
||
|
|
}
|
||
|
|
|
||
|
|
getJobs(): IFaxJob[] {
|
||
|
|
return this.loadJobs();
|
||
|
|
}
|
||
|
|
|
||
|
|
private getOrCreateJob(
|
||
|
|
jobs: IFaxJob[],
|
||
|
|
callId: string,
|
||
|
|
direction: 'outbound' | 'inbound',
|
||
|
|
now: number,
|
||
|
|
): IFaxJob {
|
||
|
|
let job = jobs.find((entry) => entry.callId === callId);
|
||
|
|
if (!job) {
|
||
|
|
job = {
|
||
|
|
id: callId,
|
||
|
|
callId,
|
||
|
|
direction,
|
||
|
|
status: 'dialing',
|
||
|
|
createdAt: now,
|
||
|
|
updatedAt: now,
|
||
|
|
};
|
||
|
|
jobs.unshift(job);
|
||
|
|
}
|
||
|
|
return job;
|
||
|
|
}
|
||
|
|
|
||
|
|
private loadJobs(): IFaxJob[] {
|
||
|
|
try {
|
||
|
|
const content = fs.readFileSync(this.jobsPath, 'utf8');
|
||
|
|
const parsed = JSON.parse(content);
|
||
|
|
return Array.isArray(parsed) ? parsed as IFaxJob[] : [];
|
||
|
|
} catch {
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private writeJobs(jobs: IFaxJob[]): void {
|
||
|
|
fs.mkdirSync(this.basePath, { recursive: true });
|
||
|
|
fs.writeFileSync(this.jobsPath, JSON.stringify(jobs, null, 2));
|
||
|
|
this.log(`[fax] persisted ${jobs.length} job(s)`);
|
||
|
|
}
|
||
|
|
}
|