UNPKG

@nethesis/astproxy

Version:

Node.js proxy for Asterisk PBX

2,047 lines (1,850 loc) 314 kB
/** * Core for asterisk 13. * * @module astproxy * @submodule proxy_logic_13 */ /** * This is the asterisk proxy logic linked to version asterisk 13. * * @class proxy_logic_13 * @static */ var fs = require('fs'); var os = require('os'); var path = require('path'); var async = require('async'); var Queue = require('../queue').Queue; var Trunk = require('../trunk').Trunk; var moment = require('moment'); var process = require('process'); var Channel = require('../channel').Channel; var TrunkChannel = require('../trunkChannel').TrunkChannel; var Parking = require('../parking').Parking; var Extension = require('../extension').Extension; var QueueMember = require('../queueMember').QueueMember; var EventEmitter = require('events').EventEmitter; var ParkedCaller = require('../parkedCaller').ParkedCaller; var Conversation = require('../conversation').Conversation; var utilChannel13 = require('./util_channel_13'); var ConfBridgeConfUser = require('../confBridgeConfUser').ConfBridgeConfUser; var ConfBridgeConference = require('../confBridgeConference').ConfBridgeConference; var RECORDING_STATUS = require('../conversation').RECORDING_STATUS; var TrunkConversation = require('../trunkConversation').TrunkConversation; var EXTEN_STATUS_ENUM = require('../extension').EXTEN_STATUS_ENUM; var QueueWaitingCaller = require('../queueWaitingCaller').QueueWaitingCaller; var QUEUE_MEMBER_TYPES_ENUM = require('../queueMember').QUEUE_MEMBER_TYPES_ENUM; /** * The module identifier used by the logger. * * @property IDLOG * @type string * @private * @final * @readOnly * @default [proxy_logic_13] */ var IDLOG = '[proxy_logic_13]'; /** * Fired when the component is ready. * * @event ready */ /** * The name of the ready event. * * @property EVT_READY * @type string * @default "ready" */ var EVT_READY = 'ready'; /** * Fired when the component has been reloaded. * * @event reloaded */ /** * The name of the reloaded event. * * @property EVT_RELOADED * @type string * @default "reloaded" */ var EVT_RELOADED = 'reloaded'; /** * Fired to process when asterisk was restarted. * * @event reloadApp */ /** * The name of the restarted event. * * @property EVT_RESTARTED * @type string * @default "reloadApp" */ var EVT_RELOADAPP = 'reloadApp'; /** * Fired when something changed in an extension. * * @event extenChanged * @param {object} msg The extension object */ /** * The name of the extension changed event. * * @property EVT_EXTEN_CHANGED * @type string * @default "extenChanged" */ var EVT_EXTEN_CHANGED = 'extenChanged'; /** * Fired when dnd status of an extension has been changed. * * @event extenDndChanged * @param {object} msg The event data */ /** * The name of the extension dnd changed event. * * @property EVT_EXTEN_DND_CHANGED * @type string * @default "extenDndChanged" */ var EVT_EXTEN_DND_CHANGED = 'extenDndChanged'; /** * Fired when call forward busy status of an extension has been changed. * * @event extenCfbChanged * @param {object} msg The event data */ /** * The name of the extension cfb changed event. * * @property EVT_EXTEN_CFB_CHANGED * @type string * @default "extenCfbChanged" */ var EVT_EXTEN_CFB_CHANGED = 'extenCfbChanged'; /** * Fired when call forward on unavailable to voicemail status of an extension has been changed. * * @event extenCfuVmChanged * @param {object} msg The event data */ /** * The name of the extension cfuVm changed event. * * @property EVT_EXTEN_CFUVM_CHANGED * @type string * @default "extenCfuVmChanged" */ var EVT_EXTEN_CFUVM_CHANGED = 'extenCfuVmChanged'; /** * Fired when call forward busy to voicemail status of an extension has been changed. * * @event extenCfbVmChanged * @param {object} msg The event data */ /** * The name of the extension cfbVm changed event. * * @property EVT_EXTEN_CFBVM_CHANGED * @type string * @default "extenCfbVmChanged" */ var EVT_EXTEN_CFBVM_CHANGED = 'extenCfbVmChanged'; /** * Fired when call forward on unavailable status of an extension has been changed. * * @event extenCfuChanged * @param {object} msg The event data */ /** * The name of the extension cfu changed event. * * @property EVT_EXTEN_CFU_CHANGED * @type string * @default "extenCfuChanged" */ var EVT_EXTEN_CFU_CHANGED = 'extenCfuChanged'; /** * Fired when call forward status of an extension has been changed. * * @event extenCfChanged * @param {object} msg The event data */ /** * The name of the extension cf changed event. * * @property EVT_EXTEN_CF_CHANGED * @type string * @default "extenCfChanged" */ var EVT_EXTEN_CF_CHANGED = 'extenCfChanged'; /** * Fired when call forward to voicemail status of an extension has been changed. * * @event extenCfVmChanged * @param {object} msg The event data */ /** * The name of the extension cf to voicemail changed event. * * @property EVT_EXTEN_CFVM_CHANGED * @type string * @default "extenCfVmChanged" */ var EVT_EXTEN_CFVM_CHANGED = 'extenCfVmChanged'; /** * Fired when an hangup happened on extension. * * @event hangup * @param {object} msg The extension hangup data object */ /** * The name of the extension hangup event. * * @property EVT_EXTEN_HANGUP * @type string * @default "extenHangup" */ var EVT_EXTEN_HANGUP = 'extenHangup'; /** * Fired when something changed in an queue member. * * @event queueMemberChanged * @param {object} msg The queue member object */ /** * The name of the queue member changed event. * * @property EVT_QUEUE_MEMBER_CHANGED * @type string * @default "queueMemberChanged" */ var EVT_QUEUE_MEMBER_CHANGED = 'queueMemberChanged'; /** * Fired when something changed in a trunk. * * @event trunkChanged * @param {object} msg The trunk object */ /** * The name of the trunk changed event. * * @property EVT_TRUNK_CHANGED * @type string * @default "trunkChanged" */ var EVT_TRUNK_CHANGED = 'trunkChanged'; /** * Fired when an extension ringing. * * @event extenDialing * @param {object} data The caller identity */ /** * The name of the extension dialing event. * * @property EVT_EXTEN_DIALING * @type string * @default "extenDialing" */ var EVT_EXTEN_DIALING = 'extenDialing'; /** * Fired when a new call is incoming from a trunk. * * @event callInByTrunk * @param {object} data The caller number */ /** * The name of the call in by trunk event. * * @property EVT_CALLIN_BY_TRUNK * @type string * @default "callInByTrunk" */ let EVT_CALLIN_BY_TRUNK = 'callInByTrunk'; /** * Fired when an extension is connected in a conversation. * * @event extenConnected * @param {object} data The caller identity */ /** * The name of the extension connected event. * * @property EVT_EXTEN_CONNECTED * @type string * @default "extenConnected" */ var EVT_EXTEN_CONNECTED = 'extenConnected'; /** * Fired when something changed in a parking. * * @event parkingChanged * @param {object} msg The parking object */ /** * The name of the parking changed event. * * @property EVT_PARKING_CHANGED * @type string * @default "parkingChanged" */ var EVT_PARKING_CHANGED = 'parkingChanged'; /** * Fired when something changed in a queue. * * @event queueChanged * @param {object} msg The queue object */ /** * The name of the queue changed event. * * @property EVT_QUEUE_CHANGED * @type string * @default "queueChanged" */ var EVT_QUEUE_CHANGED = 'queueChanged'; /** * Fired when something changed in a ConfBridge conference. * * @event confBridgeConfChanged * @param {object} msg The conference object */ /** * The name of the ConfBridge conference changed event. * * @property EVT_CONFBRIDGE_CONF_CHANGED * @type string * @default "confBridgeConfChanged" */ var EVT_CONFBRIDGE_CONF_CHANGED = 'confBridgeConfChanged'; /** * Fired when something a ConfBridge conference has been ended. * * @event confBridgeConfEnd * @param {string} id The conference identifier */ /** * The name of the ConfBridge conference end event. * * @property EVT_CONFBRIDGE_CONF_END * @type string * @default "confBridgeConfEnd" */ var EVT_CONFBRIDGE_CONF_END = 'confBridgeConfEnd'; /** * Fired when new voicemail message has been left. * * @event newVoiceMessage * @param {object} msg The data about the voicemail, with the number of new and old messages */ /** * The name of the new voicemail event. * * @property EVT_NEW_VOICE_MESSAGE * @type string * @default "newVoiceMessage" */ var EVT_NEW_VOICE_MESSAGE = 'newVoiceMessage'; /** * Fired when new call detail records (cdr) has been logged into the call history. * * @event newCdr * @param {object} msg The call detail records. */ /** * The name of the new call detail records (cdr) event. * * @property EVT_NEW_CDR * @type string * @default "newCdr" */ var EVT_NEW_CDR = 'newCdr'; /** * Something has appen in the voice messages of the voicemail, for example the listen * of a new voice message from the phone. * * @event updateVoiceMessages * @param {object} msg The data about the voicemail */ /** * The name of the update voice messages event. * * @property EVT_UPDATE_VOICE_MESSAGES * @type string * @default "updateVoiceMessages" */ var EVT_UPDATE_VOICE_MESSAGES = 'updateVoiceMessages'; /** * The default base path for the recording call audio file. * * @property BASE_CALL_REC_AUDIO_PATH * @type object * @private * @default "/var/spool/asterisk/monitor" */ var BASE_CALL_REC_AUDIO_PATH = '/var/spool/asterisk/monitor'; /** * The interval time to update the details of all the queues. * * @property INTERVAL_UPDATE_QUEUE_DETAILS * @type number * @private * @final * @default 60000 */ var INTERVAL_UPDATE_QUEUE_DETAILS = 60000; /** * The default asterisk directory path of the alarms. * * @property AST_ALARMS_DIRPATH * @type string * @private * @final * @default "/var/spool/asterisk/outgoing/" */ var AST_ALARMS_DIRPATH = '/var/spool/asterisk/outgoing/'; /** * True if the component has been started. Used to emit EVT_RELOADED * instead of EVT_READY * * @property ready * @type boolean * @private * @default false */ var ready = false; /** * True during component reloading. * * @property reloading * @type boolean * @private * @default false */ var reloading = false; /** * True during component shutdown. * * @property shutdown * @type boolean * @private * @default false */ var shutdown = false; /** * The logger. It must have at least three methods: _info, warn and error._ * * @property logger * @type object * @private * @default console */ var logger = console; /** * The phonebook component. * * @property compPhonebook * @type object * @private */ var compPhonebook; /** * The identifier of the interval used to update queues details. * * @property intervalUpdateQueuesDetails * @type number * @private */ var intervalUpdateQueuesDetails; /** * The caller note component. * * @property compCallerNote * @type object * @private */ var compCallerNote; /** * The event emitter. * * @property emitter * @type object * @private */ var emitter = new EventEmitter(); /** * The asterisk proxy. * * @property astProxy * @type object * @private */ var astProxy; /** * The prefix number to be used in outgoing call. It is not used in * internal calls between extensions. * * @property prefix * @type string * @private * @default "" */ var prefix = ''; /** * The types of the automatic click2call. * * @property C2C_TYPES * @type object * @default { "AUTOMATIC": "", "MANUAL": "", "CLOUD": "" } * @private */ const C2C_TYPES = { AUTOMATIC: 'automatic', MANUAL: 'manual', CLOUD: 'cloud' }; /** * The configured type of automatic click2call. It can be * one of the C2C_TYPES. * * @property c2cMode * @type string * @default C2C_TYPES.AUTOMATIC * @private */ let c2cMode = C2C_TYPES.AUTOMATIC; /** * If the trunks events has to be managed. * * @property trunksEventsEnabled * @type boolean * @default true * @private */ var trunksEventsEnabled = true; /** * The period of time to consider a call as null. * * @property nullCallPeriod * @type number * @default 5 * @private */ var nullCallPeriod = 5; /** * The remote sites phone prefixes. * * @property remoteSitesPrefixes * @type object * @private */ var remoteSitesPrefixes; /** * Contains the information about the caller. The key is the caller * number and the value is the information object. The data are about * the created caller notes and the phonebook contacts from the centralized * and nethcti address book that match on the caller number. The information * are retrieved when a _UserEvent_ is received and are used when _Dialing_ * events occurs. This is because when a call is directed to a queue, only * one _UserEvent_ is emitted and many _Dialing_ events for each members of * the queue. So it executes only one query per call. Due to asynchronous nature * of the query, it may happen that when _Dialing_ event occurs the query is * not completed. In this case the information of the caller are those returned * by the asterisk event. In this manner we give more importance to the speed * rather than to information completeness. * * @property callerIdentityData * @type object * @private * @default {} */ var callerIdentityData = {}; /** * All extensions. The key is the extension number and the value * is the _Extension_ object. * * @property extensions * @type object * @private */ var extensions = {}; /** * Temporarly used for reloading operations. It will become extensions * only on setAllExtensionsUsername method. * * @property tempExtensions * @type object * @private */ let tempExtensions = {}; /** * All mettme conferences. The key is the extension owner number and the value * is the _ConfBridgeConference_ object. * * @property conferences * @type object * @private */ var conferences = {}; /** * Extensions data read from JSON configuration file. * * @property staticDataExtens * @type object * @private */ var staticDataExtens = {}; /** * Mac addresses data read from JSON configuration file. Keys are * mac addresses and the values are the extension identifiers. * * @property macDataByMac * @type object * @private */ let macDataByMac = {}; /** * Mac addresses data read from JSON configuration file. Keys are * extension identifiers and the values are the mac addresses. * * @property macDataByExt * @type object * @private */ let macDataByExt = {}; /** * Trunks data read from JSON configuration file. * * @property staticDataTrunks * @type object * @private */ var staticDataTrunks = {}; /** * Queues data read from JSON configuration file. * * @property staticDataQueues * @type object * @private */ var staticDataQueues = {}; /** * QM alarms notifications status read from JSON configuration file. * * @property QMAlarmsNotificationsStatus * @type boolean * @private */ var QMAlarmsNotificationsStatus; /** * Feature codes of asterisk, e.g. the pickup code. * * @property featureCodes * @type object * @private */ var featureCodes = {}; /** * Context used for blind transfer. * * @property blindTransferContext * @type string * @private */ var blindTransferContext; /** * All trunks. The key is the trunk number and the value * is the _Trunk_ object. * * @property trunks * @type object * @private */ var trunks = {}; /** * All queues. The key is the queue number and the value * is the _Queue_ object. * * @property queues * @type object * @private */ var queues = {}; /** * All parkings. The key is the parkings number and the value * is the _Parking_ object. * * @property parkings * @type object * @private */ var parkings = {}; /** * It is used to store the parked channels to be used in conjunction * with "listChannels" command plugin to get the number and name * of the parked channels. The key is the parking number and the value * is an object with the parked channel information. * * @property parkedChannels * @type object * @private */ var parkedChannels = {}; /** * Store the recording information about conversations. The key * is the conversation identifier and the value is the status. * The presence of the key means that the conversation is recording, * otherwise not. It's necessary because asterisk has not the recording * information. So, when conversation list is refreshed, it is used to * set recording status to a conversation. * * @property recordingConv * @type {object} * @private */ var recordingConv = {}; /** * These are the key names used into the asterisk structure * file created by the perl script. * * @property INI_STRUCT * @type {object} * @readOnly * @private */ var INI_STRUCT = { TYPE: { PARK: 'parking', EXTEN: 'extension', QUEUE: 'queue', TRUNK: 'trunk', GROUP: 'group' }, TECH: { SIP: 'sip', IAX: 'iax' } }; /** * Sets the logger to be used. * * @method setLogger * @param {object} log The logger object. It must have at least * three methods: _info, warn and error_ as console object. * @static */ function setLogger(log) { try { if (typeof log === 'object' && typeof log.info === 'function' && typeof log.warn === 'function' && typeof log.error === 'function') { logger = log; logger.info(IDLOG, 'new logger has been set'); } else { throw new Error('wrong logger object'); } } catch (err) { logger.error(IDLOG, err.stack); } } /** * Sets the prefix number to be used in all outgoing calls. * It does not to be used with internal calls between extensions. * * @method setPrefix * @param {string} prefix The prefix number. * @static */ function setPrefix(code) { try { // check parameter if (typeof code !== 'string') { throw new Error('wrong prefix type'); } prefix = code; logger.info(IDLOG, 'prefix number has been set to "' + prefix + '"'); } catch (err) { logger.error(IDLOG, err.stack); } } /** * Set the c2c mode to be used. * * @method setC2CMode * @param {string} status The status ("enabled"|"disabled"|"cloud"). * @static */ function setC2CMode(status) { try { if (typeof status !== 'string') { throw new Error('wrong parameters: ' + JSON.stringify(arguments)); } c2cMode = status === 'cloud' ? C2C_TYPES.CLOUD : (status === 'enabled' ? C2C_TYPES.AUTOMATIC : C2C_TYPES.MANUAL); logger.info(IDLOG, 'auto c2c has been set to "' + status + '"'); } catch (err) { logger.error(IDLOG, err.stack); } } /** * Get the configured c2c mode. * * @method getC2CMode * @return {string} The click2call mode. * @static */ function getC2CMode() { try { return c2cMode; } catch (err) { logger.error(IDLOG, err.stack); } } /** * Check if c2cmode is set to cloud. * * @method isC2CModeCloud * @return {boolean} True if the c2c mode is set to cloud. * @static */ function isC2CModeCloud() { try { return c2cMode === C2C_TYPES.CLOUD; } catch (err) { logger.error(IDLOG, err.stack); } } /** * Set the period of time to consider a call as null. All calls with * waiting time less than this period is considered null. * * @method setNullCallPeriod * @param {number} period The period of time. * @static */ function setNullCallPeriod(period) { try { if (typeof period !== 'number') { throw new Error('wrong parameters: ' + JSON.stringify(arguments)); } nullCallPeriod = period; logger.info(IDLOG, 'nullCallPeriod has been set to "' + nullCallPeriod + '"'); } catch (err) { logger.error(IDLOG, err.stack); } } /** * Get the period of time to consider a call as null. All calls with * waiting time less than this period is considered null. * * @method getNullCallPeriod * @return {number} The period of time. * @static */ function getNullCallPeriod() { try { return nullCallPeriod; } catch (err) { logger.error(IDLOG, err.stack); } } /** * Return true if the automatic click 2 call has to be used. * * @method isAutoC2CEnabled * @return {boolean} True if the automatic click 2 call has to be used. * @static */ function isAutoC2CEnabled() { try { return c2cMode === C2C_TYPES.AUTOMATIC; } catch (err) { logger.error(IDLOG, err.stack); } } /** * Sets the remote sites phone prefixes used to filter the ConfBridge conference members. * * @method setRemoteSitesPrefixes * @param {object} obj The remote sites prefixes data. * @static */ function setRemoteSitesPrefixes(obj) { try { // check parameter if (typeof obj !== 'object') { throw new Error('wrong remote sites object: ' + obj); } remoteSitesPrefixes = obj; logger.info(IDLOG, 'remote sites has been set'); } catch (err) { logger.error(IDLOG, err.stack); } } /** * Returns the code used to start a ConfBridge conference. * * @method getConfBridgeConfCode * @return {object} The code used to start a ConfBridge conference. * @static */ function getConfBridgeConfCode() { try { return featureCodes.confbridge_conf; } catch (err) { logger.error(IDLOG, err.stack); } } /** * Returns the prefix number used in all outgoing calls. * It is not used with internal calls between extensions. * * @method getPrefix * @return {string} prefix The prefix number. * @static */ function getPrefix() { try { return prefix; } catch (err) { logger.error(IDLOG, err.stack); } } /** * Get the initial trunks list read from JSON configuration file. * * @method getStaticDataQueues * @return {object} The queues object read from JSON config file * @static */ function getStaticDataQueues() { try { return staticDataQueues; } catch (err) { logger.error(IDLOG, err.stack); return {}; } } /** * Store the asterisk proxy to visit. * * @method visit * @param {object} ap The asterisk proxy module. */ function visit(ap) { try { // check parameter if (!ap || typeof ap !== 'object') { throw new Error('wrong parameters: ' + JSON.stringify(arguments)); } astProxy = ap; } catch (err) { logger.error(IDLOG, err.stack); } } /** * Validates all sip trunks of the structure ini file and * initialize sip _Trunk_ objects. * * @method sipTrunkStructValidation * @param {object} err The error received from the command * @param {array} resp The response received from the command * @private */ function sipTrunkStructValidation(err, resp) { try { if (err) { logger.error(IDLOG, 'validating sip trunk structure: ' + err.toString()); return; } // creates temporary object used to rapid check the // existence of a trunk into the asterisk var siplist = {}; var i; for (i = 0; i < resp.length; i++) { siplist[resp[i].ext] = ''; } // cycles in all elements of the structure ini file to validate var k; for (k in struct) { // validates all sip trunks if (struct[k].tech === INI_STRUCT.TECH.SIP && struct[k].type === INI_STRUCT.TYPE.TRUNK) { // current trunk of the structure ini file isn't present // into the asterisk. So remove it from the structure ini file if (siplist[struct[k].extension] === undefined) { delete struct[k]; logger.warn(IDLOG, 'inconsistency between ini structure file and asterisk for ' + k); } } } logger.info(IDLOG, 'all sip trunks have been validated'); // initialize all sip trunks as 'Trunk' objects into the 'trunks' property initializeSipTrunk(); } catch (error) { logger.error(IDLOG, error.stack); } } /** * Validates all iax trunks of the structure ini file and * initialize iax _Trunk_ objects. * * @method iaxTrunkStructValidation * @param {object} err The error received from the command * @param {array} resp The response received from the command * @private */ function iaxTrunkStructValidation(err, resp) { try { if (err) { logger.error(IDLOG, 'validating iax trunk structure: ' + err.toString()); return; } // creates temporary object used to rapid check the // existence of a trunk into the asterisk var iaxlist = {}; var i; for (i = 0; i < resp.length; i++) { iaxlist[resp[i].exten] = ''; } // cycles in all elements of the structure ini file to validate var k; for (k in struct) { // validates all iax trunks if (struct[k].tech === INI_STRUCT.TECH.IAX && struct[k].type === INI_STRUCT.TYPE.TRUNK) { // current trunk of the structure ini file isn't present // into the asterisk. So remove it from the structure ini file if (iaxlist[struct[k].extension] === undefined) { delete struct[k]; logger.warn(IDLOG, 'inconsistency between ini structure file and asterisk for ' + k); } } } logger.info(IDLOG, 'all iax trunks have been validated'); // initialize all iax trunks as 'Trunk' objects into the 'trunks' property initializeIaxTrunk(resp); } catch (error) { logger.error(IDLOG, error.stack); } } /** * Validates all iax extensions of the structure ini file and * initialize iax _Extension_ objects. * * @method iaxExtenStructValidation * @param {object} err The error received from the command. * @param {array} resp The response received from the command. * @private */ function iaxExtenStructValidation(err, resp) { try { if (err) { logger.error(IDLOG, 'validating iax extension structure: ' + err.toString()); return; } // creates temporary object used to rapid check the // existence of an extension into the asterisk var i; var iaxlist = {}; for (i = 0; i < resp.length; i++) { iaxlist[resp[i].exten] = ''; } // cycles in all elements of the structure ini file to validate var k; for (k in struct) { // validates all sip extensions if (struct[k].tech === INI_STRUCT.TECH.IAX && struct[k].type === INI_STRUCT.TYPE.EXTEN) { // current extension of the structure ini file isn't present // into the asterisk. So remove it from the structure ini file if (iaxlist[struct[k].extension] === undefined) { delete struct[k]; logger.warn(IDLOG, 'inconsistency between ini structure file and asterisk for ' + k); } } } logger.info(IDLOG, 'all iax extensions have been validated'); // initialize all iax extensions as 'Extension' objects into the 'extensions' object initializeIaxExten(resp); } catch (error) { logger.error(IDLOG, error.stack); } } /** * Set the initial trunks list read from JSON configuration file. * * @method setStaticDataQueues * @param {object} obj The queues object read from JSON config file * @static */ function setStaticDataQueues(obj) { try { staticDataQueues = obj; } catch (err) { logger.error(IDLOG, err.stack); } } /** * Set QM alarms notifications status read from JSON configuration file. * * @method setQMAlarmsNotificationsStatus * @param {boolean} val The EnableNotifications value read from JSON config file * @static */ function setQMAlarmsNotificationsStatus(val) { try { QMAlarmsNotificationsStatus = val; } catch (err) { logger.error(IDLOG, err.stack); } } /** * Return the value of the QM alarms notifications status. * * @method getQMAlarmsNotificationsStatus * @return {boolean} The EnableNotifications status * @static */ function getQMAlarmsNotificationsStatus() { try { return QMAlarmsNotificationsStatus; } catch (err) { logger.error(IDLOG, err.stack); } } /** * Set the context used for blind transfer. * * @method setBlindTransferContext * @param {string} ctx The context for blind transfer * @static */ function setBlindTransferContext(ctx) { try { blindTransferContext = ctx; } catch (err) { logger.error(IDLOG, err.stack); } } /** * Set the asterisk feature codes read from JSON configuration file. * * @method setFeatureCodes * @param {object} obj The feature codes object read from JSON config file * @static */ function setFeatureCodes(obj) { try { featureCodes = obj; } catch (err) { logger.error(IDLOG, err.stack); } } /** * Return the asterisk code of pickup operation. * * @method getPickupCode * @return {string} The asterisk code of pickup operation. * @static */ function getPickupCode() { try { return featureCodes.pickup; } catch (err) { logger.error(IDLOG, err.stack); } } /** * Set the initial trunks list read from JSON configuration file. * * @method setStaticDataTrunks * @param {object} obj The trunks object read from JSON config file * @static */ function setStaticDataTrunks(obj) { try { staticDataTrunks = obj; } catch (err) { logger.error(IDLOG, err.stack); } } /** * Set the mac addresses read from JSON configuration file. * * @method setMacDataByMac * @param {object} obj The mac address associations. Keys are the mac addresses * @static */ function setMacDataByMac(obj) { try { macDataByMac = obj; } catch (err) { logger.error(IDLOG, err.stack); } } /** * Set the mac addresses read from JSON configuration file. * * @method setMacDataByExt * @param {object} obj The mac address associations. Keys are the extension identifiers * @static */ function setMacDataByExt(obj) { try { macDataByExt = obj; } catch (err) { logger.error(IDLOG, err.stack); } } /** * Get extension from mac address. * * @method getExtenFromMac * @param {string} mac The mac address * @return {string} The extension identifier. */ function getExtenFromMac(mac) { try { return macDataByMac[mac.toLowerCase()]; } catch (err) { logger.error(IDLOG, err.stack); } } /** * Set the extension names read from JSON configuration file. * * @method setStaticDataExtens * @param {object} obj The extension names associations * @static */ function setStaticDataExtens(obj) { try { staticDataExtens = undefined; staticDataExtens = obj; } catch (err) { logger.error(IDLOG, err.stack); } } /** * Reset the component. * * @method reset * @static */ function reset() { try { clearInterval(intervalUpdateQueuesDetails); intervalUpdateQueuesDetails = null; var k; for (k in trunks) { delete trunks[k]; } trunks = {}; for (k in queues) { delete queues[k]; } queues = {}; for (k in parkings) { delete parkings[k]; } parkings = {}; for (k in parkedChannels) { delete parkedChannels[k]; } parkedChannels = {}; macDataByMac = {}; macDataByExt = {}; staticDataTrunks = {}; staticDataQueues = {}; featureCodes = {}; blindTransferContext = undefined; for (k in initializationStatus) { initializationStatus[k] = false; } } catch (err) { logger.error(IDLOG, err.stack); } } /** * Asterisk has been fully booted. * * @method evtFullyBooted * @static */ function evtFullyBooted() { try { logger.info(IDLOG, 'asterisk fully booted'); start(); // reload all components if asterisk was shutted down and restarted if (shutdown) { process.emit(EVT_RELOADAPP) shutdown = false } } catch (err) { logger.error(IDLOG, err.stack); } } var initializationStatus = { pjsipExtens: false, queues: false, parkings: false }; /** * It is called when the asterisk connection is fully booted. * * @method start * @static */ function start() { try { logger.info(IDLOG, 'start'); // initialize pjsip extensions astProxy.doCmd({ command: 'listPjsipPeers' }, initializePjsipExten); if (!reloading) { emitter.on('pjsipExtenInitialized', () => { // initialize queues astProxy.doCmd({ command: 'listQueues' }, initializeQueues); }); } // initialize parkings astProxy.doCmd({ command: 'listParkings' }, initializeParkings); // initialize sip trunks astProxy.doCmd({ command: 'listSipPeers' }, initializeSipTrunk); // initialize pjsip trunks astProxy.doCmd({ command: 'listPjsipPeers' }, initializePjsipTrunk); // initialize all iax trunks astProxy.doCmd({ command: 'listIaxPeers' }, initializeIaxTrunk); // initializes ConfBridge conferences initConfBridgeConf(); // logger.info(IDLOG, 'start asterisk structure ini file validation'); // // validates all sip extensions // astProxy.doCmd({ // command: 'listSipPeers' // }, sipExtenStructValidation); // // validates all iax extensions // astProxy.doCmd({ // command: 'listIaxPeers' // }, iaxExtenStructValidation); // // validates all queues // astProxy.doCmd({ // command: 'listQueues' // }, queueStructValidation); // // validates all parkings // astProxy.doCmd({ // command: 'listParkings' // }, parkStructValidation); // // validates all sip trunks // astProxy.doCmd({ // command: 'listSipPeers' // }, sipTrunkStructValidation); // // validates all iax trunks // astProxy.doCmd({ // command: 'listIaxPeers' // }, iaxTrunkStructValidation); // initializes ConfBridge conferences // initConfBridgeConf(); } catch (err) { logger.error(IDLOG, err.stack); } } /** * Initialize all iax extensions as _Extension_ object into the * _extensions_ property. * * @method initializeIaxExten * @param {object} resp The response of the _listIaxPeers_ command plugin. * @private */ function initializeIaxExten(resp) { try { var i, k, exten; for (k in struct) { if (struct[k].type === INI_STRUCT.TYPE.EXTEN && struct[k].tech === INI_STRUCT.TECH.IAX) { // all iax extensions exten = new Extension(struct[k].extension, struct[k].tech); extensions[exten.getExten()] = exten; extensions[exten.getExten()].setName(struct[k].label); } } // set iax information for (i = 0; i < resp.length; i++) { // this check is because some iax trunks can be present in the resp, // so in this function trunks are not considered if (extensions[resp[i].exten]) { extensions[resp[i].exten].setIp(resp[i].ip); extensions[resp[i].exten].setPort(resp[i].port); logger.info(IDLOG, 'set iax details for ext ' + resp[i].exten); // request the extension status astProxy.doCmd({ command: 'extenStatus', exten: resp[i].exten }, extenStatus); } } // request all channels logger.info(IDLOG, 'requests the channel list to initialize iax extensions'); astProxy.doCmd({ command: 'listChannels' }, updateConversationsForAllExten); } catch (err) { logger.error(IDLOG, err.stack); } } /** * Sets the details for all iax extension object. * * @method listIaxPeers * @private */ function listIaxPeers(resp) { try { // check parameter if (!resp) { throw new Error('wrong parameters: ' + JSON.stringify(arguments)); } var i; for (i = 0; i < resp.length; i++) { extensions[resp[i].ext].setIp(resp[i].ip); extensions[resp[i].ext].setPort(resp[i].port); logger.info(IDLOG, 'set iax details for ext ' + resp[i].ext); // request the extension status astProxy.doCmd({ command: 'extenStatus', exten: resp[i].ext }, extenStatus); } // request all channels logger.info(IDLOG, 'requests the channel list to initialize iax extensions'); astProxy.doCmd({ command: 'listChannels' }, updateConversationsForAllExten); } catch (err) { logger.error(IDLOG, err.stack); } } /** * Initialize all parkings as _Parking_ object into the _parkings_ property. * * @method initializeParkings * @param {object} err The error received from the command * @param {object} resp The response received from the command * @private */ function initializeParkings(err, resp) { try { if (err) { logger.error(IDLOG, 'initialing parkings: ' + err); return; } var pid, p; for (pid in resp) { // new parking object p = new Parking(pid); p.setName(pid); p.setTimeout(resp[pid].timeout); // store it parkings[p.getParking()] = p; } // request all parked channels astProxy.doCmd({ command: 'listParkedCalls' }, listParkedCalls); } catch (error) { logger.error(IDLOG, error.stack); } } /** * Add the name of the parked to the response received from "listParkedCalls" * command plugin. * * @method addParkeeNames * @param {object} resp The reponse object received from the "listParkedCalls" command plugin * @private */ function addParkeeNames(resp) { try { var p; for (p in resp) { resp[p].parkeeName = extensions[resp[p].parkeeNum] ? extensions[resp[p].parkeeNum].getName() : ''; } return resp; } catch (error) { logger.error(IDLOG, error.stack); return resp; } } /** * Store parked channels in memory and launch "listChannel" command plugin * to get the number and the name of each parked channels. * * @method listParkedCalls * @param {object} err The error object received from the "listParkedCalls" command plugin * @param {object} resp The reponse object received from the "listParkedCalls" command plugin * @private */ function listParkedCalls(err, resp) { try { if (err) { logger.error(IDLOG, 'listing parked calls: ' + err.toString()); return; } resp = addParkeeNames(resp); // check the parameter if (typeof resp !== 'object') { throw new Error('wrong parameters: ' + JSON.stringify(arguments)); } // store parked channels in global variable "parkedChannels" parkedChannels = resp; // request all channels to get the caller number information for each parked channel astProxy.doCmd({ command: 'listChannels' }, updateParkedCallerForAllParkings); } catch (error) { logger.error(IDLOG, error.stack); } } /** * Updates specified parking key of the _parkedChannels_ property with the * object received from _listParkedCalls_ command plugin. * * @method updateParkedChannelOfOneParking * @param {object} err The error object received from _listParkedCalls_ command plugin * @param {object} resp The response object received from _listParkedCalls_ command plugin * @param {string} parking The parking identifier * @private */ function updateParkedChannelOfOneParking(err, resp, parking) { try { if (err) { logger.error(IDLOG, 'updating parked channels of one parking ' + parking + ': ' + err.toString()); return; } // check the parameters if (typeof resp !== 'object' || typeof parking !== 'string') { throw new Error('wrong parameters: ' + JSON.stringify(arguments)); } resp = addParkeeNames(resp); // check if the response contains a parked channel for the specified parking // If it is not present, the parking is free if (typeof resp[parking] === 'object') { // update the parked channel of the parking parkedChannels[parking] = resp[parking]; // request all channels to get the caller number information of // the parked channel of the specified parking logger.info(IDLOG, 'request all channels to update parked caller information for parking ' + parking); astProxy.doCmd({ command: 'listChannels' }, function (err, resp) { // update the parked caller of one parking in "parkings" object list updateParkedCallerOfOneParking(err, resp, parking); }); } else { // there is not a parked caller for the parking, so // remove the parked channel from the memory delete parkedChannels[parking]; logger.info(IDLOG, 'removed parked channel from parkedChannels for parking ' + parking); // remove the parked caller from the parking object parkings[parking].removeParkedCaller(); logger.info(IDLOG, 'removed parked caller from parking ' + parking); // emit the event logger.info(IDLOG, 'emit event ' + EVT_PARKING_CHANGED + ' for parking ' + parking); astProxy.emit(EVT_PARKING_CHANGED, parkings[parking]); } } catch (error) { logger.error(IDLOG, error.stack); } } /** * Update the parked caller of the specified parking. * * @method updateParkedCallerOfOneParking * @param {object} err The error received from the _listChannels_ command plugin * @param {object} resp The response received from the _listChannels_ command plugin * @param {string} parking The parking identifier * @private */ function updateParkedCallerOfOneParking(err, resp, parking) { try { if (err) { logger.error(IDLOG, 'updating parked caller of one parking ' + parking + ': ' + err.toString()); return; } // check parameters if (typeof parking !== 'string' || typeof resp !== 'object') { throw new Error('wrong parameters: ' + JSON.stringify(arguments)); } // check if the parking exists, otherwise there is some error if (parkings[parking]) { // get the parked channel of the specified parking var ch = parkedChannels[parking].channel; if (resp[ch]) { // the channel exists // add the caller number information to the response // received from the "listParkedCalls" command plugin parkedChannels[parking].callerNum = resp[ch].callerNum; // add the caller name information for the same reason parkedChannels[parking].callerName = resp[ch].callerName; // create and store a new parked call object pCall = new ParkedCaller(parkedChannels[parking]); parkings[parking].addParkedCaller(pCall); logger.info(IDLOG, 'updated parked call ' + pCall.getNumber() + ' to parking ' + parking); // emit the event logger.info(IDLOG, 'emit event ' + EVT_PARKING_CHANGED + ' for parking ' + parking); astProxy.emit(EVT_PARKING_CHANGED, parkings[parking]); } } else { logger.warn(IDLOG, 'try to update parked caller of the non existent parking ' + parking); } } catch (error) { logger.error(IDLOG, error.stack); } } /** * Updates all parking lots with their relative parked calls, * if they are present. * * @method updateParkedCallerForAllParkings * @param {object} err The error object * @param {object} resp The object received from the "listChannels" command plugin * @private */ function updateParkedCallerForAllParkings(err, resp) { try { if (err) { logger.error(IDLOG, 'updating parked caller for all parkings: ' + err.toString()); return; } // cycle in all channels received from "listChannel" command plugin. // If a channel is present in "parkedChannels", then it is a parked // channel and so add it to relative parking var p, ch, pCall; for (p in parkedChannels) { ch = parkedChannels[p].channel; if (resp[ch]) { // the channel exists // add the caller number information to the response // received from the "listParkedCalls" command plugin parkedChannels[p].callerNum = resp[ch].callerNum; // add the caller name information for the same reason parkedChannels[p].callerName = resp[ch].callerName; // create and store a new parked call object pCall = new ParkedCaller(parkedChannels[p]); parkings[p].addParkedCaller(pCall); logger.info(IDLOG, 'added parked call ' + pCall.getNumber() + ' to parking ' + p); } } if (!reloading) { Object.keys(parkings).forEach(function (p) { logger.info(IDLOG, 'emit event ' + EVT_PARKING_CHANGED + ' for parking ' + p); astProxy.emit(EVT_PARKING_CHANGED, parkings[p]); }); } initializationStatus.parkings = true; checkInitializationStatus(); } catch (error) { logger.error(IDLOG, error.stack); } } /** * Get the details of a queue. * * @method getQueueDetails * @param {string} qid The queue identifier * @return {function} The function to be called by _initializeQueues_. * @private */ function getQueueDetails(qid) { return function (callback) { astProxy.doCmd({ command: 'queueDetails', queue: qid }, function (err, resp) { queueDetails(err, resp, callback); callback(null); }); }; } /** * Initialize all queues as _Queue_ object into the _queues_ property. * * @method initializeQueues * @param {object} err The error * @param {array} results The queues list * @private */ function initializeQueues(err, results) { try { if (err) { logger.error(IDLOG, err); return; } var arr = []; var k, q; for (k in results) { if (staticDataQueues[results[k].queue]) { q = new Queue(results[k].queue); q.setName(staticDataQueues[results[k].queue].name); // store the new queue object queues[q.getQueue()] = q; arr.push(getQueueDetails(q.getQueue())); } } async.series(arr, function (err) { if (err) { logger.error(IDLOG, err); } if (!reloading) { results.forEach(function (o) { if (staticDataQueues[o.queue]) { logger.info(IDLOG, 'emit event ' + EVT_QUEUE_CHANGED + ' for queue ' + o.queue); astProxy.emit(EVT_QUEUE_CHANGED, queues[o.queue]); } }); } initializationStatus.queues = true; checkInitializationStatus(); } ); logger.info(IDLOG, 'start the interval period to update the details of all the queues each ' + INTERVAL_UPDATE_QUEUE_DETAILS + ' msec'); startIntervalUpdateQueuesDetails(INTERVAL_UPDATE_QUEUE_DETAILS); } catch (error) { logger.error(IDLOG, error.stack); } } /** * Updates the data about all queues each interval of time. * * @method startIntervalUpdateQueuesDetails * @param {number} interval The interval time to update the details of all the queues * @private */ function startIntervalUpdateQueuesDetails(interval) { try { if (typeof interval !== 'number') { throw new Error('wrong parameters: ' + JSON.stringify(arguments)); } intervalUpdateQueuesDetails = setInterval(function () { var q; for (q in queues) { logger.info(IDLOG, 'update details of queue ' + q); astProxy.doCmd({ command: 'queueDetails', queue: q }, queueDetailsUpdate); } }, interval); } catch (err) { logger.error(IDLOG, err.stack); } } /** * Updates the details for the queue object. * * @method queueDetailsUpdate * @param {object} err The error response object * @param {object} resp The queue information object * @private */ function queueDetailsUpdate(err, resp) { try { if (err) { logger.error(IDLOG, 'updating queue details: ' + err.toString()); return; } // check the parameter if (typeof resp !== 'object' || resp.queue === undefined || resp.members === undefined || resp.holdtime === undefined || resp.talktime === undefined || resp.completedCallsCount === undefined || resp.abandonedCallsCount === undefined || resp.serviceLevelTimePeriod === undefined || resp.serviceLevelPercentage === undefined) { throw new Error('wrong parameters: ' + JSON.stringify(arguments)); } var q = resp.queue; // the queue number // check the existence of the queue if (!queues[q]) { logger.warn(IDLOG, 'try to update details of not existent queue "' + q + '"'); return; } // update the queue data setQueueData(q, resp); // emit the event logger.info(IDLOG, 'emit event ' + EVT_QUEUE_CHANGED + ' for queue ' + q); astProxy.emit(EVT_QUEUE_CHANGED, queues[q]); } catch (error) { logger.error(IDLOG, error.stack); } } /** * Sets the data for the queue object. * * @method setQueueData * @param {string} q The queue name * @param {object} resp The queue information object * @private */ function setQueueData(q, resp) { try { // check the parameter if (typeof q !== 'string' || typeof resp !== 'object' || resp.holdtime === undefined || resp.talktime === undefined || resp.completedCallsCount === undefined || resp.abandonedCallsCount === undefined || resp.serviceLevelTimePeriod === undefined || resp.serviceLevelPercentage === undefined) { throw new Error('wrong parameters: ' + JSON.stringify(arguments)); } queues[q].setAvgHoldTime(resp.holdtime); queues[q].setAvgTalkTime(resp.talktime); queues[q].setCompletedCallsCount(resp.completedCallsCount); queues[q].setAbandonedCallsCount(resp.abandonedCallsCount); queues[q].setServiceLevelTimePeriod(resp.serviceLevelTimePeriod); queues[q].setServiceLevelPercentage(resp.serviceLevelPercentage); } catch (error) { logger.error(IDLOG, error.stack); } } /** * Updates waiting callers of the queue object. * * @method updateQueueWaitingCallers * @param {object} err The error response object * @param {object} resp The queue information object * @private */ function updateQueueWaitingCallers(err, resp) { try { if (err) { logger.error(IDLOG, 'updating queue waiting callers: ' + err.toString()); return; } // check the parameter if (typeof resp !== 'object' || resp.queue === undefined) { throw new Error('wrong parameters: ' + JSON.stringify(arguments)); } var q = resp.queue; // the queue number // check the existence of the queue if (!queues[q]) { logger.warn(IDLOG, 'try to update queue waiting callers of the queue "' + q + '"'); return; } logger.info(IDLOG, 'update all waiting callers of queue "' + q + '"'); // remove all cur