nami-raw
Version:
Minor fork of NAMI. It adds a raw event emitter and ability to subscribe to events on connect, sets keepalive. Original Description: An asterisk manager interface client, uses EventEmitter to communicate events, will allow you to send actions, and recei
333 lines (305 loc) • 11.3 kB
JavaScript
/*!
* Nami core class.
*
* Copyright 2011 Marcelo Gornstein <marcelog@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
/**
* @fileoverview Nami client code.
*
* @author Marcelo Gornstein - http://marcelog.github.com
* Website: http://marcelog.github.com/Nami
*/
var net = require('net');
var events = require('events');
var action = require(__dirname + '/message/action.js');
var namiResponse = require(__dirname + '/message/response.js');
var util = require('util');
var namiEvents = require(__dirname + '/message/event.js');
var timer = require('timers');
var logger = require('../../../logger');
/**
* Nami client.
* @constructor
* @param amiData The configuration for ami.
* @augments EventEmitter
*/
function Nami(amiData) {
Nami.super_.call(this);
this.logger = logger;
// this.logger = require('log4js').getLogger('Nami.Client');
// this.logger.setLevel('ERROR');
this.connected = false;
this.amiData = amiData;
this.EOL = "\r\n";
this.EOM = this.EOL + this.EOL;
this.welcomeMessage = "Asterisk Call Manager/(1\\.[0-3]|2\\.\\d\\.\\d)" + this.EOL;
this.received = false;
this.responses = { };
this.callbacks = { };
this.on('namiRawMessage', this.onRawMessage);
this.on('namiRawResponse', this.onRawResponse);
this.on('namiRawEvent', this.onRawEvent);
}
// Nami inherits from the EventEmitter so Nami itself can throw events.
util.inherits(Nami, events.EventEmitter);
/**
* Called when a message arrives and is decoded as an event (namiRawEvent event).
* This will actually instantiate an Event. If the event has an ActionID,
* the corresponding response is looked up and will have this event appended.
* Otherwise, the event "namiEvent" is fired. Also, the event "namiEvent<EventName>"
* is fired (i.e: on event Dial, namiEventDial will be fired).
*
* @see Nami#onRawMessage(String)
* @param {Event} response An Event message.
* @returns void
*/
Nami.prototype.onRawEvent = function (event) {
this.logger.debug('Got event: ' + util.inspect(event));
if (
typeof (event.actionid) !== 'undefined'
&& typeof (this.responses[event.actionid]) !== 'undefined'
&& typeof (this.callbacks[event.actionid]) !== 'undefined'
) {
this.responses[event.actionid].events.push(event);
if (
event.event.indexOf('Complete') !== -1
|| ((typeof (event.eventlist) !== 'undefined') && event.eventlist.indexOf('Complete') !== -1)
|| event.event.indexOf('DBGetResponse') !== -1
) {
this.callbacks[event.actionid](this.responses[event.actionid]);
delete this.callbacks[event.actionid];
delete this.responses[event.actionid];
}
} else {
this.emit('namiEvent', event);
this.emit('namiEvent' + event.event, event);
}
};
/**
* Called when a message arrives and is decoded as a response (namiRawResponse event).
* This will actually instantiate a Response. If this response has associated events
* (still to be received), it is buffered.
* Otherwise, the callback used in send() will be called with the response.
* @see Nami#onRawMessage(String)
* @see Nami#send(String)
* @param {Response} response A Response message.
* @returns void
*/
Nami.prototype.onRawResponse = function (response) {
this.logger.debug('Got response: ' + util.inspect(response));
if (
(typeof (response.message) !== 'undefined')
&& (response.message.indexOf('follow') !== -1)
) {
this.responses[response.actionid] = response;
} else if (typeof (this.callbacks[response.actionid]) !== 'undefined') {
this.callbacks[response.actionid](response);
delete this.callbacks[response.actionid];
delete this.responses[response.actionid];
}
};
/**
* Called by onData() whenever a raw message has been read.
* Will fire "namiRawEvent" if the raw message represents an event.
* Will fire "namiRawResponse" if the raw message represents a response.
* @see Nami#onData(String)
* @param {String} buffer The raw message read from server.
* @returns void
*/
Nami.prototype.onRawMessage = function (buffer) {
var response, event;
this.logger.debug('Building raw message: ' + util.inspect(buffer));
if (buffer.match(/^Event: /) !== null) {
event = new namiEvents.Event(buffer);
// this.emit('namiRawEvent', event);
this.emit('namiRawEventUnmarshalled', buffer); // This includes the raw event unmarshalled with \r\n etc still intact
} else if (buffer.match(/^Response: /) !== null) {
response = new namiResponse.Response(buffer);
this.emit('namiRawResponse', response);
} else {
this.logger.warn("Discarded: |" + buffer + "|");
}
};
/**
* Called by node whenever data is available to be read from AMI.
* Will fire "namiRawMessage" for every complete message read.
* @param {String} data The data read from server.
* @see Nami#onRawMessage(String)
* @returns void
*/
Nami.prototype.onData = function (data) {
var theEOM = -1, msg;
this.logger.debug('Got data: ' + util.inspect(data));
this.received = this.received.concat(data);
theEOM = -1;
while ((theEOM = this.received.indexOf(this.EOM)) !== -1) {
msg = this.received.substr(0, theEOM);
this.emit('namiRawMessage', msg);
var startOffset = theEOM + this.EOM.length;
var skippedEolChars = 0;
var nextChar = this.received.substr(startOffset + skippedEolChars, 1);
while (nextChar === "\r" || nextChar === "\n") {
skippedEolChars++;
nextChar = this.received.substr(startOffset + skippedEolChars, 1);
};
this.logger.debug('Skipped ' + skippedEolChars + ' bytes');
this.received = this.received.substr(startOffset + skippedEolChars);
}
};
/**
* Called when the connection is established to AMI.
* @returns void
*/
Nami.prototype.onConnect = function () {
this.connected = true;
};
Nami.prototype.onClosed = function () {
this.connected = false;
};
/**
* Called when the first line is received from the server. It will check that
* the other peer is a valid AMI server. If not valid, the event "namiInvalidPeer"
* will be fired. If not, a login is tried, and onData() is set as the new handler
* for incoming data. An anonymous function will handle the login response, firing
* "namiLoginIncorrect" if the username/password were not correctly validated.
* On successfull connection, "namiConnected" is emitted.
* @param {String} data The data read from server.
* @see Nami#onData(String)
* @see Login(String, String, String)
* @returns void
*/
Nami.prototype.onWelcomeMessage = function (data) {
var self = this, welcome;
this.logger.debug('Got welcome message: ' + util.inspect(data));
var re = new RegExp(this.welcomeMessage, "");
if (data.match(re) === null) {
this.emit('namiInvalidPeer', data);
} else {
this.socket.on('data', function (data) {
self.onData(data);
});
this.send(
new action.Login(this.amiData.username, this.amiData.secret, this.amiData.events),
function (response) {
if (response.response !== 'Success') {
self.emit('namiLoginIncorrect');
} else {
self.emit('namiConnected');
}
}
);
}
};
/**
* Closes the connection to AMI.
* @returns void
*/
Nami.prototype.close = function () {
var self = this;
this.send(new action.Logoff(), function () { self.logger.info('Logged out'); });
this.logger.info('Closing connection');
this.removeAllListeners();
this.socket.removeAllListeners();
this.socket.end();
this.onClosed();
};
/**
* Opens the connection to AMI.
* @returns void
*/
Nami.prototype.open = function () {
this.logger.debug('Opening connection');
this.received = "";
this.initializeSocket();
};
/**
* Creates a new socket and handles connection events.
* @returns undefined
*/
Nami.prototype.initializeSocket = function () {
this.logger.debug('Initializing socket');
var self = this;
if (this.socket && !this.socket.destroyed) {
this.socket.removeAllListeners();
this.socket.end();
}
this.socket = new net.Socket();
this.socket.setEncoding('ascii');
this.socket.setKeepAlive(true, 10000);
var baseEvent = 'namiConnection';
this.socket.on('connect', function() {
self.logger.debug('Socket connected');
self.onConnect();
var event = { event: 'Connect' };
self.emit(baseEvent + event.event, event);
});
// @param {Error} error Fires right before the `close` event
this.socket.on('error', function (error) {
self.logger.debug('Socket error: ' + util.inspect(error));
var event = { event: 'Error', error: error };
self.emit(baseEvent + event.event, event);
});
// @param {Boolean} had_error If the connection closed from an error.
this.socket.on('close', function (had_error) {
self.logger.debug('Socket closed');
self.onClosed();
var event = { event: 'Close', had_error: had_error };
self.emit(baseEvent + event.event, event);
});
this.socket.on('timeout', function () {
self.logger.debug('Socket timeout');
var event = { event: 'Timeout' };
self.emit(baseEvent + event.event, event);
});
this.socket.on('end', function () {
self.logger.debug('Socket ended');
var event = { event: 'End' };
self.emit(baseEvent + event.event, event);
});
this.socket.once('data', function (data) {
self.onWelcomeMessage(data);
var event = { event: 'Data', data: data };
self.emit(baseEvent + event.event, event); // Added by BR
});
this.socket.connect(this.amiData.port, this.amiData.host);
};
/**
* Reopens the socket connection to AMI.
* @returns undefined
*/
Nami.prototype.reopen = function () {
this.logger.debug('Reopening connection');
this.initializeSocket();
};
/**
* Sends an action to AMI.
*
* @param {Action} action The action to be sent.
* @param {function} callback The optional callback to be invoked when the
* responses arrives.
*
* @returns void
*/
Nami.prototype.send = function (action, callback) {
this.logger.debug('Sending: ' + util.inspect(action));
this.callbacks[action.ActionID] = callback;
this.responses[action.ActionID] = "";
this.socket.write(action.marshall());
};
exports.Nami = Nami;
exports.Actions = action;
exports.Event = namiEvents;
exports.Response = namiResponse;