UNPKG

@ipcom/extended-ami

Version:

Advanced manager for connecting to Asterisk

622 lines (525 loc) 20.6 kB
'use strict'; import { Socket } from 'net'; import { _indexOfArray, _isEmpty, _isFinite, _isNull, _isUndefined, _toNumber, } from './functions.js'; import { IeAmiOptions } from './interfaces/configure.interface.js'; import { I_ActionLogin, I_Request, I_Response, } from './interfaces/actions.interface.js'; import { _AMI_EVENTS, _eAMI_EVENTS, CRLF, DEFAULT_PORT, END, HEARTBEAT_INTERVAL, MAX_RECONNECT_COUNT, RESEND_TIMEOUT, } from './constants.js'; import { eAmiActions } from './e-ami-actions.js'; import { EventEmitter } from 'events'; type Timer = ReturnType<typeof setTimeout>; export * from './typeGuards.js'; export * from './types/events.js' export const eAMI_EVENTS = _eAMI_EVENTS; export const AMI_EVENTS = _AMI_EVENTS; export class eAmi { public debug: boolean; private _host: string; private _port: number; private _userName: string; private _password: string; private _isLoggedIn: boolean; private _emitAllEvents: boolean; private _reconnect: boolean; private _heartbeatOk: boolean = false; private _lastConnectedTime: number = 0; private _maxReconnectCount: number; private _heartbeatInterval: number; private _heartbeatHandler?: Timer = undefined; // ou Timer | null = null; private _resendTimeOut: number; private _successBitsByInterval: number; private _errorBitsByInterval: number; private _countReconnect: number; private _excludeEvents: string[]; private _queueRequest: I_Request[]; public _socketHandler?: Socket = undefined; private _actions: eAmiActions; public events: EventEmitter; private _maxAuthCount: number; private _authCount: number; constructor(allOptions: IeAmiOptions) { const connect = allOptions, options = _isUndefined(connect.additionalOptions) ? {} : connect.additionalOptions; this._host = connect.host; this._port = _isNull(connect.port) ? DEFAULT_PORT : connect.port; this._userName = connect.userName; this._password = connect.password; this._reconnect = options?.reconnect ?? true; this._heartbeatInterval = (options?.heartbeatInterval ?? HEARTBEAT_INTERVAL) * 1000; this._resendTimeOut = (options?.resendTimeOut ?? RESEND_TIMEOUT) * 1000; this._excludeEvents = options?.excludeEvents ?? []; this._emitAllEvents = options?.emitAllEvents ?? false; this.debug = options?.debug ?? false; this._maxReconnectCount = options?.maxReconnectCount ?? MAX_RECONNECT_COUNT; this._countReconnect = 0; this._maxAuthCount = 5; this._authCount = 0; this._successBitsByInterval = 0; this._errorBitsByInterval = 0; this.events = new EventEmitter(); this._queueRequest = []; this._isLoggedIn = false; this._actions = new eAmiActions(this); this.internalListeners(); } private internalListeners() { this.events.on(eAMI_EVENTS.RE_LOGIN, () => { if (this._authCount < this._maxAuthCount) { setTimeout(async () => { this._authCount++; try { await this.login(); } catch (error) { if (this.debug) console.log('re-login', error); } }, 1000); } }); } get excludeEvents(): string[] { return this._excludeEvents; } set excludeEvents(events: string[]) { this._excludeEvents = events; } get isLoggedIn(): boolean { return this._isLoggedIn; } get lastConnectTime(): number { return this._lastConnectedTime; } get actions(): eAmiActions { return this._actions; } get queueRequest(): I_Request[] { return this._queueRequest; } private addSocketListeners(): void { if (this._socketHandler) { this._socketHandler .on('close', () => { if (this.debug) console.log('close AMI connect'); }) .on('end', () => { if (this.debug) console.log('end AMI connect'); }) .on('data', (buffer: Buffer) => this.getData(buffer)); } else { if (this.debug) console.log( 'Socket handler is undefined, cannot add listeners.' ); } } public destroySocket(): void { if (this._socketHandler) { this._socketHandler.destroy(); if (this.debug) console.log(CRLF + 'Socket connection destroyed'); } else { if (this.debug) console.log( 'Socket handler is undefined, cannot destroy socket.' ); } } private addRequest(request: I_Request): void { this.queueRequest.push(request); this.events.emit(eAMI_EVENTS.SEND, request); } private removeRequest(actionID: unknown): boolean { if (_isUndefined(actionID)) return false; const index: number = _indexOfArray(this.queueRequest, actionID); if (index < 0) return false; try { this.queueRequest.splice(index, 1); return true; } catch (error) { if (this.debug) console.log('Error remove request', error); return false; } } public getRequest(actionID: unknown): I_Request | null { const numActionID = _toNumber(actionID); if (_isUndefined(actionID)) return null; if (numActionID !== undefined && isFinite(numActionID)) { actionID = numActionID; } const index: number = _indexOfArray(this.queueRequest, actionID); if (index < 0) return null; return this.queueRequest[index]; } private setRequest(actionID: unknown, newRequest: I_Request): void { let request = this.getRequest(actionID); if (!request) return; request = newRequest; } private keepConnection(): Promise<boolean> { return new Promise((resolve, reject) => { (async () => { clearInterval(this._heartbeatHandler as Timer); try { const response: boolean = await this.actions.Ping(); if (response) { this._heartbeatOk = true; resolve(true); this._successBitsByInterval++; } this._heartbeatHandler = setTimeout(async () => { try { await this.keepConnection(); } catch (error) { console.log('keep timeout error', error); this._errorBitsByInterval++; } }, this._heartbeatInterval); } catch (error) { this._errorBitsByInterval++; console.log('keep connect error', error); reject(error); } })(); }); } private login(): Promise<boolean> { return new Promise((resolve, reject) => { (async () => { try { const loginOptions: I_ActionLogin = { Username: this._userName, Secret: this._password, }; this.events.emit(eAMI_EVENTS.DO_LOGIN, loginOptions); await this.actions.Login(loginOptions); this.events.emit(eAMI_EVENTS.LOGGED_IN); resolve(true); } catch (error0) { this.events.emit( eAMI_EVENTS.ERROR_LOGIN, error0, 'Authorization failed...' ); if (this._authCount < this._maxAuthCount) { setTimeout(() => { this._authCount++; this.events.emit( eAMI_EVENTS.RE_LOGIN, this._authCount ); }, 1000); } else { this.events.emit( eAMI_EVENTS.MAX_AUTH_REACH, this._authCount ); try { await this.reconnect(); } catch (error1) { reject(error1); } } } })(); }); } private logout(): Promise<boolean> { return new Promise((resolve, reject) => { (async () => { try { await this.actions.Logout(); resolve(true); } catch (error) { this.events.emit(eAMI_EVENTS.ERROR_LOGOUT, error); reject('Failed to logout'); } })(); }); } private showSendPackages(): void { setInterval(() => { console.log( 'Keep Connection. success sent - %s, error sent - %s', this._successBitsByInterval, this._errorBitsByInterval ); }, 5000); } public connect(): Promise<this | boolean> { return new Promise((resolve, reject) => { this._socketHandler = new Socket(); this._socketHandler.connect(this._port, this._host); this._socketHandler .on('connect', async () => { this.addSocketListeners(); try { if (this.debug) console.log('connection to the server'); await this.login(); this._isLoggedIn = true; this._lastConnectedTime = new Date().getTime(); if (this.debug) this.showSendPackages(); await this.keepConnection(); this.events.emit(eAMI_EVENTS.CONNECT); resolve(this); } catch (error) { if (this.debug) console.log(error); reject(error); } }) .on('error', (error) => { this.events.emit( eAMI_EVENTS.ERROR_CONNECT, error, 'Error connecting to an asterisk server' ); if (this.debug) console.log( 'Error connecting to an asterisk server', error ); reject(false); }); }); } public reconnect(): Promise<boolean> { if (!this._reconnect) return Promise.resolve(true); if (this._countReconnect < this._maxReconnectCount) this._countReconnect++; else { this.events.emit( eAMI_EVENTS.MAX_RECONNECT_REACH, this._countReconnect ); return Promise.resolve(false); } return new Promise((resolve, reject) => { (async () => { try { this.events.emit(eAMI_EVENTS.DO_RECONNECT); await this.logout(); this.destroySocket(); await this.connect(); this.events.emit(eAMI_EVENTS.RECONNECTED); resolve(true); } catch (error) { this.events.emit( eAMI_EVENTS.ERROR_RECONNECT, error, 'Could not connect to Asterisk...' ); reject('Could not connect to Asterisk...'); } })(); }); } public action<T extends I_Request, R>(request: T): Promise<R> { return new Promise((resolve, reject) => { let writed: boolean = false; let message = ''; for (const key in request) { if (key === 'ActionID') continue; message += key + ': ' + request[key] + CRLF; } if (_isUndefined(request['ActionID'])) request['ActionID'] = new Date().getTime(); const actionID = request['ActionID']; message += 'ActionID: ' + actionID + CRLF + CRLF; const _request = this.getRequest(actionID); // handlers for resolve if (_request !== null && _request !== undefined) { this.events.once(`Action_${actionID}`, (response: R) => { _request.Completed = true; if (this.debug) console.log( 'response', _request.ActionID, _request.Action ); resolve(response); }); } else { this.events.once(`Action_${actionID}`, (response: R) => { resolve(response); }); } const numActionID = _toNumber(actionID); if (numActionID !== undefined && !_isFinite(numActionID)) { this.events.once(String(actionID), (response: R) => { if (_request !== null && _request !== undefined) { _request.Completed = true; } resolve(response); }); } this.addRequest(request); if (_request) { const actionIDNumber = _toNumber(request['ActionID']); if ( typeof actionIDNumber === 'number' && _isFinite(actionIDNumber) ) { _request.ActionID = actionIDNumber; } else if ( typeof request['ActionID'] === 'string' || typeof request['ActionID'] === 'number' ) { _request.ActionID = request['ActionID']; } else { _request.ActionID = undefined; } _request.Completed = true; _request.timeOutHandler = setTimeout(async () => { if (!writed) { reject('Timeout write to socket...'); return; } if (_request.Completed === true) { try { await this.action(request); } catch (error) { if (this.debug) console.log( 'Error resend action', _request.Action, error ); reject( 'Error resend action' + _request.Action + error ); } this._errorBitsByInterval++; if (this.debug) console.log( 'resend ActionID_' + actionID, _request.Action ); return; } clearTimeout(_request.timeOutHandler as Timer); this.removeRequest(actionID); this.events.removeAllListeners(String(actionID)); this.events.removeAllListeners(`Action_${actionID}`); if (this.debug) console.log('complete ' + actionID, _request.Action); }, 3000); } const write = this._socketHandler?.write(message, () => { writed = true; }); if (write === false) { if (this.debug) console.log('Data in the sending queue'); reject('Data in the sending queue'); } }); } private getData(buffer: Buffer): I_Response { let dataStr: string = buffer.toString(), iDelim: number, typeResponse: string = '', dataArray: string[] = [], keyValueArray: string[] = [], key: string = '', value: string | number | null = null, dataObject: I_Response = {}; if (dataStr.startsWith('Asterisk Call Manager')) { dataStr = dataStr.substring(dataStr.indexOf(CRLF) + 2); } while ((iDelim = dataStr.indexOf(END)) >= 0) { dataArray = dataStr.substring(0, iDelim + 2).split(CRLF); dataStr = dataStr.substring(iDelim + 4); dataObject = {}; typeResponse = ''; keyValueArray = []; for (let index = 0; index < dataArray.length; index++) { if (dataArray[index].indexOf(': ') < 0) continue; keyValueArray = dataArray[index].split(': ', 2); key = keyValueArray[0].replace("'", ''); value = keyValueArray[1]; typeResponse = index === 0 ? key.toLowerCase() : typeResponse; if (key === 'ActionID') { const actionIDNumber = _toNumber(value); dataObject[key] = actionIDNumber !== undefined && _isFinite(actionIDNumber) ? actionIDNumber : value; continue; } const valueNumber = _toNumber(value); if (valueNumber !== undefined && _isFinite(valueNumber)) { value = valueNumber; } else if (value && value.indexOf('unknown') >= 0) { value = null; } else if (_isEmpty(value)) { value = null; } else if ( value && value.toLowerCase().indexOf('s') === 0 && value.length === 1 ) { value = null; } dataObject[key] = value ?? undefined; } const request = this.getRequest(dataObject.ActionID); dataObject.Request = request !== null ? request : undefined; const actionIDNumber = _toNumber(dataObject.ActionID); if (actionIDNumber !== undefined && _isFinite(actionIDNumber)) { this.events.emit('Action_' + actionIDNumber, dataObject); } else if (typeof dataObject.ActionID === 'string') { this.events.emit(dataObject.ActionID, dataObject); } switch (typeResponse) { case 'response': if (this.debug) console.log( eAMI_EVENTS.RESPONSE, CRLF, dataObject, CRLF ); this.events.emit(eAMI_EVENTS.RESPONSE, dataObject); break; case 'event': if (this.debug) console.log(eAMI_EVENTS.EVENTS, CRLF, dataObject, CRLF); if ( dataObject.Event !== undefined && dataObject.Event !== null && dataObject.Event !== '' && this.excludeEvents.indexOf(dataObject.Event) < 0 ) { if (this._emitAllEvents) { this.events.emit(eAMI_EVENTS.EVENTS, dataObject); } this.events.emit(dataObject.Event, dataObject); } break; default: break; } } return dataObject; } }