@ipcom/extended-ami
Version:
Advanced manager for connecting to Asterisk
474 lines • 18.3 kB
JavaScript
'use strict';
import { Socket } from 'net';
import { _indexOfArray, _isEmpty, _isFinite, _isNull, _isUndefined, _toNumber, } from './functions.js';
import { _AMI_EVENTS, _eAMI_EVENTS, CRLF, DEFAULT_PORT, END, HEARTBEAT_INTERVAL, MAX_RECONNECT_COUNT, RESEND_TIMEOUT, } from './constants.js';
import { eAmiActions } from './e-ami-actions.js';
import { EventEmitter } from 'events';
export * from './typeGuards.js';
export * from './types/events.js';
export const eAMI_EVENTS = _eAMI_EVENTS;
export const AMI_EVENTS = _AMI_EVENTS;
export class eAmi {
debug;
_host;
_port;
_userName;
_password;
_isLoggedIn;
_emitAllEvents;
_reconnect;
_heartbeatOk = false;
_lastConnectedTime = 0;
_maxReconnectCount;
_heartbeatInterval;
_heartbeatHandler = undefined; // ou Timer | null = null;
_resendTimeOut;
_successBitsByInterval;
_errorBitsByInterval;
_countReconnect;
_excludeEvents;
_queueRequest;
_socketHandler = undefined;
_actions;
events;
_maxAuthCount;
_authCount;
constructor(allOptions) {
const connect = allOptions, options = _isUndefined(connect.additionalOptions)
? {}
: connect.additionalOptions;
this._host = connect.host;
this._port = _isNull(connect.port) ? DEFAULT_PORT : connect.port;
this._userName = connect.userName;
this._password = connect.password;
this._reconnect = options?.reconnect ?? true;
this._heartbeatInterval =
(options?.heartbeatInterval ?? HEARTBEAT_INTERVAL) * 1000;
this._resendTimeOut = (options?.resendTimeOut ?? RESEND_TIMEOUT) * 1000;
this._excludeEvents = options?.excludeEvents ?? [];
this._emitAllEvents = options?.emitAllEvents ?? false;
this.debug = options?.debug ?? false;
this._maxReconnectCount =
options?.maxReconnectCount ?? MAX_RECONNECT_COUNT;
this._countReconnect = 0;
this._maxAuthCount = 5;
this._authCount = 0;
this._successBitsByInterval = 0;
this._errorBitsByInterval = 0;
this.events = new EventEmitter();
this._queueRequest = [];
this._isLoggedIn = false;
this._actions = new eAmiActions(this);
this.internalListeners();
}
internalListeners() {
this.events.on(eAMI_EVENTS.RE_LOGIN, () => {
if (this._authCount < this._maxAuthCount) {
setTimeout(async () => {
this._authCount++;
try {
await this.login();
}
catch (error) {
if (this.debug)
console.log('re-login', error);
}
}, 1000);
}
});
}
get excludeEvents() {
return this._excludeEvents;
}
set excludeEvents(events) {
this._excludeEvents = events;
}
get isLoggedIn() {
return this._isLoggedIn;
}
get lastConnectTime() {
return this._lastConnectedTime;
}
get actions() {
return this._actions;
}
get queueRequest() {
return this._queueRequest;
}
addSocketListeners() {
if (this._socketHandler) {
this._socketHandler
.on('close', () => {
if (this.debug)
console.log('close AMI connect');
})
.on('end', () => {
if (this.debug)
console.log('end AMI connect');
})
.on('data', (buffer) => this.getData(buffer));
}
else {
if (this.debug)
console.log('Socket handler is undefined, cannot add listeners.');
}
}
destroySocket() {
if (this._socketHandler) {
this._socketHandler.destroy();
if (this.debug)
console.log(CRLF + 'Socket connection destroyed');
}
else {
if (this.debug)
console.log('Socket handler is undefined, cannot destroy socket.');
}
}
addRequest(request) {
this.queueRequest.push(request);
this.events.emit(eAMI_EVENTS.SEND, request);
}
removeRequest(actionID) {
if (_isUndefined(actionID))
return false;
const index = _indexOfArray(this.queueRequest, actionID);
if (index < 0)
return false;
try {
this.queueRequest.splice(index, 1);
return true;
}
catch (error) {
if (this.debug)
console.log('Error remove request', error);
return false;
}
}
getRequest(actionID) {
const numActionID = _toNumber(actionID);
if (_isUndefined(actionID))
return null;
if (numActionID !== undefined && isFinite(numActionID)) {
actionID = numActionID;
}
const index = _indexOfArray(this.queueRequest, actionID);
if (index < 0)
return null;
return this.queueRequest[index];
}
setRequest(actionID, newRequest) {
let request = this.getRequest(actionID);
if (!request)
return;
request = newRequest;
}
keepConnection() {
return new Promise((resolve, reject) => {
(async () => {
clearInterval(this._heartbeatHandler);
try {
const response = await this.actions.Ping();
if (response) {
this._heartbeatOk = true;
resolve(true);
this._successBitsByInterval++;
}
this._heartbeatHandler = setTimeout(async () => {
try {
await this.keepConnection();
}
catch (error) {
console.log('keep timeout error', error);
this._errorBitsByInterval++;
}
}, this._heartbeatInterval);
}
catch (error) {
this._errorBitsByInterval++;
console.log('keep connect error', error);
reject(error);
}
})();
});
}
login() {
return new Promise((resolve, reject) => {
(async () => {
try {
const loginOptions = {
Username: this._userName,
Secret: this._password,
};
this.events.emit(eAMI_EVENTS.DO_LOGIN, loginOptions);
await this.actions.Login(loginOptions);
this.events.emit(eAMI_EVENTS.LOGGED_IN);
resolve(true);
}
catch (error0) {
this.events.emit(eAMI_EVENTS.ERROR_LOGIN, error0, 'Authorization failed...');
if (this._authCount < this._maxAuthCount) {
setTimeout(() => {
this._authCount++;
this.events.emit(eAMI_EVENTS.RE_LOGIN, this._authCount);
}, 1000);
}
else {
this.events.emit(eAMI_EVENTS.MAX_AUTH_REACH, this._authCount);
try {
await this.reconnect();
}
catch (error1) {
reject(error1);
}
}
}
})();
});
}
logout() {
return new Promise((resolve, reject) => {
(async () => {
try {
await this.actions.Logout();
resolve(true);
}
catch (error) {
this.events.emit(eAMI_EVENTS.ERROR_LOGOUT, error);
reject('Failed to logout');
}
})();
});
}
showSendPackages() {
setInterval(() => {
console.log('Keep Connection. success sent - %s, error sent - %s', this._successBitsByInterval, this._errorBitsByInterval);
}, 5000);
}
connect() {
return new Promise((resolve, reject) => {
this._socketHandler = new Socket();
this._socketHandler.connect(this._port, this._host);
this._socketHandler
.on('connect', async () => {
this.addSocketListeners();
try {
if (this.debug)
console.log('connection to the server');
await this.login();
this._isLoggedIn = true;
this._lastConnectedTime = new Date().getTime();
if (this.debug)
this.showSendPackages();
await this.keepConnection();
this.events.emit(eAMI_EVENTS.CONNECT);
resolve(this);
}
catch (error) {
if (this.debug)
console.log(error);
reject(error);
}
})
.on('error', (error) => {
this.events.emit(eAMI_EVENTS.ERROR_CONNECT, error, 'Error connecting to an asterisk server');
if (this.debug)
console.log('Error connecting to an asterisk server', error);
reject(false);
});
});
}
reconnect() {
if (!this._reconnect)
return Promise.resolve(true);
if (this._countReconnect < this._maxReconnectCount)
this._countReconnect++;
else {
this.events.emit(eAMI_EVENTS.MAX_RECONNECT_REACH, this._countReconnect);
return Promise.resolve(false);
}
return new Promise((resolve, reject) => {
(async () => {
try {
this.events.emit(eAMI_EVENTS.DO_RECONNECT);
await this.logout();
this.destroySocket();
await this.connect();
this.events.emit(eAMI_EVENTS.RECONNECTED);
resolve(true);
}
catch (error) {
this.events.emit(eAMI_EVENTS.ERROR_RECONNECT, error, 'Could not connect to Asterisk...');
reject('Could not connect to Asterisk...');
}
})();
});
}
action(request) {
return new Promise((resolve, reject) => {
let writed = false;
let message = '';
for (const key in request) {
if (key === 'ActionID')
continue;
message += key + ': ' + request[key] + CRLF;
}
if (_isUndefined(request['ActionID']))
request['ActionID'] = new Date().getTime();
const actionID = request['ActionID'];
message += 'ActionID: ' + actionID + CRLF + CRLF;
const _request = this.getRequest(actionID);
// handlers for resolve
if (_request !== null && _request !== undefined) {
this.events.once(`Action_${actionID}`, (response) => {
_request.Completed = true;
if (this.debug)
console.log('response', _request.ActionID, _request.Action);
resolve(response);
});
}
else {
this.events.once(`Action_${actionID}`, (response) => {
resolve(response);
});
}
const numActionID = _toNumber(actionID);
if (numActionID !== undefined && !_isFinite(numActionID)) {
this.events.once(String(actionID), (response) => {
if (_request !== null && _request !== undefined) {
_request.Completed = true;
}
resolve(response);
});
}
this.addRequest(request);
if (_request) {
const actionIDNumber = _toNumber(request['ActionID']);
if (typeof actionIDNumber === 'number' &&
_isFinite(actionIDNumber)) {
_request.ActionID = actionIDNumber;
}
else if (typeof request['ActionID'] === 'string' ||
typeof request['ActionID'] === 'number') {
_request.ActionID = request['ActionID'];
}
else {
_request.ActionID = undefined;
}
_request.Completed = true;
_request.timeOutHandler = setTimeout(async () => {
if (!writed) {
reject('Timeout write to socket...');
return;
}
if (_request.Completed === true) {
try {
await this.action(request);
}
catch (error) {
if (this.debug)
console.log('Error resend action', _request.Action, error);
reject('Error resend action' + _request.Action + error);
}
this._errorBitsByInterval++;
if (this.debug)
console.log('resend ActionID_' + actionID, _request.Action);
return;
}
clearTimeout(_request.timeOutHandler);
this.removeRequest(actionID);
this.events.removeAllListeners(String(actionID));
this.events.removeAllListeners(`Action_${actionID}`);
if (this.debug)
console.log('complete ' + actionID, _request.Action);
}, 3000);
}
const write = this._socketHandler?.write(message, () => {
writed = true;
});
if (write === false) {
if (this.debug)
console.log('Data in the sending queue');
reject('Data in the sending queue');
}
});
}
getData(buffer) {
let dataStr = buffer.toString(), iDelim, typeResponse = '', dataArray = [], keyValueArray = [], key = '', value = null, dataObject = {};
if (dataStr.startsWith('Asterisk Call Manager')) {
dataStr = dataStr.substring(dataStr.indexOf(CRLF) + 2);
}
while ((iDelim = dataStr.indexOf(END)) >= 0) {
dataArray = dataStr.substring(0, iDelim + 2).split(CRLF);
dataStr = dataStr.substring(iDelim + 4);
dataObject = {};
typeResponse = '';
keyValueArray = [];
for (let index = 0; index < dataArray.length; index++) {
if (dataArray[index].indexOf(': ') < 0)
continue;
keyValueArray = dataArray[index].split(': ', 2);
key = keyValueArray[0].replace("'", '');
value = keyValueArray[1];
typeResponse = index === 0 ? key.toLowerCase() : typeResponse;
if (key === 'ActionID') {
const actionIDNumber = _toNumber(value);
dataObject[key] =
actionIDNumber !== undefined &&
_isFinite(actionIDNumber)
? actionIDNumber
: value;
continue;
}
const valueNumber = _toNumber(value);
if (valueNumber !== undefined && _isFinite(valueNumber)) {
value = valueNumber;
}
else if (value && value.indexOf('unknown') >= 0) {
value = null;
}
else if (_isEmpty(value)) {
value = null;
}
else if (value &&
value.toLowerCase().indexOf('s') === 0 &&
value.length === 1) {
value = null;
}
dataObject[key] = value ?? undefined;
}
const request = this.getRequest(dataObject.ActionID);
dataObject.Request = request !== null ? request : undefined;
const actionIDNumber = _toNumber(dataObject.ActionID);
if (actionIDNumber !== undefined && _isFinite(actionIDNumber)) {
this.events.emit('Action_' + actionIDNumber, dataObject);
}
else if (typeof dataObject.ActionID === 'string') {
this.events.emit(dataObject.ActionID, dataObject);
}
switch (typeResponse) {
case 'response':
if (this.debug)
console.log(eAMI_EVENTS.RESPONSE, CRLF, dataObject, CRLF);
this.events.emit(eAMI_EVENTS.RESPONSE, dataObject);
break;
case 'event':
if (this.debug)
console.log(eAMI_EVENTS.EVENTS, CRLF, dataObject, CRLF);
if (dataObject.Event !== undefined &&
dataObject.Event !== null &&
dataObject.Event !== '' &&
this.excludeEvents.indexOf(dataObject.Event) < 0) {
if (this._emitAllEvents) {
this.events.emit(eAMI_EVENTS.EVENTS, dataObject);
}
this.events.emit(dataObject.Event, dataObject);
}
break;
default:
break;
}
}
return dataObject;
}
}
//# sourceMappingURL=e-ami.js.map