BelirafoN/asterisk-ami-client

View on GitHub
lib/AmiClient.js

Summary

Maintainability
B
6 hrs
Test Coverage
/**
 * 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;