asterisk.io
Version:
node.js asterisk pbx io
495 lines (397 loc) • 13.8 kB
JavaScript
/**
* @file Asterisk Gateway Interface (AGI).
* @author Zugravu Eugen Marius <marius@zugravu.com>
* @see {@link https://wiki.asterisk.org/wiki/display/AST/Asterisk+13+AGI+Commands|Asterisk 13 AGI Commands.}
*/
/** Node modules used. */
var node = {
net: require('net'),
util: require('util'),
Transform: require('stream').Transform,
EventEmitter: require('events').EventEmitter
},
/** Const errors used by module. */
error = {
E_AGI_UNDEFINED: "Undefined error.",
E_AGI_ARGUMENT_PORT: "Argument 'port' missing in function call.",
E_AGI_SERVER_ERROR: "Server error. Code: %s.",
E_AGI_SERVER_CLOSE: "Server closed.",
E_AGI_SOCKET_ERROR: "Socket error. Code: %s.",
E_AGI_SOCKET_CLOSE: "Socket closed.",
E_AGI_COMMAND_EMPTY: "Empty command.",
E_AGI_IVR_AGI_NETWORK_SCRIPT: "Missing 'ivr.agi_network_script'.",
E_AGI_IVR_ENTRY: "Missing 'ivr.entry' menu."
};
/**
* AGI Error.
*
* @constructor
* @param {string} name - error const name
*/
function AGIError(name){
var name = name || null,
args = args || [],
i = 1,
message = '';
if(!error[name]){
name = 'E_AGI_UNDEFINED';
}
args.push(error[name]);
while(arguments[i]){
args.push(arguments[i]);
i++;
}
message = node.util.format.apply(node.util, args);
Error.call(this);
Error.captureStackTrace(this, arguments.callee);
this.message = message;
this.name = name;
}
AGIError.prototype.__proto__ = Error.prototype;
/**
* Stream parser.
*
* @constructor
* @param {object} options - stream.Transform object
*/
function AGIParser(options){
node.Transform.call(this, options);
this._localBuffer = '';
this._inBody = false;
}
node.util.inherits(AGIParser, node.Transform);
AGIParser.prototype._transform = function(chunk, encoding, done){
var eol = "\n", // end of line
eom = ["\n\n", "\r\n\r\n"], // end of message
foundEol = -1,
foundEom = -1,
foundEomStr = '',
eomIndex = 0,
tmpLocalBuffer = '',
message = '';
// add chunk to local buffer
this._localBuffer += chunk.toString();
// temporary variable of local buffer as string
tmpLocalBuffer = this._localBuffer;
// we have agi_xyz variables?
if(!this._inBody){
// try to find end of message with any separator
eomIndex = 0;
while(eom[eomIndex]){
// search at least one message separator
while((foundEom = tmpLocalBuffer.indexOf(eom[eomIndex])) != -1){
// we have a message
// separator found, save it
foundEomStr = eom[eomIndex];
// get a message
message = tmpLocalBuffer.substring(0, foundEom);
// remove this message from local buffer
tmpLocalBuffer = tmpLocalBuffer.substring(foundEom+foundEomStr.length);
// we have a complete message, build key, value pairs
var lines = message.split(eol),
lineIndex = 0,
messajeJson = {},
key = '',
value = '',
foundColon = -1;
// parse evey line from the message
while(lines[lineIndex]){
// found colon on line
foundColon = lines[lineIndex].indexOf(':');
// check if we have a colon in the line
if(foundColon != -1){
// we have a good line let extract key, value pair
key = lines[lineIndex].slice(0, foundColon).trim();
value = lines[lineIndex].slice(foundColon+1).trim();
if(key.length > 0){
// add key, value to object
messajeJson[key] = value;
}
} else {
// not a good line message, do nothing with it
}
lineIndex++;
}
this.emit('headers', messajeJson);
this._inBody = true;
}
eomIndex++;
}
// save the new local buffer
this._localBuffer = tmpLocalBuffer;
} else {
while((foundEol = tmpLocalBuffer.indexOf(eol)) != -1){
// get the first message, until \n
message = tmpLocalBuffer.substring(0, foundEol);
this.emit('line', message);
// remove this message from local buffer
tmpLocalBuffer = tmpLocalBuffer.substring(foundEol+eol.length);
}
// save the new local buffer
this._localBuffer = tmpLocalBuffer;
}
// tell stream.Transform to continue
done();
}
/**
* AGI Handler.
*
* @constructor
* @param {socket} object - net.Socket object
*/
function AGIHandler(socket){
var me = this,
parser = new AGIParser(),
multiline = false,
command = {};
node.EventEmitter.call(me);
socket.setEncoding('utf8');
parser.setEncoding('utf8');
parser.on('headers', function(headers){
for(var x in headers){
me[x] = headers[x];
}
me.emit('ready');
});
parser.on('line', function(line){
if(line.toLowerCase() == 'hangup'){
me.emit('hangup');
socket.destroy();
return;
}
if(line.substring(0, 11) == '520-Invalid'){
multiline = true;
}
if(multiline){
command.rx += line+"\n";
if(line.search('520 End of proper') != -1){
multiline = false;
}
} else {
command.rx = line;
}
if(multiline){
return;
}
if(typeof command.cb == 'function'){
var code = command.rx.substring(0, 3),
result = null,
data = null;
code = parseInt(code);
if(isNaN(code)){
command.cb();
return;
}
if(code != 200){
command.cb(code);
return;
}
result = command.rx.substring(4).replace('result=', '');
if(result.indexOf(" ") != -1){
result = result.substr(0, result.indexOf(" "));
}
result = parseInt(result);
data = command.rx.split(" ");
if(data.length == 3){
data = data[2].substring(1, data[2].length-1);
} else {
data = null;
}
command.cb(code, result, data);
}
});
socket.on('error', function(err){
me.emit('error', new AGIError('E_AGI_SOCKET_ERROR', err.code));
});
socket.on('close', function(){
me.emit('close', new AGIError('E_AGI_SOCKET_CLOSE'));
});
socket.pipe(parser);
/**
* Execute command on asterisk and return code, result and data in callback function.
*
* @param {string} cmd - AGI Command with paramentes.
* @param {function} cb - Callback function when response is available.
*/
this.command = function(cmd, cb){
var cmd = cmd || '',
cb = (typeof cb == 'function') ? cb : new Function();
command = {
tx: cmd,
rx: '',
cb: cb
}
socket.write(cmd+"\n");
},
/**
* Close client socket connection.
*/
this.close = function(){
socket.destroy();
}
}
node.util.inherits(AGIHandler, node.EventEmitter);
/**
* AGI IVR Handler.
*
* @param {object} agiHandler - AGIHandler instance.
* @param {object} ivr - user defined IVR object.
*/
function AGIIVR(agiHandler, ivr){
var playMenu = null;
playMenu = function(menu, index){
// if(!ivr[menu]) {
// agiHandler.close();
// return;
// }
if(!menu.cmds){
agiHandler.close();
return;
}
if(!menu.cmds[index]){
agiHandler.close();
return;
}
if(!menu.cmds[index].command) {
agiHandler.close();
return;
}
console.log(menu.cmds[index].command);
agiHandler.command(menu.cmds[index].command, function(code, result, data){
console.log(' %s', JSON.stringify(arguments));
if(code == 200){
if(String.fromCharCode(result) == "1" && menu.cmds[index].key1){
playMenu(menu.cmds[index].key1, 0);
return;
} else if(String.fromCharCode(result) == "2" && menu.cmds[index].key2){
playMenu(menu.cmds[index].key2, 0);
return;
} else if(String.fromCharCode(result) == "3" && menu.cmds[index].key3){
playMenu(menu.cmds[index].key3, 0);
return;
} else if(String.fromCharCode(result) == "4" && menu.cmds[index].key4){
playMenu(menu.cmds[index].key4, 0);
return;
} else if(String.fromCharCode(result) == "5" && menu.cmds[index].key5){
playMenu(menu.cmds[index].key5, 0);
return;
} else if(String.fromCharCode(result) == "6" && menu.cmds[index].key6){
playMenu(menu.cmds[index].key6, 0);
return;
} else if(String.fromCharCode(result) == "7" && menu.cmds[index].key7){
playMenu(menu.cmds[index].key7, 0);
return;
} else if(String.fromCharCode(result) == "8" && menu.cmds[index].key8){
playMenu(menu.cmds[index].key8, 0);
return;
} else if(String.fromCharCode(result) == "9" && menu.cmds[index].key9){
playMenu(menu.cmds[index].key9, 0);
return;
} else if(String.fromCharCode(result) == "0" && menu.cmds[index].key0){
playMenu(menu.cmds[index].key0, 0);
return;
} else if(String.fromCharCode(result) == "*" && menu.cmds[index].keyAsterisk){
playMenu(menu.cmds[index].keyAsterisk, 0);
return;
} else if(String.fromCharCode(result) == "#" && menu.cmds[index].keyPound){
playMenu(menu.cmds[index].keyPound, 0);
return;
} else if(result == 0 && menu.cmds[index].keyNone){
playMenu(menu.cmds[index].keyNone, 0);
return;
}
playMenu(menu, index+1);
return;
}
agiHandler.close();
});
}
playMenu(ivr.entry, 0);
}
/**
* Asterisk Gateway Interface.
*
* @constructor
* @param {int} port - agi server port number
* @param {string} host - agi host, default '0.0.0.0'
*/
function AGI(port, host){
var me = this,
port = port || 0,
host = host || '0.0.0.0',
server = null,
ivrs = {};
node.EventEmitter.call(me);
function run(){
if(!port){
me.emit('error', new AGIError('E_AGI_ARGUMENT_PORT'));
return false;
}
server = node.net.createServer();
server.listen(port, host);
server.on('listening', function(){
me.emit('listening');
});
server.on('connection', function(socket){
var handler = new AGIHandler(socket);
handler.on('ready', function(){
var ivrFound = false;
for(var x in ivrs){
if(handler.agi_network_script && (x == handler.agi_network_script)){
AGIIVR(handler, ivrs[x]);
ivrFound = true;
}
}
if(!ivrFound){
me.emit('connection', handler);
}
});
});
server.on('error', function(err){
me.emit('error', new AGIError('E_AGI_SERVER_ERROR', err.code));
});
server.on('close', function(){
me.emit('close', new AGIError('E_AGI_SERVER_CLOSE'));
});
}
process.nextTick(function(){
run();
});
/**
* Handler for 'close' method of server.
*
* @param {function} cb - Callback function when response is available.
*/
this.close = function(cb){
var cb = (typeof cb == 'function') ? cb : null;
if(cb){
server.close(cb);
return;
}
server.close();
}
/**
* Play a defined IVR object for all connection with agi_network_script = ivr.agi_network_script.
*
* @param {object} ivr - IVR as JSON object.
*/
this.ivr = function(ivr, cb){
var ivr = (typeof ivr == 'object') ? ivr : {},
cb = (typeof cb == 'function') ? cb : new Function();
if(!ivr.agi_network_script){
cb(new AGIError('E_AGI_IVR_AGI_NETWORK_SCRIPT'));
return;
}
if(typeof ivr.entry != 'object'){
cb(new AGIError('E_AGI_IVR_ENTRY'));
return;
}
ivrs[ivr.agi_network_script] = ivr;
cb(null);
}
}
node.util.inherits(AGI, node.EventEmitter);
module.exports = function(port, host){
/** New instance fo AGI class. */
return new AGI(port, host);
}