import { AmiClient, type AmiMessage } from './ami-client'; import type { OriginateParams, PjsipContactSummary, PjsipEndpointSummary, QueuePauseParams, QueueStatusSummary, TelephonyProvider, } from './telephony-provider.interface'; export interface AsteriskTelephonyProviderOptions { host: string; amiPort: number; amiUsername: string; amiSecret: string; reconnect?: boolean; } export class AsteriskTelephonyProvider implements TelephonyProvider { private readonly client: AmiClient; constructor(options: AsteriskTelephonyProviderOptions) { this.client = new AmiClient({ host: options.host, port: options.amiPort, username: options.amiUsername, secret: options.amiSecret, reconnect: options.reconnect ?? true, }); } connect(): Promise { return this.client.connect(); } disconnect(): void { this.client.disconnect(); } isConnected(): boolean { return this.client.isConnected(); } async originate(params: OriginateParams): Promise { const action: AmiMessage = { Action: 'Originate', Channel: params.channel, Async: params.async === false ? 'false' : 'true', Timeout: String(params.timeoutMs ?? 30_000), }; if (params.context) action.Context = params.context; if (params.exten) action.Exten = params.exten; if (params.priority !== undefined) action.Priority = String(params.priority); if (params.application) action.Application = params.application; if (params.data) action.Data = params.data; if (params.callerId) action.CallerID = params.callerId; if (params.variables) { // AMI aceita múltiplos headers "Variable" — usamos um por par k=v. Object.entries(params.variables).forEach(([k, v], i) => { action[`Variable${i > 0 ? `-${i}` : ''}`] = `${k}=${v}`; }); } const response = await this.client.sendAction(action); if ((response.Response ?? '').toLowerCase() !== 'success') { throw new Error(`Originate falhou: ${response.Message ?? 'motivo desconhecido'}`); } return response; } async hangup(channel: string, cause?: number): Promise { const action: AmiMessage = { Action: 'Hangup', Channel: channel }; if (cause !== undefined) action.Cause = String(cause); const response = await this.client.sendAction(action); if ((response.Response ?? '').toLowerCase() !== 'success') { throw new Error(`Hangup falhou: ${response.Message ?? 'motivo desconhecido'}`); } } async queuePause(params: QueuePauseParams): Promise { const action: AmiMessage = { Action: 'QueuePause', Interface: params.interface, Paused: params.paused ? 'true' : 'false', }; if (params.queue) action.Queue = params.queue; if (params.reason) action.Reason = params.reason; const response = await this.client.sendAction(action); if ((response.Response ?? '').toLowerCase() !== 'success') { throw new Error(`QueuePause falhou: ${response.Message ?? 'motivo desconhecido'}`); } } async queueAdd(queue: string, iface: string, opts?: { penalty?: number; memberName?: string }): Promise { const action: AmiMessage = { Action: 'QueueAdd', Queue: queue, Interface: iface }; if (opts?.penalty !== undefined) action.Penalty = String(opts.penalty); if (opts?.memberName) action.MemberName = opts.memberName; const response = await this.client.sendAction(action); if ((response.Response ?? '').toLowerCase() !== 'success') { throw new Error(`QueueAdd falhou: ${response.Message ?? 'motivo desconhecido'}`); } } async queueRemove(queue: string, iface: string): Promise { const response = await this.client.sendAction({ Action: 'QueueRemove', Queue: queue, Interface: iface }); if ((response.Response ?? '').toLowerCase() !== 'success') { throw new Error(`QueueRemove falhou: ${response.Message ?? 'motivo desconhecido'}`); } } async queueStatus(queue?: string): Promise { const action: AmiMessage = { Action: 'QueueStatus' }; if (queue) action.Queue = queue; const events = await this.client.sendActionCollectEvents(action, 'StatusComplete'); const byQueue = new Map(); for (const evt of events) { const qName = evt.Queue; if (!qName) continue; if (!byQueue.has(qName)) { byQueue.set(qName, { queue: qName, members: [], entries: [] }); } const summary = byQueue.get(qName)!; if (evt.Event === 'QueueParams') summary.calls = evt.Calls; else if (evt.Event === 'QueueMember') summary.members.push(evt); else if (evt.Event === 'QueueEntry') summary.entries.push(evt); } return [...byQueue.values()]; } async extensionState(exten: string, context: string): Promise { const response = await this.client.sendAction({ Action: 'ExtensionState', Exten: exten, Context: context }); if ((response.Response ?? '').toLowerCase() !== 'success') { throw new Error(`ExtensionState falhou: ${response.Message ?? 'motivo desconhecido'}`); } return response; } async deviceState(device: string): Promise { const response = await this.client.sendAction({ Action: 'DeviceState', Device: device }); return response.State ?? 'UNKNOWN'; } async pjsipShowEndpoints(): Promise { const events = await this.client.sendActionCollectEvents({ Action: 'PJSIPShowEndpoints' }, 'EndpointListComplete'); return events .filter((e) => e.Event === 'EndpointList') .map((e) => ({ objectName: e.ObjectName ?? '', deviceState: e.DeviceState, contacts: e.Contacts })); } async pjsipShowContacts(): Promise { const events = await this.client.sendActionCollectEvents({ Action: 'PJSIPShowContacts' }, 'ContactListComplete'); return events .filter((e) => e.Event === 'ContactList') .map((e) => ({ uri: e.URI, status: e.Status, endpointName: e.EndpointName })); } async reload(module?: string): Promise { const action: AmiMessage = { Action: 'Reload' }; if (module) action.Module = module; const response = await this.client.sendAction(action); if ((response.Response ?? '').toLowerCase() !== 'success') { throw new Error(`Reload falhou: ${response.Message ?? 'motivo desconhecido'}`); } } runCommand(command: string): Promise { return this.client.runCommand(command); } onEvent(handler: (event: AmiMessage) => void): void { this.client.on('event', handler); } offEvent(handler: (event: AmiMessage) => void): void { this.client.off('event', handler); } }