Commit ada684d8 authored by sergey's avatar sergey

add some files

parent 8d2e3888
# Logs node_modules/
logs
*.log
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directory
# Commenting this out is preferred by some people, see
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git-
node_modules
# Users Environment Variables
.lock-wscript
ding-dong # node-agi
=========
node-agi clone Client for asterisk AGI protocol. Parses incomming messages into events. Dispatches AGI commands and their responses from asterisk. Most commonly used as a low level client for a fAGI server.
## note: still a work in progress
## install
```
npm install agi
```
## API
### agi.createServer([listener])
Returns a new net.Server instance. The _listener_ will be called on a new agi connection with a single __Context__ object as described below.
```js
require('agi').createServer(function(context) {
//context is a new instance of agi.Context for each new agi session
//immedately after asterisk connects to the node process
context.on('variables', function(vars) {
console.log('received new call from: ' + vars.agi_callerid + ' with uniqueid: ' + vars.agi_uniqueid);
});
}).listen(3000);
```
### new agi.Context(stream)
Constructor to create a new instance of a context. Supply a readable and writable stream to the constructor. Commonly _stream_ will be a `net.Socket` instance.
### context.exec(command, [args], [callback])
Dispatches the `EXEC` AGI command to asterisk with supplied command name and arguments. _callback_ is called with the result of the dispatch.
```js
context.exec('ANSWER', function(err, res) {
//the channel is now answered
});
context.exec('RecieveFax', '/tmp/myfax.tif', function(err, res) {
//fax has been recieved by asterisk and written to /tmp/myfax.tif
});
```
### context.hangup([callbac])
Dispatches the 'HANGUP' AGI command to asterisk. Does __not__ close the sockets automatically. _callback_ is called with the result of the dispatch.
```js
context.hangup(function(err, res) {
//the channel has now been hungup.
});
```
var Readable = require('readable-stream');
var EventEmitter = require('events').EventEmitter;
var state = require('./state');
var Context = function(stream) {
EventEmitter.call(this);
this.stream = new Readable();
this.stream.wrap(stream);
this.state = state.init;
this.msg = "";
var self = this;
this.stream.on('readable', function() {
//always keep the 'leftover' part of the message
self.msg = self.read();
});
this.msg = this.read();
this.variables = {};
this.pending = null;
this.stream.on('error', this.emit.bind(this, 'error'));
this.stream.on('close', this.emit.bind(this, 'close'));
};
require('util').inherits(Context, EventEmitter);
Context.prototype.read = function() {
var buffer = this.stream.read();
if(!buffer) return this.msg;
this.msg += buffer.toString('utf8');
if(this.state === state.init) {
if(this.msg.indexOf('\n\n') < 0) return this.msg; //we don't have whole message
this.readVariables(this.msg);
} else if(this.state === state.waiting) {
if(this.msg.indexOf('\n') < 0) return this.msg; //we don't have whole message
this.readResponse(this.msg);
}
return "";
};
Context.prototype.readVariables = function(msg) {
var lines = msg.split('\n');
for(var i = 0; i < lines.length; i++) {
var line = lines[i];
var split = line.split(':')
var name = split[0];
var value = split[1];
this.variables[name] = (value||'').trim();
}
this.emit('variables', this.variables);
this.setState(state.waiting);
return "";
};
Context.prototype.readResponse = function(msg) {
var lines = msg.split('\n');
for(var i = 0; i < lines.length; i++) {
this.readResponseLine(lines[i]);
}
return "";
};
Context.prototype.readResponseLine = function(line) {
if(!line) return;
var parsed = /^(\d{3})(?: result=)(.*)/.exec(line);
if(!parsed) {
return this.emit('hangup');
}
var response = {
code: parseInt(parsed[1]),
result: parsed[2]
};
//our last command had a pending callback
if(this.pending) {
var pending = this.pending;
this.pending = null;
pending(null, response);
}
this.emit('response', response);
}
Context.prototype.setState = function(state) {
this.state = state;
};
Context.prototype.send = function(msg, cb) {
this.pending = cb;
this.stream.write(msg);
};
Context.prototype.exec = function() {
var args = Array.prototype.slice.call(arguments, 0);
var last = args.pop();
if(typeof last !== 'function') {
args.push(last);
last = function() { }
}
this.send('EXEC ' + args.join(' ') + '\n', last);
};
Context.prototype.dial = function (num, timeout, params, cb) {
this.exec('Dial', num + ',' + timeout + ',' + params, cb);
};
Context.prototype.getVariable = function(name, cb) {
this.send('GET VARIABLE ' + name + '\n', cb || function() { });
};
Context.prototype.streamFile = function(filename, acceptDigits, cb) {
if(typeof acceptDigits === 'function') {
cb = acceptDigits;
acceptDigits = "1234567890#*";
}
this.send('STREAM FILE "' + filename + '" "' + acceptDigits + '"\n', cb);
};
Context.prototype.waitForDigit = function(timeout, cb) {
if(typeof timeout === 'function') {
cb = timeout;
//default to 2 second timeout
timeout = 5000;
}
this.send('WAIT FOR DIGIT ' + timeout + '\n', cb);
};
Context.prototype.hangup = function(cb) {
this.send('HANGUP\n', cb);
};
Context.prototype.end = function() {
this.stream.end();
};
module.exports = Context;
var Context = require('./context');
var agi = {
state: require('./state'),
Context: Context,
createServer: function(handler) {
return require('net').createServer(function(stream) {
var context = new Context(stream);
handler(context);
});
}
};
module.exports = agi;
//states for context
var state = {
init: 0,
waiting: 2
};
module.exports = state;
{
"author": "Sergey Dmitriev <serge.dmitriev@gmail.com>",
"name": "ding-dong",
"description": "AGI (Asterisk Gateway Interface) for writing dialplan scripts",
"version": "0.0.2",
"repository": {
"type": "git",
"url": "git://github.com/antirek/ding-dong.git"
},
"main": "lib/",
"scripts": {
"test": "mocha -R tap"
},
"engines": {
"node": ">=0.8.0"
},
"dependencies": {
"readable-stream": "*"
},
"devDependencies": {
"mocha": "*",
"expect.js": "*",
"memstream": "*"
}
}
var MemoryStream = require('memstream').MemoryStream;
var agi = require('./../lib')
var expect = require('expect.js');
var Context = agi.Context;
var state = agi.state;
//helpers
var writeVars = function(stream) {
stream.write('agi_network: yes\n');
stream.write('agi_uniqueid: 13507138.14\n');
stream.write('agi_arg_1: test\n');
stream.write('\n\n');
};
var context = function(cb) {
var stream = new MemoryStream();
var ctx = new Context(stream);
//TODO nasty
ctx.send = function(msg, cb) {
ctx.pending = cb;
ctx.sent = ctx.sent || [];
ctx.sent.push(msg);
};
ctx.once('variables', function(vars) {
cb(ctx);
});
writeVars(stream);
};
describe('Context', function() {
beforeEach(function(done) {
var self = this;
context(function(context) {
self.context = context;
done();
});
});
describe('parsing variables', function() {
it('works', function(done) {
var vars = this.context.variables;
expect(vars['agi_network']).ok();
expect(vars['agi_network']).to.eql('yes');
expect(vars['agi_uniqueid']).to.eql('13507138.14');
expect(vars['agi_arg_1']).to.eql('test');
done();
});
it('puts context into waiting state', function() {
expect(this.context.state).to.eql(state.waiting);
});
});
describe('sending command', function() {
it('writes out', function() {
this.context.send('EXEC test');
expect(this.context.sent.length).to.eql(1);
expect(this.context.sent.join('')).to.eql('EXEC test');
});
});
describe('context.exec', function() {
it('sends exec command', function() {
this.context.exec('test', 'bang', 'another');
expect(this.context.sent.join('')).to.eql('EXEC test bang another\n');
});
});
describe('command flow', function() {
describe('success', function() {
it('emits proper repsonse', function(done) {
var context = this.context;
process.nextTick(function() {
context.exec('test', 'bang', 'another');
context.stream.write('200');
context.stream.write(' result=0\n\n');
});
context.on('response', function(msg) {
expect(msg.code).to.equal(200);
expect(msg.result).to.eql('0');
done();
});
});
it('invokes callback with response', function(done) {
var context = this.context;
process.nextTick(function(){
context.stream.write('200 result=0');
context.stream.write('\n');
context.stream.write('200 result=0');
context.stream.write('\n');
});
context.exec('test', 'boom', function(err, res) {
done(err);
});
});
});
describe('two commands', function(done) {
it('invokes two callbacks', function(done) {
var context = this.context;
process.nextTick(function() {
context.stream.write('200 result=0\n');
});
context.exec('test', function(err, res) {
expect(res.result).to.eql('0');
context.exec('test 2', function(err, res) {
expect(res.result).to.eql('1');
done();
});
process.nextTick(function() {
context.stream.write('200 result=1\n');
});
});
});
});
});
describe('hangup', function() {
it('raises hangup on context', function(done) {
this.context.on('hangup', done);
this.context.stream.write('HANGUP\n');
});
describe('in command response', function() {
it('is passed to callback', function(done) {
var context = this.context;
this.context.exec('whatever', function(err, res) {
});
this.context.on('hangup', done);
process.nextTick(function() {
context.stream.write('200 result=-1\nHANGUP\n');
})
});
});
});
describe('getVariable', function() {
it('sends correct command', function() {
this.context.getVariable('test');
expect(this.context.sent.join('')).to.eql('GET VARIABLE test\n');
});
it('gets result', function(done) {
this.context.getVariable('test', function(err, res) {
expect(res.result).eql('1 (abcd)');
done();
});
var self = this;
process.nextTick(function() {
self.context.stream.write('200 result=1 (abcd)\n');
})
});
});
describe('stream file', function() {
it('sends', function() {
this.context.streamFile('test', '1234567890#*', function() {});
expect(this.context.sent.join('')).to.eql('STREAM FILE "test" "1234567890#*"\n');
});
it('defaults to all digits', function() {
this.context.streamFile('test', function() {});
expect(this.context.sent.join('')).to.eql('STREAM FILE "test" "1234567890#*"\n');
});
});
describe('waitForDigit', function() {
it('sends with default timeout', function() {
this.context.waitForDigit(function() {});
expect(this.context.sent.join('')).to.eql('WAIT FOR DIGIT 5000\n');
});
it('sends with specified timeout', function() {
this.context.waitForDigit(-1, function() {});
expect(this.context.sent.join('')).to.eql('WAIT FOR DIGIT -1\n');
});
});
describe('hangup', function() {
it('sends "HANGUP\\n"', function() {
this.context.hangup();
expect(this.context.sent.join('')).to.eql('HANGUP\n');
});
});
describe('events', function() {
describe('error', function () {
it('is emitted when socket emits error', function(done) {
this.context.on('error', function(err) {
expect(err).to.eql('test');
done();
});
this.context.stream.emit('error', "test");
});
});
describe('close', function() {
it('is emitted when socket emits close', function(done) {
this.context.on('close', function(hasError) {
expect(hasError).ok();
done();
});
this.context.stream.emit('close', true);
});
});
});
});
describe('agi#createServer', function() {
it('returns instance of net.Server', function() {
var net = require('net');
var server = agi.createServer();
expect(server instanceof net.Server).ok();
});
it('invokes callback when a new connection is established', function(done) {
var server = agi.createServer(function(context) {
expect(context instanceof agi.Context);
done();
});
server.emit('connection', new MemoryStream());
});
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment