UNPKG

asterisk-ami-client

Version:
388 lines (350 loc) 11.4 kB
/** * Developer: BelirafoN * Date: 27.04.2016 * Time: 15:37 */ "use strict"; const EventEmitter = require('events').EventEmitter; const amiConnector = require('asterisk-ami-connector'); const debugLog = require('debug')('AmiClient'); const debugError = require('debug')('AmiClient:error'); /** * AmiClient class */ class AmiClient extends EventEmitter{ /** * Constructor */ constructor(options){ super(); Object.assign(this, { _specPrefix: '--spec_', _connector: null, _kaTimer: null, _kaActionId: null, _options: Object.assign({ reconnect: false, maxAttemptsCount: 30, attemptsDelay: 1000, keepAlive: false, keepAliveDelay: 1000, emitEventsByTypes: true, eventTypeToLowerCase: false, emitResponsesById: true, dontDeleteSpecActionId: false, addTime: false, eventFilter: null }, options || {}), _connection: null, _lastAction: null, _credentials: {user: null, secret: null}, _connectionOptions: {}, _userDisconnect: false, _prEmitter: new EventEmitter(), _prPendingActions: {} }); this._prepareOptions(); this._connector = amiConnector({ reconnect: this._options.reconnect, maxAttemptsCount: this._options.maxAttemptsCount, attemptsDelay: this._options.attemptsDelay }); this.on('disconnect', () => { Object.keys(this._prPendingActions).forEach(actionId => { this._prEmitter.emit(`disconnect_${actionId}`); debugLog(`disconnect_${actionId}`); }, this); }); } /** * * @param user * @param secret * @param options * @returns {Promise} */ connect(user, secret, options){ this._credentials = {user, secret}; this._connectionOptions = options || {}; return this._connector.connect(user, secret, options) .then(amiConnection => { this._connection = amiConnection; this._userDisconnect = false; this.emit('connect', this._connection); this._connection .on('event', event => { if(!this._eventIsAllow(event)){ return; } if(this._options.addTime){ event.$time = Date.now(); } this.emit('event', event); if(this._options.emitEventsByTypes && event.Event){ let eventName = this._options.eventTypeToLowerCase ? event.Event.toLowerCase() : event.Event; this.emit(eventName, event); } }) .on('response', response => { if(this._options.keepAlive && response.ActionID === this._kaActionId){ debugLog('keep-alive heart bit'); this._keepAliveBit(); return; } if(this._options.addTime){ response.$time = Date.now(); } if(response.ActionID){ if(this._options.emitResponsesById){ this.emit(`resp_${response.ActionID}`, response); } this._prEmitter.emit(`resp_${response.ActionID}`, response); if(!this._options.dontDeleteSpecActionId && response.ActionID.startsWith(this._specPrefix)){ delete response.ActionID; } } this.emit('response', response); }) .on('data', chunk => this.emit('data', chunk)) .on('error', error => this.emit('internalError', error)) .on('close', () => { clearTimeout(this._kaTimer); this.emit('disconnect'); this._prEmitter.emit('disconnect'); setTimeout(() => { this._connection.removeAllListeners(); if(!this._userDisconnect && this._options.reconnect){ this.emit('reconnection'); this.connect( this._credentials.user, this._credentials.secret, this._connectionOptions ) .catch(error => this.emit('internalError', error)); } }, 1); }); if(this._options.keepAlive){ this._keepAliveBit(); } return this._connection; }) } /** * Disconnect from Asterisk */ disconnect(){ this._userDisconnect = true; clearTimeout(this._kaTimer); this.emit('disconnect'); if(this._connection){ this._connection.close(); setTimeout(this._connection.removeAllListeners, 1); } return this; } /** * * @param message * @param promisable * @returns {AmiClient} */ action(message, promisable){ if(!this._connection){ throw new Error(`Call 'connect' method before.`); } this._lastAction = message; this.emit('action', message); if(!message.ActionID){ message.ActionID = this._genActionId(this._specPrefix); } if(promisable){ return this._promisable(message); } this._connection.write(message); return this; } /** * * @param message * @param promisable * @returns {*} */ write(message, promisable){ return this.action(message, promisable); } /** * * @param message * @param promisable * @returns {*} */ send(message, promisable){ return this.action(message, promisable); } /** * * @param name * @param value * @returns {*} */ option(name, value){ if(!Object.hasOwnProperty.call(this._options, name)){ return value === undefined ? undefined : false; } if(value !== undefined){ this._options[name] = value; this._prepareOptions(); return true; } return this._options[name]; } /** * * @param newOptions * @returns {*} */ options(newOptions){ if(newOptions === undefined){ return this._options; } Object.keys(this._options).forEach(optionName => { if(Object.hasOwnProperty.call(newOptions, optionName)){ this._options[optionName] = newOptions[optionName]; } }, this); return this._prepareOptions(); } /** * Keep-alive heart bit handler * @private */ _keepAliveBit(){ this._kaTimer = setTimeout(() => { if(this._options.keepAlive && this._connection && this.isConnected){ this._kaActionId = this._genActionId(this._specPrefix); this._connection.write({ Action: 'Ping', ActionID: this._kaActionId }); } }, this._options.keepAliveDelay); this._kaTimer.unref(); return this; } /** * * @returns {AmiClient} * @private */ _prepareOptions(){ if(this._options.eventFilter && !(this._options.eventFilter instanceof Set)){ let eventNames = this._options.eventFilter; if(!Array.isArray(eventNames)){ eventNames = Object.keys(this._options.eventFilter); } eventNames = eventNames.reduce((result, eventName) => { let name = eventName ? eventName.toString() : ''; if(name){ result.push(name.toLowerCase()); } return result; }, []); this._options.eventFilter = new Set(eventNames); } return this; } /** * * @param event * @private */ _eventIsAllow(event){ let eventName = event.Event ? event.Event.toLowerCase() : null; if(eventName && this._options.eventFilter){ return !this._options.eventFilter.has(eventName); } return true; } /** * * @param prefix * @returns {string} * @private */ _genActionId(prefix){ prefix = prefix || ''; return `${prefix}${Date.now()}`; } /** * * @param message * @private */ _promisable(message){ return new Promise((resolve, reject) => { let resolveTimer = setTimeout(() => { reject(new Error('Timeout response came.')); }, 10000).unref(); this._connection.write(message); this._prPendingActions[message.ActionID] = message; this._prEmitter .on(`resp_${message.ActionID}`, response => { clearTimeout(resolveTimer); resolve(response); }) .on(`disconnect_${message.ActionID}`, () => { clearTimeout(resolveTimer); reject(new Error('Client disconnected.')); }); }) .catch(error => error) .then(response => { this._prEmitter.removeAllListeners(`disconnect_${message.ActionID}`); this._prEmitter.removeAllListeners(`resp_${message.ActionID}`); delete this._prPendingActions[message.ActionID]; if(response instanceof Error){ throw response; } return response; }); } /** * * @returns {null} */ get lastEvent(){ return this._connection ? this._connection.lastEvent : null; } /** * * @returns {null} */ get lastResponse(){ let response = this._connection ? this._connection.lastResponse : null; if(response && response.ActionID && !this._options.dontDeleteSpecActionId && response.ActionID.startsWith(this._specPrefix)){ delete response.ActionID; } return response } /** * * @returns {boolean} */ get isConnected(){ return this._connection ? this._connection.isConnected : null; } /** * * @returns {null} */ get lastAction(){ return this._lastAction; } /** * * @returns {T|*} */ get connection(){ return this._connection; } } module.exports = AmiClient;