UNPKG

asterisk-manager

Version:

A node.js module for interacting with the Asterisk Manager API.

365 lines (300 loc) 10.5 kB
/* * NodeJS Asterisk Manager API * (Based on https://github.com/mscdex/node-asterisk.git) * But radically altered thereafter so as to constitute a new work. * * © See LICENSE file * */ /* jshint node:true, newcap:false */ 'use strict'; var debug = false; var EventEmitter = require('events').EventEmitter; var Net = require('net') var Utils = require('./utils'); var Manager = function Manager(port, host, username, password, events) { var obj = {}; var context = { backoff: 10000 }; var properties = ['on', 'once', 'addListener', 'removeListener', 'removeAllListeners', 'listeners', 'setMaxListeners', 'emit']; context.emitter = new EventEmitter(); context.held = []; properties.map(function(property){ Object.defineProperty(obj, property, { value: context.emitter[property].bind(context.emitter) }); }) obj.options = { port: port, host: host || "", username: username || "", password: password || "", events: events || false }; obj.connect = ManagerConnect.bind(obj, context); obj.keepConnected = ManagerKeepConnected.bind(obj, context); obj.login = ManagerLogin.bind(obj, context); obj.action = ManagerAction.bind(obj, context); obj.disconnect = ManagerDisconnect.bind(obj, context); obj.isConnected = ManagerIsConnected.bind(obj, context); obj.connected = obj.isConnected; obj.on('rawevent', ManagerEvent.bind(obj, context)); obj.on('error', function (err) {}); obj.on('connect', ManagerResetBackoff.bind(obj, context )); if (port){ obj.connect( obj.options.port, obj.options.host, obj.options.username ? obj.login.bind(obj, obj.options.username, password, events) : undefined ); } return obj; }; function ManagerConnect(context, port, host, callback) { callback = Utils.defaultCallback(callback); context.connection = (context.connection && (context.connection.readyState != 'closed')) ? context.connection : undefined; if (context.connection) return callback.call(this, null); context.authenticated = false; context.connection = Net.createConnection(port, host); context.connection.setKeepAlive(true); context.connection.setNoDelay(true); context.connection.setEncoding('utf-8'); context.connection.once('connect', callback.bind(this, null)); context.connection.on('connect', this.emit.bind(this, 'connect')); context.connection.on('close', this.emit.bind(this, 'close')); context.connection.on('end', this.emit.bind(this, 'end')); context.connection.on('data', ManagerReader.bind(this, context)); context.connection.on('error', ManagerConnectionError.bind(this, context)); } function ManagerConnectionError(context, error) { this.emit('error', error); if (debug) { error = String(error.stack).split(/\r?\n/); var msg = error.shift(); error = error.map(function(line) { return ' ↳ ' + line.replace(/^\s*at\s+/, ''); }); error.unshift(msg); error.forEach(function(line) { process.stderr.write(line + '\n'); }); } } function ManagerReader(context, data) { context.lines = context.lines || []; context.leftOver = context.leftOver || ''; context.leftOver += String(data); context.lines = context.lines.concat(context.leftOver.split(/\r?\n/)); context.leftOver = context.lines.pop(); var lines = []; var follow = 0 var item = {}; while (context.lines.length) { var line = context.lines.shift(); if (!lines.length && (line.substr(0, 21) === 'Asterisk Call Manager')) { // Ignore Greeting } else if (!lines.length && (line.substr(0, 9).toLowerCase() === 'response:') && (line.toLowerCase().indexOf('follow') > -1)) { follow = 1; lines.push(line); } else if (follow && ((line === '--END COMMAND--') || (line === '--END SMS EVENT--'))) { follow = 2; lines.push(line); } else if ((follow > 1) && !line.length) { follow = 0; lines.pop(); item = { 'response': 'follows', 'content': lines.join('\n') }; var matches = item.content.match(/actionid: ([^\r\n]+)/i); item.actionid = matches ? matches[1] : item.actionid; lines = []; this.emit('rawevent', item); } else if (!follow && !line.length) { // Have a Complete Item lines = lines.filter(Utils.stringHasLength); item = {}; while (lines.length) { line = lines.shift(); line = line.split(': '); var key = Utils.removeSpaces(line.shift()).toLowerCase(); line = line.join(': '); if (key === 'variable' || key === 'chanvariable') { // Handle special case of one or more variables attached to an event and // create a variables subobject in the event object if (typeof(item[key]) !== 'object') item[key] = {}; line = line.split('='); var subkey = line.shift(); item[key][subkey] = line.join('='); } else { // Generic case of multiple copies of a key in an event. // Create an array of values. if (key in item) { if (Array.isArray(item[key])) item[key].push(line); else item[key] = [item[key], line]; } else item[key] = line; } } context.follow = false; lines = []; this.emit('rawevent', item); } else { lines.push(line); } } context.lines = lines; } function ManagerEvent(context, event) { var emits = []; if (event.response && event.actionid && typeof event.response == "string") { // This is the response to an Action emits.push(this.emit.bind(this, event.actionid, (event.response.toLowerCase() == 'error') ? event : undefined, event)); emits.push(this.emit.bind(this, 'response', event)); } else if (event.response && event.content) { // This is a follows response emits.push(this.emit.bind(this, context.lastid, undefined, event)); emits.push(this.emit.bind(this, 'response', event)); } if (event.event) { // This is a Real-Event event.event = Array.isArray(event.event) ? event.event.shift() : event.event; event.event += ''; // Make Sure that this is always a string emits.push(this.emit.bind(this, 'managerevent', event)); emits.push(this.emit.bind(this, event.event.toLowerCase(), event)); if (('userevent' === event.event.toLowerCase()) && event.userevent) emits.push(this.emit.bind(this, 'userevent-' + event.userevent.toLowerCase(), event)); } else { // Ooops I dont know what this is emits.push(this.emit.bind(this, 'asterisk', event)); } emits.forEach(process.nextTick.bind(process)); } function ManagerLogin(context, callback) { callback = Utils.defaultCallback(callback); var options = this.options; this.action({ 'action': 'login', 'username': options.username, 'secret': options.password, 'event': options.events ? 'on' : 'off' }, (function(err) { if (err) return callback(err); process.nextTick(callback.bind(this)); context.authenticated = true; var held = context.held; context.held = []; held.forEach((function(held) { this.action(held.action, held.callback); }).bind(this)); }).bind(this)); return; } function ManagerKeepConnected(context) { if (this.reconnect) return; if (this.isConnected() === false) { this.reconnect = ManagerReconnect.bind(this, context); this.on('close', this.reconnect); } } function ManagerReconnect(context) { console.log('Trying to reconnect to AMI in '+ (context.backoff / 1000) +' seconds'); var connect = this.connect.bind(context, this.options.port, this.options.host, this.login.bind(this)); setTimeout(connect, context.backoff); if(context.backoff < 60000){ //The maximum reconection time is 60 seconds context.backoff += 10000; //Increase reconnection time by 10 seconds } } function ManagerResetBackoff(context) { context.backoff = 10000; } function MakeManagerAction(req, id) { var msg = []; msg.push('ActionID: ' + id); Object.keys(req).forEach(function (key) { var nkey = Utils.removeSpaces(key).toLowerCase(); if (!nkey.length || ('actionid' == nkey)) return; var nval = req[key]; nkey = nkey.substr(0, 1).toUpperCase() + nkey.substr(1); switch (typeof nval) { case 'undefined': return; case 'object': if (!nval) return; if (nval instanceof Array) { nval = nval.map(function(e) { return String(e); }).join(','); } else if (nval instanceof RegExp === false) { Object.keys(nval).forEach( function(name) { msg.push( nkey + ": " + name + "=" + nval[name].toString() ); }); return; } break; default: nval = String(nval); break; } msg.push([nkey, nval].join(': ')); }); msg.sort(); return msg.join('\r\n') + '\r\n\r\n'; } function ManagerAction(context, action, callback) { action = action || {}; callback = Utils.defaultCallback(callback); var id = action.actionid || String((new Date()).getTime()); while (this.listeners(id).length) id += String(Math.floor(Math.random() * 9)); if (action.actionid) delete action.actionid; if (!context.authenticated && (action.action !== 'login')) { context.held = context.held || []; action.actionid = id; context.held.push({ action: action, callback: callback }); return id; } try { if (!context.connection) { throw new Error('There is no connection yet'); } context.connection.write(MakeManagerAction(action, id), 'utf-8'); } catch (e) { console.log('ERROR: ', e); context.held = context.held || []; action.actionid = id; context.held.push({ action: action, callback: callback }); return id; } this.once(id, callback); return context.lastid = id; } function ManagerDisconnect(context, callback) { if (this.reconnect) { this.removeListener('close', this.reconnect); } if (context.connection && context.connection.readyState === 'open') { context.connection.end(); } delete context.connection; if ('function' === typeof callback) { setImmediate(callback); } } function ManagerIsConnected(context) { return (context.connection && context.connection.readyState === 'open'); } // Expose `Manager`. module.exports = Manager;