Map portal call events into CRM logs, store the API token for OutgoingCall, and surface a dedicated settings tab for wiring the webhook URL. Co-authored-by: Cursor <cursoragent@cursor.com>
127 lines
3.9 KiB
TypeScript
127 lines
3.9 KiB
TypeScript
import { Injectable, Logger } from '@nestjs/common';
|
|
|
|
const BASE_URL = 'https://coreapi.daftareshoma.com/api/Customize';
|
|
const MAX_AUDIO_BYTES = 8 * 1024 * 1024;
|
|
|
|
export type DaftaresomaOutgoingCallInput = {
|
|
fromNumber: string;
|
|
toNumber: string;
|
|
callerExtension: string;
|
|
};
|
|
|
|
export type DaftaresomaCallSearchRow = {
|
|
id?: string;
|
|
fromNumber?: string;
|
|
toNumber?: string;
|
|
recordingId?: string | null;
|
|
direction?: string;
|
|
reportDispositionType?: string;
|
|
startTime?: string;
|
|
endTime?: string;
|
|
duration?: string;
|
|
extension?: string;
|
|
};
|
|
|
|
@Injectable()
|
|
export class DaftaresomaClient {
|
|
private readonly logger = new Logger(DaftaresomaClient.name);
|
|
|
|
async outgoingCall(
|
|
apiToken: string,
|
|
input: DaftaresomaOutgoingCallInput,
|
|
): Promise<void> {
|
|
const res = await fetch(`${BASE_URL}/OutgoingCall`, {
|
|
method: 'POST',
|
|
headers: {
|
|
Authorization: `Bearer ${apiToken}`,
|
|
'Content-Type': 'application/json',
|
|
Accept: 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
from_number: input.fromNumber,
|
|
to_number: input.toNumber,
|
|
caller_extension: input.callerExtension,
|
|
}),
|
|
signal: AbortSignal.timeout(15000),
|
|
});
|
|
if (!res.ok) {
|
|
const text = await res.text().catch(() => '');
|
|
this.logger.warn(
|
|
`OutgoingCall failed (${res.status}): ${text.slice(0, 300)}`,
|
|
);
|
|
throw new Error(
|
|
text.trim()
|
|
? `تماس خروجی دفتر شما ناموفق بود (${res.status}): ${text.slice(0, 200)}`
|
|
: `تماس خروجی دفتر شما ناموفق بود (${res.status})`,
|
|
);
|
|
}
|
|
}
|
|
|
|
async findCallById(
|
|
apiToken: string,
|
|
callId: string,
|
|
): Promise<DaftaresomaCallSearchRow | null> {
|
|
const res = await fetch(`${BASE_URL}/CustomerCallSearch`, {
|
|
method: 'POST',
|
|
headers: {
|
|
Authorization: `Bearer ${apiToken}`,
|
|
'Content-Type': 'application/json',
|
|
Accept: 'application/json',
|
|
},
|
|
body: JSON.stringify({ id: callId, limit: 1, pagination: 1 }),
|
|
signal: AbortSignal.timeout(20000),
|
|
});
|
|
if (!res.ok) {
|
|
const text = await res.text().catch(() => '');
|
|
this.logger.warn(
|
|
`CustomerCallSearch failed (${res.status}): ${text.slice(0, 200)}`,
|
|
);
|
|
return null;
|
|
}
|
|
const json = (await res.json()) as {
|
|
data?: DaftaresomaCallSearchRow[];
|
|
};
|
|
const row = Array.isArray(json.data) ? json.data[0] : null;
|
|
return row ?? null;
|
|
}
|
|
|
|
async downloadRecording(
|
|
apiToken: string,
|
|
recordingId: string,
|
|
): Promise<{ buffer: ArrayBuffer; contentType: string; filename: string }> {
|
|
const url = new URL(`${BASE_URL}/GetCallRecording`);
|
|
url.searchParams.set('recordingId', recordingId);
|
|
const res = await fetch(url.toString(), {
|
|
method: 'GET',
|
|
headers: {
|
|
Authorization: `Bearer ${apiToken}`,
|
|
Accept: '*/*',
|
|
'User-Agent': 'MontactCRM-VoIP/1.0',
|
|
},
|
|
signal: AbortSignal.timeout(60000),
|
|
redirect: 'follow',
|
|
});
|
|
if (!res.ok) {
|
|
const text = await res.text().catch(() => '');
|
|
throw new Error(
|
|
`دانلود ضبط دفتر شما ناموفق بود (${res.status})${text ? `: ${text.slice(0, 160)}` : ''}`,
|
|
);
|
|
}
|
|
const contentLength = Number(res.headers.get('content-length') || 0);
|
|
if (contentLength > MAX_AUDIO_BYTES) {
|
|
throw new Error('حجم فایل ضبط بیش از حد مجاز است');
|
|
}
|
|
const buffer = await res.arrayBuffer();
|
|
if (buffer.byteLength > MAX_AUDIO_BYTES) {
|
|
throw new Error('حجم فایل ضبط بیش از حد مجاز است');
|
|
}
|
|
if (!buffer.byteLength) {
|
|
throw new Error('فایل ضبط خالی است');
|
|
}
|
|
const contentType =
|
|
res.headers.get('content-type')?.split(';')[0]?.trim() || 'audio/wav';
|
|
const filename = `daftareshoma-${recordingId}.wav`;
|
|
return { buffer, contentType, filename };
|
|
}
|
|
}
|