├── LICENSE ├── README.md ├── index.js ├── lib ├── ami.js └── utils.js └── package.json /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | ----------- 3 | 4 | Copyright (C) 2012 - 2017 by 5 | Philipp Dunkel 6 | abroweb 7 | Igor Escobar 8 | Tekay 9 | Kofi Hagan 10 | Hugo Chinchilla Carbonell 11 | Nick Mooney 12 | Asp3ctus 13 | Christian Gutierrez 14 | bchavet 15 | Joserwan 16 | Joseph Garrone 17 | 18 | Based on a work Copyright (C) 2010 Brian White , but radically altered thereafter so as to constitute a new work. 19 | 20 | Permission is hereby granted, free of charge, to any person obtaining a copy 21 | of this software and associated documentation files (the "Software"), to deal 22 | in the Software without restriction, including without limitation the rights 23 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 24 | copies of the Software, and to permit persons to whom the Software is 25 | furnished to do so, subject to the following conditions: 26 | 27 | The above copyright notice and this permission notice shall be included in 28 | all copies or substantial portions of the Software. 29 | 30 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 31 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 32 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 33 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 34 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 35 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 36 | THE SOFTWARE. 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Asterisk Manager API 2 | 3 | For a project of mine I needed a low level interface to the Asterisk Manager API. I looked around and found https://github.com/mscdex/node-asterisk . While it was a good starting point, it had too many abstractions for my taste. Which is why I based my version on it an then radically refactored it. In the end there now is very little in common with it. 4 | 5 | So this is basically a different piece of work, but since there is a shared DNA and I got a good start by depending on Brian's work, I feel like giving credit is appropriate. 6 | 7 | ## Install 8 | 9 | ``` 10 | $ npm install asterisk-manager 11 | ``` 12 | 13 | ## Usage 14 | ```javascript 15 | /** 16 | * port: port server 17 | * host: host server 18 | * username: username for authentication 19 | * password: username's password for authentication 20 | * events: this parameter determines whether events are emited. 21 | **/ 22 | var ami = new require('asterisk-manager')('port','host','username','password', true); 23 | 24 | // In case of any connectiviy problems we got you coverd. 25 | ami.keepConnected(); 26 | 27 | // Listen for any/all AMI events. 28 | ami.on('managerevent', function(evt) {}); 29 | 30 | // Listen for specific AMI events. A list of event names can be found at 31 | // https://wiki.asterisk.org/wiki/display/AST/Asterisk+11+AMI+Events 32 | ami.on('hangup', function(evt) {}); 33 | ami.on('confbridgejoin', function(evt) {}); 34 | 35 | // Listen for Action responses. 36 | ami.on('response', function(evt) {}); 37 | 38 | // Perform an AMI Action. A list of actions can be found at 39 | // https://wiki.asterisk.org/wiki/display/AST/Asterisk+11+AMI+Actions 40 | ami.action({ 41 | 'action':'originate', 42 | 'channel':'SIP/myphone', 43 | 'context':'default', 44 | 'exten':1234, 45 | 'priority':1, 46 | 'variable':{ 47 | 'name1':'value1', 48 | 'name2':'value2' 49 | } 50 | }, function(err, res) {}); 51 | ``` 52 | ## Contributors 53 | 54 | * [Philipp Dunkel](https://github.com/pipobscure) 55 | * [Igor Escobar](https://github.com/igorescobar) 56 | * [Tekay](https://github.com/Tekay) 57 | * [Kofi Hagan](https://github.com/kofibentum) 58 | * [Hugo Chinchilla Carbonell](https://github.com/hugochinchilla) 59 | * [Nick Mooney](https://github.com/Gnewt) 60 | * [Asp3ctus](https://github.com/Asp3ctus) 61 | * [Christian Gutierrez](https://github.com/chesstrian) 62 | * [bchavet](https://github.com/bchavet) 63 | * [Joserwan](https://github.com/joserwan) 64 | * [Joseph Garrone](https://github.com/garronej) 65 | 66 | ## License 67 | 68 | MIT License 69 | ----------- 70 | 71 | Copyright (C) 2012 - 2017 by 72 | [Philipp Dunkel](https://github.com/pipobscure) 73 | [abroweb](https://github.com/abroweb) 74 | [Igor Escobar](https://github.com/igorescobar) 75 | [Tekay](https://github.com/Tekay) 76 | [Kofi Hagan](https://github.com/kofibentum) 77 | [Hugo Chinchilla Carbonell](https://github.com/hugochinchilla) 78 | [Nick Mooney](https://github.com/Gnewt) 79 | [Asp3ctus](https://github.com/Asp3ctus) 80 | [Christian Gutierrez](https://github.com/chesstrian) 81 | [bchavet](https://github.com/bchavet) 82 | [Joserwan](https://github.com/joserwan) 83 | [Joseph Garrone](https://github.com/garronej) 84 | 85 | Based on a work Copyright (C) 2010 Brian White , but radically altered thereafter so as to constitute a new work. 86 | 87 | Permission is hereby granted, free of charge, to any person obtaining a copy 88 | of this software and associated documentation files (the "Software"), to deal 89 | in the Software without restriction, including without limitation the rights 90 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 91 | copies of the Software, and to permit persons to whom the Software is 92 | furnished to do so, subject to the following conditions: 93 | 94 | The above copyright notice and this permission notice shall be included in 95 | all copies or substantial portions of the Software. 96 | 97 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 98 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 99 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 100 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 101 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 102 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 103 | THE SOFTWARE. 104 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * NodeJS Asterisk Manager API 3 | * (Based on https://github.com/mscdex/node-asterisk.git) 4 | * But radically altered thereafter so as to constitute a new work. 5 | * 6 | * © See LICENSE file 7 | * 8 | */ 9 | 10 | module.exports = require('./lib/ami.js'); 11 | -------------------------------------------------------------------------------- /lib/ami.js: -------------------------------------------------------------------------------- 1 | /* 2 | * NodeJS Asterisk Manager API 3 | * (Based on https://github.com/mscdex/node-asterisk.git) 4 | * But radically altered thereafter so as to constitute a new work. 5 | * 6 | * © See LICENSE file 7 | * 8 | */ 9 | /* jshint node:true, newcap:false */ 10 | 'use strict'; 11 | var debug = false; 12 | 13 | var EventEmitter = require('events').EventEmitter; 14 | var Net = require('net') 15 | var Utils = require('./utils'); 16 | 17 | var Manager = function Manager(port, host, username, password, events) { 18 | 19 | var obj = {}; 20 | var context = { backoff: 10000 }; 21 | var properties = ['on', 'once', 'addListener', 'removeListener', 'removeAllListeners', 22 | 'listeners', 'setMaxListeners', 'emit']; 23 | 24 | context.emitter = new EventEmitter(); 25 | context.held = []; 26 | 27 | properties.map(function(property){ 28 | Object.defineProperty(obj, property, { 29 | value: context.emitter[property].bind(context.emitter) 30 | }); 31 | }) 32 | 33 | obj.options = { 34 | port: port, 35 | host: host || "", 36 | username: username || "", 37 | password: password || "", 38 | events: events || false 39 | }; 40 | 41 | obj.connect = ManagerConnect.bind(obj, context); 42 | obj.keepConnected = ManagerKeepConnected.bind(obj, context); 43 | obj.login = ManagerLogin.bind(obj, context); 44 | obj.action = ManagerAction.bind(obj, context); 45 | obj.disconnect = ManagerDisconnect.bind(obj, context); 46 | obj.isConnected = ManagerIsConnected.bind(obj, context); 47 | obj.connected = obj.isConnected; 48 | 49 | obj.on('rawevent', ManagerEvent.bind(obj, context)); 50 | obj.on('error', function (err) {}); 51 | obj.on('connect', ManagerResetBackoff.bind(obj, context )); 52 | 53 | if (port){ 54 | obj.connect( 55 | obj.options.port, 56 | obj.options.host, 57 | obj.options.username ? obj.login.bind(obj, obj.options.username, password, events) : undefined 58 | ); 59 | } 60 | 61 | return obj; 62 | }; 63 | 64 | function ManagerConnect(context, port, host, callback) { 65 | callback = Utils.defaultCallback(callback); 66 | 67 | context.connection = (context.connection && (context.connection.readyState != 'closed')) ? context.connection : undefined; 68 | 69 | if (context.connection) 70 | return callback.call(this, null); 71 | 72 | context.authenticated = false; 73 | context.connection = Net.createConnection(port, host); 74 | context.connection.setKeepAlive(true); 75 | context.connection.setNoDelay(true); 76 | context.connection.setEncoding('utf-8'); 77 | context.connection.once('connect', callback.bind(this, null)); 78 | context.connection.on('connect', this.emit.bind(this, 'connect')); 79 | context.connection.on('close', this.emit.bind(this, 'close')); 80 | context.connection.on('end', this.emit.bind(this, 'end')); 81 | context.connection.on('data', ManagerReader.bind(this, context)); 82 | context.connection.on('error', ManagerConnectionError.bind(this, context)); 83 | 84 | } 85 | 86 | function ManagerConnectionError(context, error) { 87 | this.emit('error', error); 88 | if (debug) { 89 | error = String(error.stack).split(/\r?\n/); 90 | var msg = error.shift(); 91 | error = error.map(function(line) { 92 | return ' ↳ ' + line.replace(/^\s*at\s+/, ''); 93 | }); 94 | error.unshift(msg); 95 | error.forEach(function(line) { 96 | process.stderr.write(line + '\n'); 97 | }); 98 | } 99 | } 100 | 101 | function ManagerReader(context, data) { 102 | 103 | context.lines = context.lines || []; 104 | context.leftOver = context.leftOver || ''; 105 | context.leftOver += String(data); 106 | context.lines = context.lines.concat(context.leftOver.split(/\r?\n/)); 107 | context.leftOver = context.lines.pop(); 108 | 109 | var lines = []; 110 | var follow = 0 111 | var item = {}; 112 | while (context.lines.length) { 113 | var line = context.lines.shift(); 114 | if (!lines.length && (line.substr(0, 21) === 'Asterisk Call Manager')) { 115 | // Ignore Greeting 116 | } else if (!lines.length && (line.substr(0, 9).toLowerCase() === 'response:') && (line.toLowerCase().indexOf('follow') > -1)) { 117 | follow = 1; 118 | lines.push(line); 119 | } else if (follow && ((line === '--END COMMAND--') || (line === '--END SMS EVENT--'))) { 120 | follow = 2; 121 | lines.push(line); 122 | } else if ((follow > 1) && !line.length) { 123 | follow = 0; 124 | lines.pop(); 125 | item = { 126 | 'response': 'follows', 127 | 'content': lines.join('\n') 128 | }; 129 | 130 | var matches = item.content.match(/actionid: ([^\r\n]+)/i); 131 | item.actionid = matches ? matches[1] : item.actionid; 132 | 133 | lines = []; 134 | this.emit('rawevent', item); 135 | } else if (!follow && !line.length) { 136 | // Have a Complete Item 137 | lines = lines.filter(Utils.stringHasLength); 138 | item = {}; 139 | while (lines.length) { 140 | line = lines.shift(); 141 | line = line.split(': '); 142 | var key = Utils.removeSpaces(line.shift()).toLowerCase(); 143 | line = line.join(': '); 144 | 145 | if (key === 'variable' || key === 'chanvariable') { 146 | 147 | // Handle special case of one or more variables attached to an event and 148 | // create a variables subobject in the event object 149 | if (typeof(item[key]) !== 'object') 150 | item[key] = {}; 151 | line = line.split('='); 152 | var subkey = line.shift(); 153 | item[key][subkey] = line.join('='); 154 | } else { 155 | // Generic case of multiple copies of a key in an event. 156 | // Create an array of values. 157 | if (key in item) { 158 | if (Array.isArray(item[key])) 159 | item[key].push(line); 160 | else 161 | item[key] = [item[key], line]; 162 | } else 163 | item[key] = line; 164 | } 165 | } 166 | context.follow = false; 167 | lines = []; 168 | this.emit('rawevent', item); 169 | } else { 170 | lines.push(line); 171 | } 172 | } 173 | context.lines = lines; 174 | } 175 | 176 | function ManagerEvent(context, event) { 177 | var emits = []; 178 | if (event.response && event.actionid && typeof event.response == "string") { 179 | // This is the response to an Action 180 | emits.push(this.emit.bind(this, event.actionid, (event.response.toLowerCase() == 'error') ? event : undefined, event)); 181 | emits.push(this.emit.bind(this, 'response', event)); 182 | } else if (event.response && event.content) { 183 | // This is a follows response 184 | emits.push(this.emit.bind(this, context.lastid, undefined, event)); 185 | emits.push(this.emit.bind(this, 'response', event)); 186 | } 187 | 188 | if (event.event) { 189 | // This is a Real-Event 190 | event.event = Array.isArray(event.event) ? event.event.shift() : event.event; 191 | event.event += ''; // Make Sure that this is always a string 192 | emits.push(this.emit.bind(this, 'managerevent', event)); 193 | emits.push(this.emit.bind(this, event.event.toLowerCase(), event)); 194 | if (('userevent' === event.event.toLowerCase()) && event.userevent) 195 | emits.push(this.emit.bind(this, 'userevent-' + event.userevent.toLowerCase(), event)); 196 | 197 | } else { 198 | // Ooops I dont know what this is 199 | emits.push(this.emit.bind(this, 'asterisk', event)); 200 | } 201 | emits.forEach(process.nextTick.bind(process)); 202 | } 203 | 204 | function ManagerLogin(context, callback) { 205 | callback = Utils.defaultCallback(callback); 206 | var options = this.options; 207 | 208 | this.action({ 209 | 'action': 'login', 210 | 'username': options.username, 211 | 'secret': options.password, 212 | 'event': options.events ? 'on' : 'off' 213 | }, (function(err) { 214 | if (err) return callback(err); 215 | 216 | process.nextTick(callback.bind(this)); 217 | context.authenticated = true; 218 | 219 | var held = context.held; 220 | context.held = []; 221 | held.forEach((function(held) { 222 | this.action(held.action, held.callback); 223 | }).bind(this)); 224 | 225 | }).bind(this)); 226 | 227 | return; 228 | } 229 | 230 | function ManagerKeepConnected(context) { 231 | if (this.reconnect) return; 232 | if (this.isConnected() === false) { 233 | this.reconnect = ManagerReconnect.bind(this, context); 234 | this.on('close', this.reconnect); 235 | } 236 | } 237 | function ManagerReconnect(context) { 238 | console.log('Trying to reconnect to AMI in '+ (context.backoff / 1000) +' seconds'); 239 | 240 | var connect = this.connect.bind(context, this.options.port, this.options.host, this.login.bind(this)); 241 | setTimeout(connect, context.backoff); 242 | if(context.backoff < 60000){ //The maximum reconection time is 60 seconds 243 | context.backoff += 10000; //Increase reconnection time by 10 seconds 244 | } 245 | } 246 | function ManagerResetBackoff(context) { 247 | context.backoff = 10000; 248 | } 249 | 250 | function MakeManagerAction(req, id) { 251 | var msg = []; 252 | msg.push('ActionID: ' + id); 253 | 254 | Object.keys(req).forEach(function (key) { 255 | var nkey = Utils.removeSpaces(key).toLowerCase(); 256 | if (!nkey.length || ('actionid' == nkey)) 257 | return; 258 | 259 | var nval = req[key]; 260 | 261 | nkey = nkey.substr(0, 1).toUpperCase() + nkey.substr(1); 262 | 263 | switch (typeof nval) { 264 | case 'undefined': 265 | return; 266 | case 'object': 267 | if (!nval) return; 268 | if (nval instanceof Array) { 269 | nval = nval.map(function(e) { 270 | return String(e); 271 | }).join(','); 272 | } else if (nval instanceof RegExp === false) { 273 | Object.keys(nval).forEach( function(name) { 274 | msg.push( nkey + ": " + name + "=" + nval[name].toString() ); 275 | }); 276 | return; 277 | } 278 | break; 279 | default: 280 | nval = String(nval); 281 | break; 282 | } 283 | 284 | msg.push([nkey, nval].join(': ')); 285 | }); 286 | 287 | msg.sort(); 288 | 289 | return msg.join('\r\n') + '\r\n\r\n'; 290 | 291 | } 292 | 293 | function ManagerAction(context, action, callback) { 294 | action = action || {}; 295 | callback = Utils.defaultCallback(callback); 296 | 297 | var id = action.actionid || String((new Date()).getTime()); 298 | 299 | while (this.listeners(id).length) 300 | id += String(Math.floor(Math.random() * 9)); 301 | 302 | if (action.actionid) 303 | delete action.actionid; 304 | 305 | if (!context.authenticated && (action.action !== 'login')) { 306 | context.held = context.held || []; 307 | action.actionid = id; 308 | context.held.push({ 309 | action: action, 310 | callback: callback 311 | }); 312 | return id; 313 | } 314 | 315 | try { 316 | 317 | if (!context.connection) { 318 | throw new Error('There is no connection yet'); 319 | } 320 | 321 | context.connection.write(MakeManagerAction(action, id), 'utf-8'); 322 | } catch (e) { 323 | 324 | console.log('ERROR: ', e); 325 | 326 | context.held = context.held || []; 327 | action.actionid = id; 328 | context.held.push({ 329 | action: action, 330 | callback: callback 331 | }); 332 | 333 | return id; 334 | } 335 | 336 | this.once(id, callback); 337 | 338 | return context.lastid = id; 339 | } 340 | 341 | 342 | function ManagerDisconnect(context, callback) { 343 | 344 | if (this.reconnect) { 345 | this.removeListener('close', this.reconnect); 346 | } 347 | 348 | if (context.connection && context.connection.readyState === 'open') { 349 | context.connection.end(); 350 | } 351 | 352 | delete context.connection; 353 | 354 | if ('function' === typeof callback) { 355 | setImmediate(callback); 356 | } 357 | } 358 | 359 | function ManagerIsConnected(context) { 360 | return (context.connection && context.connection.readyState === 'open'); 361 | } 362 | 363 | // Expose `Manager`. 364 | module.exports = Manager; 365 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | module.exports.stringHasLength = function(line) { 2 | return line && line.length; 3 | }; 4 | 5 | module.exports.defaultCallback = function(callback) { 6 | return 'function' === typeof callback ? callback : function() {}; 7 | }; 8 | 9 | module.exports.removeSpaces = function (string) { 10 | return (string || "").replace(/^\s*|\s*$/g, ''); 11 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "asterisk-manager", 3 | "author": "Philipp Dunkel ", 4 | "maintainers": [ 5 | { 6 | "name": "Igor Escobar", 7 | "email": "blog@igorescobar.com", 8 | "web": "https://github.com/igorescobar" 9 | } 10 | ], 11 | "version": "0.2.0", 12 | "description": "A node.js module for interacting with the Asterisk Manager API.", 13 | "keywords": [ 14 | "asterisk", 15 | "voip", 16 | "ami", 17 | "asterisk-manager" 18 | ], 19 | "main": "index.js", 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/pipobscure/NodeJS-AsteriskManager.git" 23 | } 24 | } 25 | --------------------------------------------------------------------------------