├── .eslintrc.yml ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.js ├── lib ├── action-specifications.js ├── action.js ├── client.js ├── constants.js ├── create-action-sub-class.js ├── event.js ├── get-id.js ├── index.js ├── loggers │ ├── logger.js │ └── silent-logger.js ├── message.js ├── response-formats │ └── queue-message.js └── response.js ├── package-lock.json ├── package.json └── test ├── action.js ├── client.js ├── create-action-sub-class.js ├── event.js ├── index.js ├── logger.js ├── message.js └── response.js /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | extends: 2 | - airbnb-base 3 | - prettier 4 | 5 | plugins: 6 | - prettier 7 | - eslint-comments 8 | 9 | env: 10 | es6: true 11 | node: true 12 | 13 | rules: 14 | prettier/prettier: 15 | - error 16 | - printWidth: 80 17 | tabWidth: 2 18 | semi: true 19 | trailingComma: es5 20 | bracketSpacing: true 21 | jsxBracketSameLine: false 22 | singleQuote: true 23 | 24 | no-plusplus: off 25 | consistent-return: off 26 | no-underscore-dangle: off 27 | no-param-reassign: off 28 | func-names: off 29 | linebreak-style: [off] 30 | no-implicit-coercion: error 31 | padding-line-between-statements: [ 32 | error, 33 | { blankLine: always, prev: [const, let, var], next: "*" }, 34 | { blankLine: always, prev: "*", next: [const, let, var] }, 35 | { blankLine: any, prev: [const, let, var], next: [const, let, var] }, 36 | { blankLine: always, prev: "*", next: return }, 37 | { blankLine: always, prev: "*", next: [case, default] }, 38 | { blankLine: always, prev: [const, let, var, block, block-like], next: [block, block-like] }, 39 | { blankLine: always, prev: directive, next: "*" }, 40 | { blankLine: any, prev: directive, next: directive }, 41 | ] 42 | 43 | import/no-named-as-default: off 44 | import/newline-after-import: error 45 | 46 | eslint-comments/disable-enable-pair: error 47 | eslint-comments/no-duplicate-disable: error 48 | eslint-comments/no-unlimited-disable: error 49 | eslint-comments/no-unused-disable: warn 50 | eslint-comments/no-unused-enable: warn 51 | 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | .nyc_output 29 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### JetBrains template 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio 4 | 5 | *.iml 6 | 7 | ## Directory-based project format: 8 | .idea/ 9 | # if you remove the above rule, at least ignore the following: 10 | 11 | # User-specific stuff: 12 | # .idea/workspace.xml 13 | # .idea/tasks.xml 14 | # .idea/dictionaries 15 | 16 | # Sensitive or high-churn files: 17 | # .idea/dataSources.ids 18 | # .idea/dataSources.xml 19 | # .idea/sqlDataSources.xml 20 | # .idea/dynamic.xml 21 | # .idea/uiDesigner.xml 22 | 23 | # Gradle: 24 | # .idea/gradle.xml 25 | # .idea/libraries 26 | 27 | # Mongo Explorer plugin: 28 | # .idea/mongoSettings.xml 29 | 30 | ## File-based project format: 31 | *.ipr 32 | *.iws 33 | 34 | ## Plugin-specific files: 35 | 36 | # IntelliJ 37 | /out/ 38 | 39 | # mpeltonen/sbt-idea plugin 40 | .idea_modules/ 41 | 42 | # JIRA plugin 43 | atlassian-ide-plugin.xml 44 | 45 | # Crashlytics plugin (for Android Studio and IntelliJ) 46 | com_crashlytics_export_strings.xml 47 | crashlytics.properties 48 | crashlytics-build.properties 49 | ### Node template 50 | # Logs 51 | logs 52 | *.log 53 | npm-debug.log* 54 | 55 | # Runtime data 56 | pids 57 | *.pid 58 | *.seed 59 | 60 | # Directory for instrumented libs generated by jscoverage/JSCover 61 | lib-cov 62 | 63 | # Coverage directory used by tools like istanbul 64 | coverage 65 | 66 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 67 | .grunt 68 | 69 | # node-waf configuration 70 | .lock-wscript 71 | 72 | # Compiled binary addons (http://nodejs.org/api/addons.html) 73 | build/Release 74 | 75 | # Dependency directory 76 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 77 | node_modules 78 | 79 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10.0.0" 4 | - "10" 5 | - "11" 6 | - "12.0.0" 7 | - "12" 8 | - "13" 9 | script: npm test 10 | after_script: npm run coveralls 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2.0.0 2 | * Change AmiIo.Client#shouldReconnect -> AmiIo.Client#reconnectable 3 | 4 | #### Breaking changes 5 | * Drop nodejs v4.x support. 6 | * Remove QueueUnpause action. Use QueuePause instead, please. 7 | ```js 8 | // Old 9 | const action = new Action.QueueUnpause(...args); 10 | // New 11 | const action = new Action.QueuePause(); 12 | action.Paused = false; 13 | // or 14 | const action = new Action.QueuePause({ Paused: false }); 15 | ``` 16 | * InterLoggers throw if called without `new`. 17 | 18 | 19 | ## 1.2.1 20 | Add `encoding` into config to allow replace default `ascii` encoding with `utf8`. 21 | 22 | ### 1.2.0 23 | - Add BridgeInfo, BridgeList, SIPpeerstatus actions. ([@oxygen](https://github.com/oxygen)) 24 | 25 | ### 1.1.2 26 | Update readme to describe work with variables. 27 | 28 | ## 1.1.0 29 | 30 | Add silent logger. 31 | 32 | 33 | ## 1.0.0 34 | * Now channel variables (`ChanVariable*`-like key in event) do not rewrite. 35 | It make an child object, where all key-value pairs are contained. 36 | It can brake some application so it is an major release. 37 | For example: 38 | 39 | ``` 40 | Event: Bridge 41 | Privilege: call,all 42 | Timestamp: 1469112499.321389 43 | Bridgestate: Unlink 44 | Bridgetype: core 45 | Channel1: SIP/with-TSE-LIM2-0000a194 46 | Channel2: Local/5842@from-queue-0000af99;1 47 | Uniqueid1: 1469111531.131272 48 | Uniqueid2: 1469111552.131275 49 | CallerID1: 4959810606 50 | CallerID2: 5836 51 | ChanVariable(SIP/with-TSE-LIM2-0000a194): CDR(linkedid)=1469111531.131272 52 | ChanVariable(SIP/with-TSE-LIM2-0000a194): CDR(dst)=5899 53 | ChanVariable(Local/5842@from-queue-0000af99;1): CDR(linkedid)=1469111531.131272 54 | ChanVariable(Local/5842@from-queue-0000af99;1): CDR(dst)=5842 55 | 56 | 57 | Maps to=> 58 | { 59 | variables: {}, 60 | event: 'Bridge', 61 | privilege: 'call,all', 62 | timestamp: '1469112499.321389', 63 | bridgestate: 'Unlink', 64 | bridgetype: 'core', 65 | channel1: 'SIP/with-TSE-LIM2-0000a194', 66 | channel2: 'Local/5842@from-queue-0000af99;1', 67 | uniqueid1: '1469111531.131272', 68 | uniqueid2: '1469111552.131275', 69 | callerid1: '4959810606', 70 | callerid2: '5836', 71 | 'chanvariable(sip/with_tse_lim2_0000a194)': { 72 | 'cdr(linkedid)': '1469111531.131272', 73 | 'cdr(dst)': '5899' 74 | }, 75 | 'chanvariable(local/5842@from_queue_0000af99;1)': { 76 | 'cdr(linkedid)': '1469111531.131272', 77 | 'cdr(dst)': '5842' 78 | }, 79 | incomingData: [ ... ] 80 | } 81 | 82 | 83 | ``` 84 | 85 | 86 | 87 | 88 | ## 0.2.9 89 | * Fix crash if socket is closed and lib try send action. 90 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Konstantine Petryaev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ami-io - node.js/io.js client for Asterisk AMI. 2 | =========================== 3 | 4 | [![Greenkeeper badge](https://badges.greenkeeper.io/NumminorihSF/ami-io.svg)](https://greenkeeper.io/) 5 | 6 | This is a AMI client. List of available commands is below. 7 | 8 | Install with: 9 | 10 | npm install ami-io 11 | 12 | 13 | ## Usage 14 | 15 | Simple example: 16 | 17 | ```js 18 | 19 | var AmiIo = require("ami-io"), 20 | SilentLogger = new AmiIo.SilentLogger(), //use SilentLogger if you just want remove logs 21 | amiio = AmiIo.createClient(), 22 | amiio2 = new AmiIo.Client({ logger: SilentLogger }); 23 | 24 | //Both of this are similar 25 | 26 | amiio.on('incorrectServer', function () { 27 | amiio.logger.error("Invalid AMI welcome message. Are you sure if this is AMI?"); 28 | process.exit(); 29 | }); 30 | amiio.on('connectionRefused', function(){ 31 | amiio.logger.error("Connection refused."); 32 | process.exit(); 33 | }); 34 | amiio.on('incorrectLogin', function () { 35 | amiio.logger.error("Incorrect login or password."); 36 | process.exit(); 37 | }); 38 | amiio.on('event', function(event){ 39 | amiio.logger.info('event:', event); 40 | }); 41 | amiio.connect(); 42 | amiio.on('connected', function(){ 43 | setTimeout(function(){ 44 | amiio.disconnect(); 45 | amiio.on('disconnected', process.exit()); 46 | },30000); 47 | }); 48 | 49 | ``` 50 | 51 | Used events you can see below. 52 | 53 | ### Standalone run 54 | 55 | You can use `node index.js user password [host[:port]] [-h host] [-p port]` to test lib and watch events on screen. 56 | Also, if you use `-f filePath` parameter on run - before close node, it will try to write array of events to file. 57 | 58 | 59 | # API 60 | 61 | ## Connection Events 62 | 63 | `client` will emit some events about the state of the connection to the AMI. 64 | 65 | ### "connectionRefused" 66 | 67 | `client` will emit `connectionRefused` if server refused connection. 68 | 69 | ### "incorrectServer" 70 | 71 | `client` will emit `incorrectServer` if server, you try connect is not an AMI. 72 | 73 | ### "incorrectLogin" 74 | 75 | `client` will emit `incorrectLogin` if login or password aren't valid. 76 | 77 | ### "connected" 78 | 79 | `client` will emit `connect` after connect to AMI and success authorize. 80 | 81 | ### "disconnected" 82 | 83 | `client` will emit `disconnect` when connection close. 84 | 85 | ## AMI Events 86 | 87 | ### "event" 88 | 89 | `client` will emit `event` when has new event object. All of them should find at 90 | https://wiki.asterisk.org/wiki/display/AST/Asterisk+11+AMI+Events. 91 | 92 | ### "responseEvent" 93 | 94 | `client` will emit `responseEvent` when some response has event as part of itself. 95 | 96 | ### "rawEvent" 97 | 98 | `client` will emit `rawEvent` when has new event object or a part of response object. 99 | Note that use event and rawEvent at the same time is not a good idea. 100 | 101 | ### "rawEvent."+eventName 102 | 103 | `client` will emit `rawEvent.`+eventName when has new event object or a part of response object. 104 | You can find event names at https://wiki.asterisk.org/wiki/display/AST/Asterisk+11+AMI+Events 105 | 106 | 107 | 108 | # Methods 109 | 110 | ## amiio.createClient() 111 | 112 | * `amiio.createClient() = amiio.createClient({port:5038, host:'127.0.0.1', login:'admin', password:'admin', encoding: 'ascii'})` 113 | 114 | If some of object key are undefined - will use default value. 115 | 116 | * `host`: which host amiio should use. Defaults to `127.0.0.1`. 117 | * `port`: which port amiio should use. Defaults to `5038`. 118 | * `login`: Default to `admin`. 119 | * `password`: Default to `admin`. 120 | * `encoding`: which encoding should amiio use to transfer data. Defaults to `ascii`. **Be careful** with changing 121 | encoding to any other value manually, cause in order to AMI's protocol spec, AMI use ASCII. 122 | 123 | 124 | ## client.connect([shouldReconnect[, reconnectTimeout]]) 125 | 126 | When connecting to AMI servers you can use: `client.connect(true)` to create connection with auto-reconnect. 127 | Auto-reconnect works only if auth was success. If you use `client.disconnect()` connection will close and 128 | shouldn't be any reconnect. 129 | If use `client.connect()` reconnect will not work. 130 | 131 | Default reconnect timeout is 5000ms. 132 | 133 | Also you may want to set timeout of reconnecting. Then use `client.connect(true, timeoutInMs)`. 134 | You don't need to set up timeout for every time you connect (in one client object). After `client.disconnect()` 135 | timeout will not be set to default, so you can use `client.connect(true)` to connect again with similar timeout. 136 | 137 | 138 | ## client.disconnect() 139 | 140 | Forcibly close the connection to the AMI server. Also stop reconnecting. 141 | 142 | 143 | ```js 144 | var amiio = require("ami-io"), 145 | client = amiio.createClient(); 146 | 147 | client.connect(); 148 | //Some code here 149 | client.disconnect(); 150 | ``` 151 | 152 | 153 | ## client.unref() 154 | 155 | Call `unref()` on the underlying socket connection to the AMI server, 156 | allowing the program to exit once no more commands are pending. 157 | 158 | ```js 159 | var AmiIo = require("ami-io"); 160 | var client = AmiIo.createClient(); 161 | 162 | /* 163 | Calling unref() will allow this program to exit immediately after the get command finishes. 164 | Otherwise the client would hang as long as the client-server connection is alive. 165 | */ 166 | client.unref(); 167 | //will close process if only AmiIo is in it. 168 | client.connect(); 169 | ``` 170 | 171 | ## client.ref() 172 | 173 | Call `ref()` will cancel `unref()` effect. 174 | 175 | ## client.useLogger 176 | 177 | Use `client.useLogger(LoggerObject)` if you want to use some another logger. 178 | By default use console and ignore any logging levels. 179 | 180 | ```js 181 | var AmiIo = require("ami-io"); 182 | var client = AmiIo.createClient(); 183 | var client.useLogger(logger); 184 | ``` 185 | 186 | logger should has `trace`,`debug`,`info`,`warn`,`error`,`fatal` methods. 187 | Of course you can emulate them if some lib has not it. 188 | 189 | 190 | # Extras 191 | 192 | Some other things you might like to know about. 193 | 194 | ## client.connected 195 | 196 | `true` if client is connected of `false` if it is not. 197 | 198 | ## client.reconnectionTimeout 199 | 200 | Timeout for reconnect. If you didn't want reconnect ever, `client.reconnectionTimeout == undefined`. 201 | 202 | ## client.shouldReconnect 203 | 204 | `true` if will be reconnect, or `false` if will not. 205 | 206 | # Send action to AMI 207 | 208 | Available actions: 209 | 210 | * AGI 211 | * AbsoluteTimeout 212 | * AgentLogoff 213 | * Agents 214 | * AttendedTransfer 215 | * BlindTransfer 216 | * Bridge 217 | * ChangeMonitor 218 | * Command 219 | * ConfbridgeKick 220 | * ConfbridgeList 221 | * ConfbridgeListRooms 222 | * ConfbridgeLock 223 | * ConfbridgeMute 224 | * ConfbridgeUnlock 225 | * ConfbridgeUnmute 226 | * CoreSettings 227 | * CoreShowChannels 228 | * CoreStatus 229 | * CreateConfig 230 | * DahdiDialOffHook 231 | * DahdiDndOff 232 | * DahdiDndOn 233 | * DahdiHangup 234 | * DahdiRestart 235 | * DahdiShowChannels 236 | * DbDel 237 | * DbDeltree 238 | * DbGet 239 | * DbPut 240 | * ExtensionState 241 | * GetConfig 242 | * GetConfigJson 243 | * GetVar 244 | * Hangup 245 | * JabberSend 246 | * ListCategories 247 | * ListCommands 248 | * LocalOptimizeAway 249 | * Login 250 | * Logoff 251 | * MailboxCount 252 | * MailboxStatus 253 | * MeetmeList 254 | * MeetmeMute 255 | * MeetmeUnmute 256 | * ModuleCheck 257 | * ModuleLoad 258 | * ModuleReload 259 | * ModuleUnload 260 | * Monitor 261 | * Originate 262 | * Park 263 | * ParkedCalls 264 | * PauseMonitor 265 | * Ping 266 | * PlayDtmf 267 | * QueueAdd 268 | * QueueLog 269 | * QueuePause 270 | * QueueRemove 271 | * QueueRule 272 | * QueueStatus 273 | * QueueSummary 274 | * QueueUnpause 275 | * Queues 276 | * Redirect 277 | * Reload 278 | * SendText 279 | * SetVar 280 | * ShowDialPlan 281 | * SipPeers 282 | * SipQualifyPeer 283 | * SipShowPeer 284 | * SipShowRegistry 285 | * Status 286 | * StopMonitor 287 | * UnpauseMonitor 288 | * VoicemailUsersList 289 | * SIPpeerstatus 290 | * BridgeList 291 | * BridgeInfo 292 | 293 | Description of all commands and variables they need, you can find at 294 | https://wiki.asterisk.org/wiki/display/AST/Asterisk+11+AMI+Actions 295 | All values, needed in commands, should passed like this: 296 | 297 | ```js 298 | var action = new amiio.Action.QueueSummary(); 299 | action.queue = "some queue's name"; 300 | amiioClient.send(action, function(err, data){ 301 | if (err){ 302 | //in current time - may be without error. need test 303 | //err === null if ami response match(/success/i), else response will pass as error 304 | } 305 | }); 306 | ``` 307 | 308 | ### Custom Action 309 | If there is not action you need inside available list, you may build action manual 310 | and set all variables and fields by yourself. For example: 311 | ```js 312 | var action = new amiio.Action.Action('MuteAudio'); 313 | ``` 314 | 315 | ### Action Variables 316 | 317 | If you need send some variables to AMI, use `action.variables` object like this: 318 | 319 | ```js 320 | var action = new amiio.Action.SomeAction(); 321 | action.variables.VariableA = 1; 322 | action.variables.VariableB = 2; 323 | action.variables.VariableC = 3; 324 | ``` 325 | Or you can do it like this: 326 | ```js 327 | var action = new amiio.Action.SomeAction(); 328 | action.variables = { 329 | VariableA: 1, 330 | VariableB: 2, 331 | VariableC: 3 332 | }; 333 | ``` 334 | 335 | Be sure, that you don't use the same names for different values. 336 | 337 | ## Action.Originate 338 | 339 | Now, you can send OriginateAction with response like OriginateResponse event. 340 | See https://wiki.asterisk.org/wiki/display/AST/Asterisk+13+ManagerEvent_OriginateResponse for description. 341 | 342 | ```js 343 | var action = new amiio.Action.Originate(); 344 | action.Channel = 'sip/123'; 345 | action.Context = 'default'; 346 | action.Exten = '456'; 347 | action.Priority = 1; 348 | action.Async = true; 349 | action.WaitEvent = true; 350 | 351 | amiioClient.send(action, function(err, data){ 352 | if (err){ 353 | //err will be event like OriginateResponse if (#response !== 'Success') 354 | } 355 | else { 356 | //data is event like OriginateResponse if (#response === 'Success') 357 | } 358 | }); 359 | ``` 360 | 361 | ## SilentLogger 362 | 363 | If you want remove logs, you may use `AmiIo.SilentLogger`'s instance as logger. 364 | Just pass it as argument to AmiIo constructor. 365 | 366 | ## LICENSE - "MIT License" 367 | 368 | Copyright (c) 2015 Konstantine Petryaev 369 | 370 | Permission is hereby granted, free of charge, to any person obtaining a copy 371 | of this software and associated documentation files (the "Software"), to deal 372 | in the Software without restriction, including without limitation the rights 373 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 374 | copies of the Software, and to permit persons to whom the Software is 375 | furnished to do so, subject to the following conditions: 376 | 377 | The above copyright notice and this permission notice shall be included in all 378 | copies or substantial portions of the Software. 379 | 380 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 381 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 382 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 383 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 384 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 385 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 386 | SOFTWARE. 387 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2015 (NumminorihSF) Konstantine Petryaev 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | /* eslint-disable no-console */ 25 | const lib = require('./lib'); 26 | 27 | if (module.parent) { 28 | module.exports = lib; 29 | } else { 30 | const config = { 31 | host: process.env.AMI_HOST, 32 | port: process.env.AMI_PORT, 33 | login: process.env.AMI_LOGIN, 34 | password: process.env.AMI_PASSWORD, 35 | encoding: process.env.AMI_ENCODING, 36 | }; 37 | let file = null; 38 | 39 | const args = process.argv.slice(2); 40 | const syntax = 41 | 'Use: [iojs|node] ami-io user password [host[:port]] [-h host] [-p port]\n' + 42 | ' -h host - AMI host (default 127.0.0.1)\n' + 43 | ' -p port - AMI port (default 5038)\n'; 44 | 45 | if ( 46 | args.indexOf('?') !== -1 || 47 | args.indexOf('help') !== -1 || 48 | args.indexOf('--help') !== -1 49 | ) { 50 | console.info(syntax); 51 | process.exit(); 52 | } 53 | 54 | if (args.indeincludes('-f')) { 55 | file = args[args.indexOf('-f') + 1]; 56 | } 57 | 58 | if (args.length < 3) { 59 | console.error(syntax); 60 | process.exit(1); 61 | } 62 | 63 | (function () { 64 | if (args[2].match(/[\w\d.-]+:?\d*/)) { 65 | if (args[2].indexOf(':') !== -1) { 66 | config.host = config.host || args[2].slice(0, args[2].indexOf(':')); 67 | config.port = config.port || args[2].slice(args[2].indexOf(':') + 1); 68 | } else config.host = config.host || args[2]; 69 | } else if (args.indexOf('-h') !== -1) 70 | config.host = config.host || args[args.indexOf('-h') + 1]; 71 | if (args.indexOf('-p') !== -1) 72 | config.port = config.port || args[args.indexOf('-p') + 1]; 73 | })(); 74 | 75 | config.host = config.host || '127.0.0.1'; 76 | config.port = config.port || 5038; 77 | config.login = config.login || args[0] || 'admin'; 78 | config.password = config.password || args[1] || 'password'; 79 | 80 | const amiio = new lib.Client(config); 81 | let count = 0; 82 | const time = Date.now(); 83 | const eventsArray = []; 84 | 85 | if (file) 86 | amiio.on('event', (event) => { 87 | eventsArray.push(event); 88 | }); 89 | 90 | amiio.on('incorrectServer', () => { 91 | amiio.logger.error( 92 | 'Invalid AMI welcome message. Are you sure if this is AMI?' 93 | ); 94 | process.exit(); 95 | }); 96 | amiio.on('connectionRefused', () => { 97 | amiio.logger.error('Connection refused.'); 98 | process.exit(); 99 | }); 100 | amiio.on('incorrectLogin', () => { 101 | amiio.logger.error('Incorrect login or password.'); 102 | process.exit(); 103 | }); 104 | // eslint-disable-next-line no-unused-vars 105 | amiio.on('event', (event) => { 106 | count++; 107 | // events that ami sends by itself 108 | }); 109 | // eslint-disable-next-line no-unused-vars 110 | amiio.on('responseEvent', (event) => { 111 | // events that ami sends as part of responses 112 | }); 113 | // eslint-disable-next-line no-unused-vars 114 | amiio.on('rawEvent', (event) => { 115 | // every event that ami sends (event + responseEvent) 116 | }); 117 | amiio.on('connected', () => { 118 | amiio.send(new lib.Action.Ping(), (err, data) => { 119 | if (err) return amiio.logger.error('PING', err); 120 | 121 | return amiio.logger.info('PING', data); 122 | }); 123 | 124 | amiio.send(new lib.Action.CoreStatus(), (err, data) => { 125 | if (err) return amiio.logger.error(err); 126 | 127 | return amiio.logger.info(data); 128 | }); 129 | 130 | amiio.send(new lib.Action.CoreSettings(), (err, data) => { 131 | if (err) return amiio.logger.error(err); 132 | 133 | return amiio.logger.info(data); 134 | }); 135 | 136 | amiio.send(new lib.Action.Status(), (err, data) => { 137 | if (err) return amiio.logger.error(err); 138 | 139 | return amiio.logger.info(data); 140 | }); 141 | 142 | amiio.send(new lib.Action.ListCommands(), (err, data) => { 143 | if (err) return amiio.logger.error(err); 144 | 145 | return amiio.logger.info(data); 146 | }); 147 | 148 | amiio.send(new lib.Action.QueueStatus(), (err, data) => { 149 | if (err) return amiio.logger.error(err); 150 | 151 | return amiio.logger.info(data); 152 | }); 153 | 154 | amiio.send(new lib.Action.QueueSummary(), (err, data) => { 155 | if (err) return amiio.logger.error(err); 156 | 157 | return amiio.logger.info(data); 158 | }); 159 | 160 | amiio.send(new lib.Action.GetConfig('sip.conf'), (err, data) => { 161 | if (err) return amiio.logger.error(err); 162 | 163 | return amiio.logger.info(data); 164 | }); 165 | 166 | amiio.send(new lib.Action.GetConfigJson('sip.conf'), (err, data) => { 167 | if (err) return amiio.logger.error(err); 168 | 169 | return amiio.logger.info(data); 170 | }); 171 | }); 172 | 173 | process.on('SIGINT', () => { 174 | amiio.disconnect(); 175 | if (file) 176 | // eslint-disable-next-line global-require 177 | require('fs').writeFileSync( 178 | file, 179 | JSON.stringify( 180 | eventsArray.map((v) => { 181 | delete v.incomingData; 182 | 183 | return v; 184 | }), 185 | null, 186 | ' ' 187 | ), 188 | { encoding: 'utf8' } 189 | ); 190 | process.exit(); 191 | }); 192 | 193 | process.on('SIGTERM', () => { 194 | amiio.disconnect(); 195 | if (file) 196 | // eslint-disable-next-line global-require 197 | require('fs').writeFileSync( 198 | file, 199 | JSON.stringify( 200 | eventsArray.map((v) => { 201 | delete v.incomingData; 202 | 203 | return v; 204 | }), 205 | null, 206 | ' ' 207 | ), 208 | { encoding: 'utf8' } 209 | ); 210 | process.exit(); 211 | }); 212 | 213 | setInterval(() => { 214 | console.log(`Count of events: ${count}`); 215 | console.log(`Events in second: ${count / (Date.now() - time)}`); 216 | console.log( 217 | `Mem: ${Math.floor(process.memoryUsage().rss / (1024 * 1024))}` 218 | ); 219 | console.log( 220 | `Heap: ${ 221 | Math.floor( 222 | (process.memoryUsage().heapUsed * 10000) / 223 | process.memoryUsage().heapTotal 224 | ) / 100 225 | }% (${Math.floor(process.memoryUsage().heapTotal / (1024 * 1024))})` 226 | ); 227 | }, 300000); 228 | 229 | amiio.connect(); 230 | } 231 | 232 | /* eslint-enable no-console */ 233 | -------------------------------------------------------------------------------- /lib/action-specifications.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2015 (NumminorihSF) Konstantine Petryaev 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | module.exports = [ 26 | { name: 'Login', params: ['Username', 'Secret'] }, 27 | { name: 'CoreShowChannels', params: [] }, 28 | { name: 'Ping', params: [] }, 29 | { name: 'Hangup', params: [] }, 30 | { name: 'CoreStatus', params: [] }, 31 | { name: 'Status', params: [] }, 32 | { name: 'DahdiShowChannels', params: [] }, 33 | { name: 'CoreSettings', params: [] }, 34 | { name: 'ListCommands', params: [] }, 35 | { name: 'Logoff', params: [] }, 36 | { name: 'AbsoluteTimeout', params: [] }, 37 | { name: 'SipShowPeer', params: [] }, 38 | { name: 'SipShowRegistry', params: [] }, 39 | { name: 'SipQualifyPeer', params: [] }, 40 | { name: 'SipPeers', params: [] }, 41 | { name: 'AgentLogoff', params: [] }, 42 | { name: 'Agents', params: [] }, 43 | { name: 'AttendedTransfer', params: [] }, 44 | { name: 'ChangeMonitor', params: [] }, 45 | { name: 'Command', params: [] }, 46 | { name: 'CreateConfig', params: [] }, 47 | { name: 'DahdiDialOffHook', params: [] }, 48 | { name: 'DahdiDndOff', params: [] }, 49 | { name: 'DahdiDndOn', params: [] }, 50 | { name: 'DahdiHangup', params: [] }, 51 | { name: 'DahdiRestart', params: [] }, 52 | { name: 'DbDel', params: [] }, 53 | { name: 'DbDeltree', params: [] }, 54 | { name: 'DbGet', params: [] }, 55 | { name: 'DbPut', params: [] }, 56 | { name: 'ExtensionState', params: [] }, 57 | { name: 'GetConfig', params: ['Filename'] }, 58 | { name: 'GetConfigJson', params: ['Filename'] }, 59 | { name: 'GetVar', params: [] }, 60 | { name: 'JabberSend', params: [] }, 61 | { name: 'ListCategories', params: [] }, 62 | { name: 'PauseMonitor', params: [] }, 63 | { name: 'UnpauseMonitor', params: [] }, 64 | { name: 'StopMonitor', params: [] }, 65 | { name: 'LocalOptimizeAway', params: [] }, 66 | { name: 'SetVar', params: [] }, 67 | { name: 'Reload', params: [] }, 68 | { name: 'PlayDtmf', params: [] }, 69 | { name: 'Park', params: [] }, 70 | { name: 'ParkedCalls', params: [] }, 71 | { 72 | name: 'Monitor', 73 | params: ['format', 'mix'], 74 | defaults: { format: 'wav', mix: true }, 75 | }, 76 | { name: 'ModuleCheck', params: [] }, 77 | { name: 'ModuleLoad', params: ['LoadType'], defaults: { LoadType: 'Load' } }, 78 | { 79 | name: 'ModuleUnload', 80 | params: ['LoadType'], 81 | defaults: { LoadType: 'Unload' }, 82 | }, 83 | { 84 | name: 'ModuleReload', 85 | params: ['LoadType'], 86 | defaults: { LoadType: 'record' }, 87 | }, 88 | { name: 'MailboxCount', params: [] }, 89 | { name: 'MailboxStatus', params: [] }, 90 | { name: 'VoicemailUsersList', params: [] }, 91 | { name: 'Redirect', params: [] }, 92 | { name: 'Bridge', params: [] }, 93 | { name: 'ShowDialPlan', params: [] }, 94 | { name: 'SendText', params: [] }, 95 | { name: 'Queues', params: [] }, 96 | { 97 | name: 'QueuePause', 98 | params: ['Interface', 'Paused', 'Queue', 'Reason'], 99 | optional: ['Queue', 'Reason'], 100 | defaults: { Paused: true }, 101 | }, 102 | { name: 'QueueSummary', params: [] }, 103 | { name: 'QueueRule', params: [] }, 104 | { name: 'QueueStatus', params: [] }, 105 | { name: 'QueueReset', params: [] }, 106 | { name: 'QueueRemove', params: ['Interface', 'Queue'] }, 107 | { name: 'Originate', params: [] }, 108 | { name: 'QueueAdd', params: ['Interface', 'Queue'] }, 109 | { name: 'QueueLog', params: [] }, 110 | { name: 'MeetmeList', params: ['Conference'], optional: ['Conference'] }, 111 | { name: 'MeetmeMute', params: ['Meetme', 'Usernum'] }, 112 | { name: 'MeetmeUnmute', params: ['Meetme', 'Usernum'] }, 113 | { name: 'ConfbridgeListRooms', params: [] }, 114 | { name: 'ConfbridgeList', params: ['Conference'] }, 115 | { name: 'ConfbridgeKick', params: ['Conference', 'Channel'] }, 116 | { name: 'ConfbridgeLock', params: ['Conference'] }, 117 | { name: 'ConfbridgeUnlock', params: ['Conference'] }, 118 | { name: 'ConfbridgeMute', params: ['Conference', 'Channel'] }, 119 | { name: 'ConfbridgeUnmute', params: ['Conference', 'Channel'] }, 120 | { name: 'AGI', params: ['Channel', 'Command', 'CommandId'] }, 121 | { name: 'BlindTransfer', params: ['Channel', 'Context', 'Extension'] }, 122 | { name: 'SIPpeerstatus' }, 123 | { name: 'BridgeList' }, 124 | { name: 'BridgeInfo' }, 125 | ]; 126 | -------------------------------------------------------------------------------- /lib/action.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2015 (NumminorihSF) Konstantine Petryaev 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | const actionSpecifications = require('./action-specifications.js'); 26 | const createActionSubClass = require('./create-action-sub-class'); 27 | const getId = require('./get-id.js'); 28 | const Message = require('./message.js'); 29 | 30 | const emptyObject = Object.freeze({}); 31 | 32 | class Action extends Message { 33 | static from(params = emptyObject) { 34 | return Object.assign(new Action(params.Action), params); 35 | } 36 | 37 | constructor(name) { 38 | super(); 39 | this.id = getId(); 40 | this.set('ActionID', this.id); 41 | this.set('Action', name); 42 | } 43 | } 44 | 45 | actionSpecifications.forEach((actionSpecification) => { 46 | module.exports[actionSpecification.name] = createActionSubClass( 47 | actionSpecification, 48 | Action 49 | ); 50 | }); 51 | 52 | module.exports.createAction = function createAction(actionSpecification) { 53 | return createActionSubClass(actionSpecification, Action); 54 | }; 55 | 56 | module.exports.Action = Action; 57 | -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2015 (NumminorihSF) Konstantine Petryaev 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | const net = require('net'); 26 | const EventEmitter = require('events'); 27 | 28 | const Logger = require('./loggers/logger.js'); 29 | 30 | const logger = new Logger(); 31 | const { EOM, REGEXP_EOM } = require('./constants.js'); 32 | 33 | const Action = require('./action.js'); 34 | const Response = require('./response.js'); 35 | const AmiEvent = require('./event.js'); 36 | 37 | const CONFIG_DEFAULTS = { 38 | logger, 39 | host: '127.0.0.1', 40 | port: 5038, 41 | login: 'admin', 42 | password: 'admin', 43 | encoding: 'ascii', 44 | }; 45 | 46 | class Client extends EventEmitter { 47 | constructor(config = {}) { 48 | super(); 49 | 50 | this.connected = false; 51 | this.reconnectable = false; 52 | this.config = Object.freeze(Object.assign({}, CONFIG_DEFAULTS, config)); 53 | this.logger = this.config.logger; 54 | this.tailInput = ''; 55 | this.responses = {}; 56 | this.originateResponses = {}; 57 | this.callbacks = {}; 58 | this.unformatWait = {}; 59 | this.follows = {}; 60 | this.version = ''; 61 | 62 | this.addListeners(); 63 | } 64 | 65 | addListeners() { 66 | this.on('connected', () => { 67 | this.connected = true; 68 | }); 69 | this.on('needAuth', (data) => this.auth(data)); 70 | this.on('needParseMessage', (raw) => this.parseMessage(raw)); 71 | this.on('needParseEvent', (eObj) => this.parseEvent(eObj)); 72 | this.on('needParseResponse', (response) => this.parseResponse(response)); 73 | this.on('disconnected', () => { 74 | this.connected = false; 75 | if (this.reconnectable) { 76 | setTimeout(() => { 77 | this.reconnect(this.reconnectable); 78 | }, this.reconnectTimeout); 79 | } 80 | }); 81 | this.on('incorrectLogin', () => { 82 | this.reconnectable = false; 83 | }); 84 | this.on('incorrectServer', () => { 85 | this.reconnectable = false; 86 | }); 87 | } 88 | 89 | connect(reconnectable, reconnectTimeout) { 90 | if (reconnectable) { 91 | this.reconnectable = Boolean(reconnectable); 92 | this.reconnectTimeout = 93 | Number(reconnectTimeout) || this.reconnectTimeout || 5000; 94 | } else this.reconnectable = false; 95 | this.logger.debug('Opening connection'); 96 | this.messagesQueue = []; 97 | this.initSocket(); 98 | } 99 | 100 | initSocket() { 101 | this.logger.trace('Initializing socket'); 102 | 103 | if (this.socket) { 104 | if (!this.socket.destroyed) { 105 | this.socket.end(); 106 | } 107 | this.socket.removeAllListeners(); 108 | } 109 | 110 | this.socket = new net.Socket(); 111 | this.socket.setEncoding(this.config.encoding); 112 | if (this.unrefed) this.socket.unref(); 113 | 114 | this.socket.on('connect', () => { 115 | this.emit('socketConnected'); 116 | }); 117 | 118 | this.socket.on('error', (error) => { 119 | this.logger.debug('Socket error:', error); 120 | if (error.code === 'ECONNREFUSED') this.emit('connectionRefused'); 121 | this.emit('socketError', error); 122 | }); 123 | 124 | this.socket.on('close', (hadError) => { 125 | this.emit('disconnected', hadError); 126 | }); 127 | 128 | this.socket.on('timeout', () => { 129 | this.emit('connectTimeout'); 130 | }); 131 | 132 | this.socket.on('end', () => { 133 | this.emit('connectEnd'); 134 | }); 135 | 136 | this.socket.once('data', (data) => { 137 | this.emit('needAuth', data); 138 | }); 139 | 140 | this.socket.connect(this.config.port, this.config.host); 141 | } 142 | 143 | auth(message) { 144 | this.logger.debug('First message:', message); 145 | if (message.match(/Asterisk Call Manager/)) { 146 | this._setVersion(message); 147 | this.socket.on('data', (data) => { 148 | this.splitMessages(data); 149 | }); 150 | this.send( 151 | new Action.Login(this.config.login, this.config.password), 152 | (error, response) => { 153 | if (error) { 154 | if (error instanceof Response) this.emit('incorrectLogin'); 155 | else this.emit('error', error); 156 | } else if (response && response.response === 'Success') 157 | this.emit('connected'); 158 | else this.emit('incorrectLogin'); 159 | } 160 | ); 161 | } else { 162 | this.emit('incorrectServer', message); 163 | } 164 | } 165 | 166 | splitMessages(data) { 167 | this.logger.trace('Data:', data); 168 | 169 | const buffer = this.tailInput.concat(data.replace(REGEXP_EOM, EOM)); 170 | const messages = buffer.split(EOM); 171 | 172 | this.tailInput = messages.pop(); // If all messages aren't spitted, tailInput = '', 173 | // else tailInput = part of last message and will concat with second part of it 174 | for (let i = 0; i < messages.length; i++) { 175 | (function (message) { 176 | this.emit('needParseMessage', message); 177 | }.bind(this)(messages[i])); 178 | } 179 | } 180 | 181 | parseMessage(raw) { 182 | this.logger.trace('Message:', raw); 183 | if (raw.match(/^Response: /)) { 184 | const response = new Response(raw); 185 | 186 | if (response.actionid) this.responses[response.actionid] = response; 187 | 188 | return this.emit('needParseResponse', response); 189 | } 190 | if (raw.match(/^Event: /)) 191 | return this.emit('needParseEvent', new AmiEvent(raw)); 192 | 193 | return this._parseUnformatMessage(raw); 194 | } 195 | 196 | _parseUnformatMessage(raw) { 197 | const keys = Object.keys(this.unformatWait); 198 | 199 | if (keys.length === 0) 200 | return this.logger.warn('Unexpected: \n<< %s >>', raw); 201 | 202 | const self = this; 203 | 204 | Response.tryFormat({ raw }, (err, data) => { 205 | if (err) return self.logger.warn('Fail fromat:', err); 206 | 207 | if (!self.unformatWait[data.type]) 208 | return self.logger.warn("Doesn't wait:", data.type); 209 | 210 | if ( 211 | !self.unformatWait[data.type].res || 212 | self.unformatWait[data.type].res.length === 0 213 | ) { 214 | if (data.res.length === 1) self.unformatWait[data.type].res = data.res; 215 | else { 216 | self.unformatWait[data.type].res = [ 217 | data.res[1].replace( 218 | '%REPLACE_ACTION_ID%', 219 | self.unformatWait[data.type].id 220 | ), 221 | data.res[0].replace( 222 | '%REPLACE_ACTION_ID%', 223 | self.unformatWait[data.type].id 224 | ), 225 | ]; 226 | } 227 | } else 228 | self.unformatWait[data.type].res.push( 229 | data.res[0].replace( 230 | '%REPLACE_ACTION_ID%', 231 | self.unformatWait[data.type].id 232 | ) 233 | ); 234 | 235 | clearTimeout(self.unformatWait.queues.timeout); 236 | 237 | if (self.unformatWait[data.type].res.length === 1) 238 | return self.emit( 239 | 'needParseMessage', 240 | self.unformatWait[data.type].res[0] 241 | ); 242 | 243 | (self.unformatWait[data.type].timeout = setTimeout(() => { 244 | self.unformatWait[data.type].res.push( 245 | data.res[2].replace( 246 | '%REPLACE_ACTION_ID%', 247 | self.unformatWait[data.type].id 248 | ) 249 | ); 250 | self.unformatWait[data.type].res.forEach((mes) => { 251 | self.emit('needParseMessage', mes); 252 | }); 253 | }, 100)).unref(); 254 | }); 255 | } 256 | 257 | parseEvent(eObj) { 258 | this.logger.debug('AmiEvent:', eObj); 259 | 260 | const id = eObj.actionid; 261 | 262 | if ( 263 | id !== undefined && 264 | this.responses[id] !== undefined && 265 | this.callbacks[id] !== undefined 266 | ) { 267 | this.emit('responseEvent', eObj); 268 | if (!this.responses[id].events) { 269 | this.logger.fatal('No events in this.responses.'); 270 | this.logger.fatal(this.responses[id]); 271 | this.responses[id].events = []; 272 | } 273 | this.responses[id].events.push(eObj); 274 | if (this.originateResponses[id]) { 275 | if (eObj.event === 'OriginateResponse') { 276 | if (eObj.response && eObj.response.match(/Success/i)) { 277 | this.callbacks[id](null, eObj); 278 | } else this.callbacks[id](eObj); 279 | } 280 | } else if ( 281 | eObj.event.indexOf('Complete') !== -1 || 282 | eObj.event.indexOf('DBGetResponse') !== -1 || 283 | (eObj.eventlist && eObj.eventlist.indexOf('Complete') !== -1) 284 | ) { 285 | if ( 286 | this.responses[id].response && 287 | this.responses[id].response.match(/Success/i) 288 | ) { 289 | this.callbacks[id](null, this.responses[id]); 290 | } else this.callbacks[id](this.responses[id]); 291 | } 292 | } else { 293 | this.emit('event', eObj); 294 | // this.emit('amiEvent' + eObj.event, eObj); 295 | } 296 | this.emit('rawEvent', eObj); 297 | this.emit(`rawEvent.${eObj.event}`, eObj); 298 | } 299 | 300 | parseResponse(res) { 301 | this.logger.debug('Response:', res); 302 | 303 | const id = res.actionid; 304 | 305 | if (res.message !== undefined && res.message.indexOf('follow') !== -1) { 306 | this.responses[id] = res; 307 | 308 | return res; 309 | } 310 | 311 | if (this.callbacks[id]) { 312 | if (!this.originateResponses[id]) { 313 | if (res.response && res.response.match(/success/i)) 314 | this.callbacks[id](null, res); 315 | else this.callbacks[id](res); 316 | } 317 | } 318 | } 319 | 320 | reconnect(reconnectable) { 321 | this.reconnectable = reconnectable || false; 322 | this.initSocket(); 323 | } 324 | 325 | disconnect() { 326 | this.reconnectable = false; 327 | this.send(new Action.Logoff(), () => { 328 | this.logger.info('Logged out'); 329 | }); 330 | this.logger.info('Closing connection'); 331 | this.removeAllListeners(); 332 | this.socket.removeAllListeners(); 333 | this.socket.end(); 334 | this.emit('disconnected'); 335 | } 336 | 337 | useLogger(loggerToUse) { 338 | this.logger = loggerToUse; 339 | } 340 | 341 | send(action, callback) { 342 | const self = this; 343 | 344 | this.logger.debug('Send:', action); 345 | if (!this.connected) { 346 | if (action.Action !== 'Login') { 347 | return callback(new Error('Server is disconnected')); 348 | } 349 | } 350 | this.socket.write(action.format(), (err) => { 351 | if (err) { 352 | return callback(err); 353 | } 354 | 355 | let timeout; 356 | 357 | (timeout = setTimeout(() => { 358 | if (self.callbacks[action.ActionID]) 359 | self.callbacks[action.ActionID](new Error('ERRTIMEDOUT')); 360 | }, 300000)).unref(); 361 | 362 | // eslint-disable-next-line no-shadow 363 | this.callbacks[action.ActionID] = function (err, data) { 364 | clearTimeout(timeout); 365 | delete self.callbacks[action.ActionID]; 366 | delete self.responses[action.ActionID]; 367 | delete self.originateResponses[action.ActionID]; 368 | delete self.unformatWait[action.ActionID]; 369 | 370 | return callback(err, data); 371 | }; 372 | 373 | if (action.Action && action.Action.toLowerCase() === 'queues') { 374 | this.unformatWait[action.Action.toLowerCase()] = { 375 | id: action.ActionID, 376 | timeout: null, 377 | res: [], 378 | }; 379 | } else if ( 380 | action.Action && 381 | action.Action.toLowerCase() === 'originate' && 382 | action.WaitEvent 383 | ) { 384 | this.originateResponses[action.ActionID] = {}; 385 | } 386 | this.responses[action.ActionID] = ''; 387 | }); 388 | } 389 | 390 | unref() { 391 | this.unrefed = true; 392 | if (this.socket) this.socket.unref(); 393 | } 394 | 395 | ref() { 396 | delete this.unrefed; 397 | if (this.socket) this.socket.ref(); 398 | } 399 | 400 | _setVersion(version) { 401 | const v = version.match(/Asterisk call manager\/([\d.]*[-\w\d.]*)/i); 402 | 403 | if (v) { 404 | [, this.version] = v; 405 | } 406 | } 407 | 408 | getVersion() { 409 | return this.version; 410 | } 411 | } 412 | 413 | exports.Client = Client; 414 | 415 | exports.createClient = function (config) { 416 | return new Client(config); 417 | }; 418 | -------------------------------------------------------------------------------- /lib/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2015 (NumminorihSF) Konstantine Petryaev 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | exports.EOL = '\r\n'; 26 | exports.EOM = '\r\n\r\n'; 27 | exports.REGEXP_EOM = /\r\n\r\n[\r\n]+/; 28 | -------------------------------------------------------------------------------- /lib/create-action-sub-class.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2015 (NumminorihSF) Konstantine Petryaev 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | /** 26 | * Checks if there are no default values for optional fields. 27 | * @param {Object} defaults - Default values for existed fields. 28 | * @param {Array} optional - Names for optional fields. 29 | * @throws In case of any optional field have default value. 30 | * @private 31 | */ 32 | function ensureIfNoDefaultsForOptional(defaults, optional) { 33 | optional.forEach((fieldName) => { 34 | if (Object.prototype.hasOwnProperty.call(defaults, fieldName)) { 35 | throw new Error(`Unexpected default value for field ${fieldName}.`); 36 | } 37 | }); 38 | } 39 | 40 | /** 41 | * Checks if every optional field exists in regular fields list. 42 | * @param {Array} params - Names for non-optional fields. 43 | * @param {Array} optional - Names for optional fields. 44 | * @throws In case of any optional field missed in regular fields list. 45 | * @private 46 | */ 47 | function ensureIfOptionalExistsInParams(params, optional) { 48 | optional.forEach((fieldName) => { 49 | if (params.includes(fieldName)) return; 50 | 51 | throw new Error(`Did not found optional field ${fieldName} in params.`); 52 | }); 53 | } 54 | 55 | /** 56 | * Transforms array of arguments into object with named arguments. 57 | * @param {Array<*>} args - Arguments passed into action constructor. 58 | * @param {Array} params - List of params' names. 59 | * @returns {Object} Named arguments. 60 | * @private 61 | */ 62 | function getArgsObject(args, params) { 63 | const result = params.reduce((resultLocal, field, i) => { 64 | if (i < args.length) { 65 | resultLocal[field] = args[i]; 66 | } 67 | 68 | return resultLocal; 69 | }, {}); 70 | 71 | return result; 72 | } 73 | 74 | function applyDefaults(self, defaults) { 75 | return Object.assign(self, defaults); 76 | } 77 | 78 | const emptyObject = {}; 79 | const emptyArray = []; 80 | 81 | module.exports = function createActionSubClass( 82 | { 83 | name, 84 | params: paramNames = emptyArray, 85 | optional = emptyArray, 86 | defaults = emptyObject, 87 | }, 88 | ParentClass 89 | ) { 90 | ensureIfNoDefaultsForOptional(defaults, optional); 91 | ensureIfOptionalExistsInParams(paramNames, optional); 92 | 93 | const ResultClass = class extends ParentClass { 94 | static from(...params) { 95 | if (params.length === 0) return new ResultClass(); 96 | if (params[0] === undefined) { 97 | throw new Error("Can't work with passed undefined as the 1st arg"); 98 | } 99 | 100 | if (typeof params[0] === 'object' && params[0] !== null) { 101 | return new ResultClass(params[0]); 102 | } 103 | 104 | throw new Error( 105 | `${name}.from() can be called only without arguments or with named arguments (pass plain object)` 106 | ); 107 | } 108 | 109 | constructor(...args) { 110 | super(name); 111 | 112 | if (args.length === 0) { 113 | // action without passed arguments, will use manual params setup 114 | return applyDefaults(this, defaults); 115 | } 116 | 117 | const [params] = args; 118 | 119 | if (typeof params === 'undefined') { 120 | throw new Error("Can't work with passed undefined as the 1st arg"); 121 | } 122 | 123 | // Backward capability 124 | if (params === null || typeof params !== 'object') { 125 | return new ResultClass(getArgsObject(args, paramNames)); 126 | } 127 | 128 | applyDefaults(this, defaults); 129 | 130 | Object.assign(this, params); 131 | } 132 | }; 133 | 134 | Object.defineProperty(ResultClass, 'name', { value: name }); 135 | 136 | return ResultClass; 137 | }; 138 | -------------------------------------------------------------------------------- /lib/event.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2015 (NumminorihSF) Konstantine Petryaev 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | const Message = require('./message.js'); 25 | 26 | class AmiEvent extends Message { 27 | static from(data) { 28 | return Message.parse(data); 29 | } 30 | 31 | constructor(data) { 32 | super(); 33 | 34 | this.parse(data); 35 | } 36 | } 37 | 38 | module.exports = AmiEvent; 39 | -------------------------------------------------------------------------------- /lib/get-id.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2015 (NumminorihSF) Konstantine Petryaev 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | let currentId = 0; 26 | 27 | module.exports = function getId() { 28 | return currentId++; 29 | }; 30 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2015 (NumminorihSF) Konstantine Petryaev 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | const { Client, createClient } = require('./client'); 26 | const Action = require('./action.js'); 27 | const Response = require('./response.js'); 28 | const Logger = require('./loggers/logger.js'); 29 | const AmiEvent = require('./event.js'); 30 | 31 | const SilentLogger = require('./loggers/silent-logger'); 32 | 33 | exports.createClient = createClient; 34 | 35 | exports.Client = Client; 36 | exports.Action = Action; 37 | exports.Actions = Action; 38 | exports.Event = AmiEvent; 39 | exports.Response = Response; 40 | exports.SilentLogger = SilentLogger; 41 | exports.Logger = Logger; 42 | -------------------------------------------------------------------------------- /lib/loggers/logger.js: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2015 (NumminorihSF) Konstantine Petryaev 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | const LEVELS = ['fatal', 'error', 'warn', 'info', 'debug', 'trace']; 26 | 27 | const priority = LEVELS.reduce( 28 | (accumulator, level, index) => Object.assign(accumulator, { [level]: index }), 29 | {} 30 | ); 31 | 32 | class Logger { 33 | constructor(minimalLogLevel = 'warn') { 34 | this.minimalLogLevel = minimalLogLevel; 35 | } 36 | 37 | shouldSkip(level) { 38 | return priority[level] < priority[this.minimalLogLevel]; 39 | } 40 | 41 | setMinimalLogLevel(minimalLogLevel) { 42 | this.minimalLogLevel = minimalLogLevel; 43 | } 44 | 45 | /* eslint-disable no-console */ 46 | fatal(...rest) { 47 | if (this.shouldSkip('fatal')) return; 48 | console.error(...rest); 49 | } 50 | 51 | error(...rest) { 52 | if (this.shouldSkip('error')) return; 53 | console.error(...rest); 54 | } 55 | 56 | warn(...rest) { 57 | if (this.shouldSkip('warn')) return; 58 | console.warn(...rest); 59 | } 60 | 61 | info(...rest) { 62 | if (this.shouldSkip('info')) return; 63 | console.info(...rest); 64 | } 65 | 66 | debug(...rest) { 67 | if (this.shouldSkip('debug')) return; 68 | console.log(...rest); 69 | } 70 | 71 | trace(...rest) { 72 | if (this.shouldSkip('trace')) return; 73 | console.log(...rest); 74 | } 75 | /* eslint-enable no-console */ 76 | } 77 | 78 | module.exports = Logger; 79 | -------------------------------------------------------------------------------- /lib/loggers/silent-logger.js: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2015 (NumminorihSF) Konstantine Petryaev 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | class SilentLogger { 26 | /* eslint-disable class-methods-use-this */ 27 | fatal() {} 28 | 29 | error() {} 30 | 31 | warn() {} 32 | 33 | info() {} 34 | 35 | debug() {} 36 | 37 | trace() {} 38 | /* eslint-enable class-methods-use-this */ 39 | } 40 | 41 | module.exports = SilentLogger; 42 | -------------------------------------------------------------------------------- /lib/message.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2015 (NumminorihSF) Konstantine Petryaev 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | const { EOL } = require('./constants.js'); 26 | 27 | const FIELDS_TO_SKIP_WHILE_FORMAT = ['variables', 'inputData']; 28 | 29 | class Message { 30 | static parse(rawData) { 31 | const message = new Message(); 32 | 33 | message.parse(rawData); 34 | 35 | return message; 36 | } 37 | 38 | constructor() { 39 | this.variables = {}; 40 | } 41 | 42 | format() { 43 | return `${this.formatFields()}${this.formatVariables()}${EOL}`; 44 | } 45 | 46 | formatFields() { 47 | return Object.keys(this) 48 | .filter((key) => !FIELDS_TO_SKIP_WHILE_FORMAT.includes(key)) 49 | .filter( 50 | (key) => 51 | typeof this[key] !== 'function' && typeof this[key] !== 'undefined' 52 | ) 53 | .reduce( 54 | (accumulator, key) => `${accumulator}${key}: ${this[key]}${EOL}`, 55 | '' 56 | ); 57 | } 58 | 59 | formatVariables() { 60 | return Object.keys(this.variables).reduce( 61 | (accumulator, key) => 62 | `${accumulator}Variable: ${key}=${this.variables[key]}${EOL}`, 63 | '' 64 | ); 65 | } 66 | 67 | parse(rawData) { 68 | const data = rawData.split(EOL); 69 | 70 | let lastVariable = ''; 71 | 72 | for (let i = 0; i < data.length; i++) { 73 | const value = data[i].split(':'); 74 | const keyName = value.shift().toLowerCase().replace(/-/g, '_'); 75 | const keyValue = value.join(':').replace(/(^\s+)|(\s+$)/g, ''); 76 | let subKey; 77 | let subVal; 78 | 79 | if (keyName === 'variable') { 80 | lastVariable = keyValue; 81 | } else if (keyName === 'value') { 82 | this.variables[lastVariable] = keyValue; 83 | lastVariable = ''; 84 | } else if (/^chanvariable/.test(keyName)) { 85 | this[keyName] = this[keyName] || {}; 86 | subVal = keyValue.split('='); 87 | subKey = subVal.shift().toLowerCase(); 88 | this[keyName][subKey] = subVal.join('='); 89 | continue; // eslint-disable-line no-continue 90 | } 91 | this.set(keyName, keyValue); 92 | } 93 | 94 | this.incomingData = data; 95 | } 96 | 97 | set(name, value) { 98 | if (name === 'variables') { 99 | throw new Error( 100 | 'Can\'t set field with name = "variables". Use Message#setVariable(name, value).' 101 | ); 102 | } 103 | this[name] = value; 104 | } 105 | 106 | setVariable(name, value) { 107 | this.variables[name] = value; 108 | } 109 | } 110 | 111 | module.exports = Message; 112 | -------------------------------------------------------------------------------- /lib/response-formats/queue-message.js: -------------------------------------------------------------------------------- 1 | function formatQueueMessage(message, callback) { 2 | const res = []; 3 | const strings = message.raw.split('\r\n'); 4 | 5 | let queue = strings[0].match(/^[\w\d_\-.]* /); 6 | 7 | if (queue && queue[0]) queue = queue[0].trim(); 8 | 9 | let calls = strings[0].match(/has \d* call/); 10 | 11 | if (calls && calls[0]) calls = parseInt(calls[0].substr(3), 10); 12 | 13 | let strategy = strings[0].match(/in '\w*' strategy/); 14 | 15 | if (strategy && strategy[0]) 16 | strategy = strategy[0].replace(/(^in )|'|( strategy$)/g, ''); 17 | 18 | let holdtime = strings[0].match(/\d*s holdtime/); 19 | 20 | if (holdtime && holdtime[0]) holdtime = holdtime[0].replace(/\D*/g, ''); 21 | 22 | let talktime = strings[0].match(/\d*s talktime/); 23 | 24 | if (talktime && talktime[0]) talktime = talktime[0].replace(/\D*/g, ''); 25 | 26 | let w = strings[0].match(/W:[\d.]*,/); 27 | 28 | if (w && w[0]) w = w[0].replace(/[W:,]/g, ''); 29 | 30 | let c = strings[0].match(/C:[\d.]*,/); 31 | 32 | if (c && c[0]) c = c[0].replace(/[C:,]/g, ''); 33 | 34 | let a = strings[0].match(/A:[\d.]*,/); 35 | 36 | if (a && a[0]) a = a[0].replace(/[A:,]/g, ''); 37 | 38 | let sl = strings[0].match(/SL:.*$/); 39 | 40 | if (sl && sl[0]) sl = sl[0].substr(3); 41 | 42 | const members = []; 43 | 44 | let i = 2; 45 | 46 | for (i = 2; i < strings.length; i++) { 47 | if (strings[i].match(/Callers/)) break; 48 | members.push(strings[i].match(/.*? \(.*?\)/)[0].trim()); 49 | } 50 | 51 | const callers = []; 52 | 53 | for (i++; i < strings.length; i++) { 54 | callers.push(strings[i].trim()); 55 | } 56 | 57 | res.push( 58 | `Event: Queues\r\nQueue: ${queue}\r\nMembers: ${members.join(';')}\r\n` + 59 | `Strategy: ${strategy}\r\n` + 60 | `Calls: ${calls}\r\n` + 61 | `Callers: ${callers.join(';')}\r\n` + 62 | `Weight: ${w}\r\n` + 63 | `CallsAnswered: ${c}\r\n` + 64 | `HoldTime: ${holdtime}\r\n` + 65 | `TalkTime: ${talktime}\r\n` + 66 | `CallsAnswered: ${c}\r\n` + 67 | `CallsAbandoned: ${a}\r\n` + 68 | `ServiceLevel: ${sl}\r\n` + 69 | `Strategy: ${strategy}\r\n` + 70 | `ActionID: %REPLACE_ACTION_ID%` 71 | ); 72 | 73 | res.push( 74 | 'Response: Success\r\nActionID: %REPLACE_ACTION_ID%' + 75 | '\r\nMessage: Queues will follow' 76 | ); 77 | res.push('Event: QueuesComplete\r\nActionID: %REPLACE_ACTION_ID%'); 78 | 79 | return callback(null, { type: 'queues', res }); 80 | } 81 | 82 | module.exports = formatQueueMessage; 83 | -------------------------------------------------------------------------------- /lib/response.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2015 (NumminorihSF) Konstantine Petryaev 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in all 14 | * copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | * SOFTWARE. 23 | */ 24 | 25 | const Message = require('./message.js'); 26 | const formatQueueMessage = require('./response-formats/queue-message'); 27 | 28 | const QUEUE_REGEXP = /.* has .* calls .* in .*/i; 29 | 30 | function isQueueMessage(message) { 31 | const { incommingData } = message; 32 | 33 | return QUEUE_REGEXP.test(incommingData[0]); 34 | } 35 | 36 | class Response extends Message { 37 | static tryFormat(message, callback) { 38 | if (isQueueMessage(message)) { 39 | return formatQueueMessage(message, callback); 40 | } 41 | 42 | return callback(new Error('Undefined format')); 43 | } 44 | 45 | constructor(rawData) { 46 | super(); 47 | this.parse(rawData); 48 | this.events = []; 49 | } 50 | } 51 | 52 | module.exports = Response; 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Konstantine Petryaev ", 3 | "name": "ami-io", 4 | "description": "Asterisk Manager Interface client.", 5 | "version": "1.2.1", 6 | "keywords": [ 7 | "asterisk", 8 | "manager", 9 | "interface", 10 | "node", 11 | "iojs", 12 | "ami" 13 | ], 14 | "repository": { 15 | "type": "git", 16 | "url": "git://github.com/NumminorihSF/ami-io.git" 17 | }, 18 | "scripts": { 19 | "lint": "eslint -c .eslintrc.yml ./index.js \"./lib/**/*.js\"", 20 | "lint:fix": "eslint -c .eslintrc.yml ./index.js \"./lib/**/*.js\" --fix", 21 | "test": "nyc mocha --recursive", 22 | "coveralls": "nyc report --reporter=text-lcov | coveralls" 23 | }, 24 | "main": "index.js", 25 | "license": "MIT", 26 | "devDependencies": { 27 | "chai": "^4.2.0", 28 | "coveralls": "3.1.1", 29 | "eslint": "^6.0.0", 30 | "eslint-config-airbnb-base": "^13.1.0", 31 | "eslint-config-prettier": "^5.0.0", 32 | "eslint-plugin-eslint-comments": "^3.1.1", 33 | "eslint-plugin-import": "^2.14.0", 34 | "eslint-plugin-prettier": "^3.1.0", 35 | "mocha": "9.1.3", 36 | "nyc": "15.1.0", 37 | "prettier": "^2.1.2" 38 | }, 39 | "engineStrict": true, 40 | "engines": { 41 | "node": ">=8.0" 42 | }, 43 | "dependencies": {} 44 | } 45 | -------------------------------------------------------------------------------- /test/action.js: -------------------------------------------------------------------------------- 1 | const Message = require('../lib/message.js'); 2 | const Action = require('../lib/action.js'); 3 | const expect = require('chai').expect; 4 | 5 | describe('AmiIo.Action', () => { 6 | describe('#Action', () => { 7 | describe('#constructor()', () => { 8 | it('creates instance of Action', () => { 9 | expect(new Action.Action('')).to.be.instanceOf(Action.Action); 10 | }); 11 | 12 | it('has Message as prototype', () => { 13 | expect(new Action.Action('')).to.be.instanceOf(Message); 14 | }); 15 | 16 | it('sets up numeric id of action', () => { 17 | expect(new Action.Action('').id).to.be.a('number'); 18 | }); 19 | 20 | it('sets up ActionID field to id of action', () => { 21 | const action = new Action.Action(''); 22 | 23 | expect(action.id).to.be.equal(action.ActionID); 24 | }); 25 | 26 | it('sets up Action (name) field to name of action', () => { 27 | const action = new Action.Action('dvskljkljaer'); 28 | 29 | expect(action.Action).to.be.equal('dvskljkljaer'); 30 | }); 31 | }); 32 | }); 33 | 34 | for (const actionName in Action) { 35 | if (actionName === 'Action' || actionName === 'createAction') { 36 | continue; 37 | } 38 | 39 | describe(`#${actionName}()`, () => { 40 | it('do not throw Error', () => { 41 | expect(() => { 42 | new Action[actionName](); 43 | }).to.not.throw(Error); 44 | }); 45 | 46 | it('has Action.Action as prototype', () => { 47 | expect(new Action[actionName]()).to.be.instanceOf(Action.Action); 48 | }); 49 | 50 | it(`create instance of ${actionName}`, () => { 51 | expect(new Action[actionName]()).to.be.instanceOf(Action[actionName]); 52 | }); 53 | 54 | const getVals = function(object) { 55 | return Object.keys(object).reduce((res, val) => { 56 | res.push(object[val]); 57 | 58 | return res; 59 | }, []); 60 | }; 61 | 62 | if (Action[actionName].length > 0) { 63 | it('use 1st arg at state', () => { 64 | expect(getVals(new Action[actionName]('first argument'))).to.include( 65 | 'first argument' 66 | ); 67 | }); 68 | 69 | if (Action[actionName].length > 1) { 70 | it('use 2nd arg at state', () => { 71 | expect( 72 | getVals(new Action[actionName]('first', 'second argument')) 73 | ).to.include('second argument'); 74 | }); 75 | 76 | if (Action[actionName].length > 2) { 77 | it('use 3rd arg at state', () => { 78 | expect( 79 | getVals( 80 | new Action[actionName]('first', 'second', 'third argument') 81 | ) 82 | ).to.include('third argument'); 83 | }); 84 | 85 | if (Action[actionName].length > 3) { 86 | it('use 4th arg at state', () => { 87 | expect( 88 | getVals( 89 | new Action[actionName]( 90 | 'first', 91 | 'second', 92 | 'third', 93 | 'forth state' 94 | ) 95 | ) 96 | ).to.include('forth state'); 97 | }); 98 | } 99 | } 100 | } 101 | } 102 | }); 103 | } 104 | }); 105 | -------------------------------------------------------------------------------- /test/client.js: -------------------------------------------------------------------------------- 1 | const net = require('net'); 2 | const AmiIo = require('../lib/client'); 3 | const expect = require('chai').expect; 4 | 5 | describe('AmiIo.Client', () => { 6 | describe('#constructor()', () => { 7 | it('creates instance of Client', () => { 8 | expect(new AmiIo.Client()).to.be.instanceOf(AmiIo.Client); 9 | }); 10 | 11 | it('has EventEmitter as proto', () => { 12 | expect(new AmiIo.Client()).to.be.instanceOf( 13 | require('events').EventEmitter 14 | ); 15 | }); 16 | 17 | it('use host from config', () => { 18 | expect(new AmiIo.Client({ host: '123' })).to.have.nested.property( 19 | 'config.host', 20 | '123' 21 | ); 22 | }); 23 | 24 | it("use '127.0.0.1' as default host", () => { 25 | expect(new AmiIo.Client()).to.have.nested.property( 26 | 'config.host', 27 | '127.0.0.1' 28 | ); 29 | }); 30 | 31 | it('use port from config', () => { 32 | expect(new AmiIo.Client({ port: 6666 })).to.have.nested.property( 33 | 'config.port', 34 | 6666 35 | ); 36 | }); 37 | 38 | it('use 5038 as default port', () => { 39 | expect(new AmiIo.Client()).to.have.nested.property('config.port', 5038); 40 | }); 41 | 42 | it('use login from config', () => { 43 | expect(new AmiIo.Client({ login: 'trim' })).to.have.nested.property( 44 | 'config.login', 45 | 'trim' 46 | ); 47 | }); 48 | 49 | it("use 'admin' as default login", () => { 50 | expect(new AmiIo.Client()).to.have.nested.property( 51 | 'config.login', 52 | 'admin' 53 | ); 54 | }); 55 | 56 | it('use password from config', () => { 57 | expect(new AmiIo.Client({ password: 'ouch' })).to.have.nested.property( 58 | 'config.password', 59 | 'ouch' 60 | ); 61 | }); 62 | 63 | it("use 'admin' as default password", () => { 64 | expect(new AmiIo.Client()).to.have.nested.property( 65 | 'config.password', 66 | 'admin' 67 | ); 68 | }); 69 | 70 | it("use '' as initial value for version", () => { 71 | expect(new AmiIo.Client()).to.have.property('version', ''); 72 | }); 73 | }); 74 | 75 | describe("Event: 'connected'", () => { 76 | it('sets #connected to true', () => { 77 | const client = new AmiIo.Client(); 78 | 79 | client.emit('connected'); 80 | expect(client.connected).to.be.equal(true); 81 | }); 82 | }); 83 | 84 | describe("Event: 'disconnected'", () => { 85 | it('sets #connected to false', () => { 86 | const client = new AmiIo.Client(); 87 | 88 | client.emit('disconnected'); 89 | expect(client.connected).to.be.equal(false); 90 | }); 91 | 92 | it('calls reconnect function if #reconnectable is true', done => { 93 | const client = new AmiIo.Client(); 94 | let reconnectCalled = false; 95 | 96 | client.reconnectable = true; 97 | client.timeout = 1; 98 | client.reconnect = function() { 99 | reconnectCalled = true; 100 | }; 101 | client.emit('disconnected'); 102 | setTimeout(() => { 103 | expect(reconnectCalled).to.be.equal(true); 104 | done(); 105 | }, 1); 106 | }); 107 | 108 | it('does not call reconnect function if #reconnectable is true', done => { 109 | const client = new AmiIo.Client(); 110 | let called = false; 111 | 112 | client.reconnectable = false; 113 | client.timeout = 1; 114 | client.reconnect = function() { 115 | called = true; 116 | }; 117 | client.emit('disconnected'); 118 | setTimeout(() => { 119 | expect(called).to.be.equal(false); 120 | done(); 121 | }, 20); 122 | }); 123 | }); 124 | 125 | describe("Event: 'needAuth'", () => { 126 | it('call #auth function', () => { 127 | const old = AmiIo.Client.prototype.auth; 128 | const client = new AmiIo.Client(); 129 | let authCalled = false; 130 | 131 | AmiIo.Client.prototype.auth = function() { 132 | authCalled = true; 133 | }; 134 | 135 | client.emit('needAuth'); 136 | expect(authCalled).to.be.equal(true); 137 | AmiIo.Client.prototype.auth = old; 138 | }); 139 | }); 140 | 141 | describe("Event: 'needParseMessage'", () => { 142 | it('call #parseMessage function', () => { 143 | const old = AmiIo.Client.prototype.parseMessage; 144 | const client = new AmiIo.Client(); 145 | let parseMessageCalled = false; 146 | 147 | AmiIo.Client.prototype.parseMessage = function() { 148 | parseMessageCalled = true; 149 | }; 150 | client.emit('needParseMessage'); 151 | expect(parseMessageCalled).to.be.equal(true); 152 | AmiIo.Client.prototype.parseMessage = old; 153 | }); 154 | }); 155 | 156 | describe("Event: 'needParseEvent'", () => { 157 | it('call #parseEvent function', () => { 158 | const old = AmiIo.Client.prototype.parseEvent; 159 | const client = new AmiIo.Client(); 160 | let parseEventCalled = false; 161 | 162 | AmiIo.Client.prototype.parseEvent = function() { 163 | parseEventCalled = true; 164 | }; 165 | client.emit('needParseEvent'); 166 | expect(parseEventCalled).to.be.equal(true); 167 | AmiIo.Client.prototype.parseEvent = old; 168 | }); 169 | }); 170 | 171 | describe("Event: 'needParseResponse'", () => { 172 | it('call #parseResponse function', () => { 173 | const old = AmiIo.Client.prototype.parseResponse; 174 | const client = new AmiIo.Client(); 175 | let parseResponseCalled = false; 176 | 177 | AmiIo.Client.prototype.parseResponse = function() { 178 | parseResponseCalled = true; 179 | }; 180 | client.emit('needParseResponse'); 181 | expect(parseResponseCalled).to.be.equal(true); 182 | AmiIo.Client.prototype.parseResponse = old; 183 | }); 184 | }); 185 | 186 | describe("Event: 'incorrectLogin'", () => { 187 | it('set #reconnectable to false', () => { 188 | const client = new AmiIo.Client(); 189 | 190 | client.reconnectable = true; 191 | client.emit('incorrectLogin'); 192 | expect(client.reconnectable).to.be.equal(false); 193 | }); 194 | }); 195 | 196 | describe("Event: 'incorrectServer'", () => { 197 | it('set #reconnectable to false', () => { 198 | const client = new AmiIo.Client(); 199 | 200 | client.reconnectable = true; 201 | client.emit('incorrectServer'); 202 | expect(client.reconnectable).to.be.equal(false); 203 | }); 204 | }); 205 | 206 | describe('#createClient()', () => { 207 | it('creates instance of Client', () => { 208 | expect(AmiIo.createClient()).to.be.instanceOf(AmiIo.Client); 209 | }); 210 | 211 | it('has EventEmitter as prototype', () => { 212 | expect(AmiIo.createClient()).to.be.instanceOf( 213 | require('events').EventEmitter 214 | ); 215 | }); 216 | 217 | it('use host from config', () => { 218 | expect(AmiIo.createClient({ host: '123' })).to.have.nested.property( 219 | 'config.host', 220 | '123' 221 | ); 222 | }); 223 | 224 | it("use '127.0.0.1' as default host", () => { 225 | expect(AmiIo.createClient()).to.have.nested.property( 226 | 'config.host', 227 | '127.0.0.1' 228 | ); 229 | }); 230 | 231 | it('use port from config', () => { 232 | expect(AmiIo.createClient({ port: 6666 })).to.have.nested.property( 233 | 'config.port', 234 | 6666 235 | ); 236 | }); 237 | 238 | it('use 5038 as default port', () => { 239 | expect(AmiIo.createClient()).to.have.nested.property('config.port', 5038); 240 | }); 241 | 242 | it('use login from config', () => { 243 | expect(AmiIo.createClient({ login: 'trim' })).to.have.nested.property( 244 | 'config.login', 245 | 'trim' 246 | ); 247 | }); 248 | 249 | it("use 'admin' as default login", () => { 250 | expect(AmiIo.createClient()).to.have.nested.property( 251 | 'config.login', 252 | 'admin' 253 | ); 254 | }); 255 | 256 | it('use password from config', () => { 257 | expect(AmiIo.createClient({ password: 'ouch' })).to.have.nested.property( 258 | 'config.password', 259 | 'ouch' 260 | ); 261 | }); 262 | 263 | it("use 'admin' as default password", () => { 264 | expect(AmiIo.createClient()).to.have.nested.property( 265 | 'config.password', 266 | 'admin' 267 | ); 268 | }); 269 | 270 | it("use '' as initial value for version", () => { 271 | expect(AmiIo.createClient()).to.nested.property('version', ''); 272 | }); 273 | }); 274 | 275 | describe('#connect()', () => { 276 | let c; 277 | 278 | beforeEach(() => { 279 | c = new AmiIo.Client(); 280 | c.initSocket = function() {}; 281 | }); 282 | 283 | it('set should reconnect to true, if need', () => { 284 | c.connect(true); 285 | expect(c.reconnectable).to.be.equal(true); 286 | }); 287 | 288 | it('set should reconnect to false, if need', () => { 289 | c.connect(false); 290 | expect(c.reconnectable).to.be.equal(false); 291 | }); 292 | 293 | it('set should reconnect to false, by default', () => { 294 | c.connect(); 295 | expect(c.reconnectable).to.be.equal(false); 296 | }); 297 | 298 | it('does not set reconnect timeout if does not should reconnect', () => { 299 | c.connect(false, 1000); 300 | expect(c.timeout).to.not.exist; 301 | }); 302 | 303 | it('set reconnect timeout to number, if need', () => { 304 | c.connect(true, 1000); 305 | expect(c.reconnectTimeout).to.be.equal(1000); 306 | }); 307 | 308 | it('set reconnect timeout to default if non number use', () => { 309 | c.connect(true, 'asd'); 310 | expect(c.reconnectTimeout).to.be.equal(5000); 311 | }); 312 | 313 | it('set reconnect timeout to 5000, by default', () => { 314 | c.connect(true); 315 | expect(c.reconnectTimeout).to.be.equal(5000); 316 | }); 317 | 318 | it('use previous correct value as default', () => { 319 | c.connect(true, 1234); 320 | c.connect(true, 'rer'); 321 | expect(c.reconnectTimeout).to.be.equal(1234); 322 | }); 323 | }); 324 | 325 | describe('#initSocket()', () => { 326 | const old = net.Socket.prototype.connect; 327 | let current; 328 | let client; 329 | 330 | before(() => { 331 | net.Socket.prototype.connect = function(port, host) { 332 | current = { port, host }; 333 | }; 334 | }); 335 | 336 | beforeEach(() => { 337 | client = new AmiIo.Client(); 338 | current = null; 339 | }); 340 | 341 | it('create socket at #socket and connect', () => { 342 | client.initSocket(); 343 | expect(current).to.deep.equal({ port: 5038, host: '127.0.0.1' }); 344 | }); 345 | 346 | it('set socket encoding to ASCII', () => { 347 | const old = net.Socket.prototype.setEncoding; 348 | 349 | net.Socket.prototype.setEncoding = function(enc) { 350 | expect(enc.toLowerCase()).to.be.equal('ascii'); 351 | }; 352 | client.initSocket(); 353 | net.Socket.prototype.setEncoding = old; 354 | }); 355 | 356 | it('close exist if socket present and is not destroyed', () => { 357 | let ended = false; 358 | 359 | client.socket = { 360 | destroyed: false, 361 | removeAllListeners() {}, 362 | end() { 363 | ended = true; 364 | }, 365 | }; 366 | client.initSocket(); 367 | expect(ended).to.be.equal(true); 368 | }); 369 | 370 | it('remove all listeners from socket if socket present and is not destroyed', () => { 371 | let removed = false; 372 | 373 | client.socket = { 374 | destroyed: false, 375 | removeAllListeners() { 376 | removed = true; 377 | }, 378 | end() {}, 379 | }; 380 | client.initSocket(); 381 | expect(removed).to.be.equal(true); 382 | }); 383 | 384 | it('remove all listeners from socket if socket present and is destroyed', () => { 385 | let removed = false; 386 | 387 | client.socket = { 388 | destroyed: true, 389 | removeAllListeners() { 390 | removed = true; 391 | }, 392 | end() {}, 393 | }; 394 | client.initSocket(); 395 | expect(removed).to.be.equal(true); 396 | }); 397 | 398 | it('unref() new socket if last was unrefed', () => { 399 | let s = false; 400 | const old = net.Socket.prototype.unref; 401 | 402 | net.Socket.prototype.unref = function(enc) { 403 | s = true; 404 | }; 405 | client.unrefed = true; 406 | client.initSocket(); 407 | net.Socket.prototype.unref = old; 408 | expect(s).to.be.equal(true); 409 | }); 410 | 411 | it("throw 'connect' event to client 'socketConnected' event", () => { 412 | let s = false; 413 | 414 | client.on('socketConnected', () => { 415 | s = true; 416 | }); 417 | client.initSocket(); 418 | client.socket.emit('connect'); 419 | expect(s).to.be.equal(true); 420 | }); 421 | 422 | it("throw 'error' event to client 'socketError' event", () => { 423 | let s = false; 424 | 425 | client.on('socketError', () => { 426 | s = true; 427 | }); 428 | client.initSocket(); 429 | client.socket.emit('error', {}); 430 | expect(s).to.be.equal(true); 431 | }); 432 | 433 | it("throw 'error' with code 'ECONNREFUSED' to client 'socketError' event", () => { 434 | let s = false; 435 | 436 | client.on('socketError', () => { 437 | s = true; 438 | }); 439 | client.initSocket(); 440 | client.socket.emit('error', { code: 'ECONNREFUSED' }); 441 | expect(s).to.be.equal(true); 442 | }); 443 | 444 | it("throw 'error' with code 'ECONNREFUSED' to client 'connectionRefused' event", () => { 445 | let s = false; 446 | 447 | client.on('connectionRefused', () => { 448 | s = true; 449 | }); 450 | client.initSocket(); 451 | client.socket.emit('error', { code: 'ECONNREFUSED' }); 452 | expect(s).to.be.equal(true); 453 | }); 454 | 455 | it("throw 'close' event to client 'disconnected' event", () => { 456 | let s = false; 457 | 458 | client.on('disconnected', () => { 459 | s = true; 460 | }); 461 | client.initSocket(); 462 | client.socket.emit('close'); 463 | expect(s).to.be.equal(true); 464 | }); 465 | 466 | it("throw 'timeout' event to client 'connectTimeout' event", () => { 467 | let s = false; 468 | 469 | client.on('connectTimeout', () => { 470 | s = true; 471 | }); 472 | client.initSocket(); 473 | client.socket.emit('timeout'); 474 | expect(s).to.be.equal(true); 475 | }); 476 | 477 | it("throw 'end' event to client 'connectEnd' event", () => { 478 | let s = false; 479 | 480 | client.on('connectEnd', () => { 481 | s = true; 482 | }); 483 | client.initSocket(); 484 | client.socket.emit('end'); 485 | expect(s).to.be.equal(true); 486 | }); 487 | 488 | it("throw 'data' event to client 'needAuth' event", () => { 489 | let s = false; 490 | 491 | client.on('needAuth', () => { 492 | s = true; 493 | }); 494 | client.initSocket(); 495 | client.socket.emit('data', ''); 496 | expect(s).to.be.equal(true); 497 | }); 498 | 499 | it("throw 'data' event to client 'needAuth' event only once", () => { 500 | let s = false; 501 | 502 | client.once('needAuth', c => { 503 | s = c; 504 | }); 505 | client.initSocket(); 506 | client.socket.emit('data', 'rew'); 507 | client.socket.emit('data', 'awe'); 508 | expect(s).to.be.equal('rew'); 509 | }); 510 | 511 | after(() => { 512 | net.Socket.prototype.connect = old; 513 | }); 514 | }); 515 | 516 | describe('#auth()', () => { 517 | let c; 518 | 519 | beforeEach(() => { 520 | c = new AmiIo.Client(); 521 | }); 522 | 523 | it("emit to object 'incorrectServer' then not asterisk Hello Message", () => { 524 | let d; 525 | 526 | c.on('incorrectServer', m => { 527 | d = m; 528 | }); 529 | c.auth('bla Lan Bla'); 530 | expect(d).to.be.equal('bla Lan Bla'); 531 | }); 532 | 533 | it('parse version from asterisk Hello Message', () => { 534 | c.socket = new (require('events')).EventEmitter(); 535 | c.send = function() {}; 536 | c.auth('Asterisk Call Manager/123.23-erfw2'); 537 | expect(c.version).to.be.equal('123.23-erfw2'); 538 | }); 539 | 540 | it('create data listener on socket after success', () => { 541 | c.socket = new (require('events')).EventEmitter(); 542 | c.send = function() {}; 543 | 544 | let s = false; 545 | 546 | c.socket.on('newListener', event => { 547 | if (event === 'data') { 548 | s = true; 549 | } 550 | }); 551 | c.auth('Asterisk Call Manager/123.23-erfw2'); 552 | expect(s).to.be.equal(true); 553 | }); 554 | 555 | it('try send something to socket after success', () => { 556 | c.socket = new (require('events')).EventEmitter(); 557 | c.send = function() {}; 558 | 559 | let s = false; 560 | 561 | c.send = function() { 562 | s = true; 563 | }; 564 | c.auth('Asterisk Call Manager/123.23-erfw2'); 565 | expect(s).to.be.equal(true); 566 | }); 567 | }); 568 | }); 569 | -------------------------------------------------------------------------------- /test/create-action-sub-class.js: -------------------------------------------------------------------------------- 1 | const createActionSubClass = require('../lib/create-action-sub-class'); 2 | const { expect } = require('chai'); 3 | 4 | describe('create-action-sub-class', () => { 5 | it('throws if any optional parameter have default value', () => { 6 | class Parent {} 7 | 8 | expect(() => 9 | createActionSubClass( 10 | { 11 | name: 'test', 12 | defaults: { arg1: '' }, 13 | optional: ['arg1'], 14 | params: ['arg1'], 15 | }, 16 | Parent 17 | ) 18 | ).to.throw(Error, `Unexpected default value for field arg1.`); 19 | }); 20 | 21 | it('throws if any optional parameter missed in all parameters list', () => { 22 | class Parent {} 23 | 24 | expect(() => 25 | createActionSubClass( 26 | { 27 | name: 'test', 28 | defaults: {}, 29 | optional: ['arg1'], 30 | params: [], 31 | }, 32 | Parent 33 | ) 34 | ).to.throw(Error, `Did not found optional field arg1 in params.`); 35 | }); 36 | 37 | it('does not throw if descriptor is valid', () => { 38 | class Parent {} 39 | 40 | expect(() => 41 | createActionSubClass( 42 | { 43 | name: 'test', 44 | defaults: {}, 45 | optional: ['arg1'], 46 | params: ['arg1'], 47 | }, 48 | Parent 49 | ) 50 | ).not.to.throw(Error); 51 | }); 52 | 53 | it("returns a subclass with proper #name 's value", () => { 54 | class MyClass {} 55 | 56 | const SubClass = createActionSubClass({ name: 'test' }, MyClass); 57 | 58 | const instance = new SubClass(); 59 | 60 | expect(instance).to.be.instanceOf(SubClass); 61 | expect(instance).to.be.instanceOf(MyClass); 62 | expect(SubClass.name).to.be.equal('test'); 63 | }); 64 | 65 | describe('returned subclass', () => { 66 | function sharedTests(getInstance) { 67 | it('calls super with #name', () => { 68 | let called = false; 69 | let args = null; 70 | 71 | class Super { 72 | constructor(...rest) { 73 | called = true; 74 | args = rest; 75 | } 76 | } 77 | 78 | const SubClass = createActionSubClass({ name: 'test' }, Super); 79 | 80 | getInstance(SubClass); 81 | 82 | expect(called).to.be.equal(true); 83 | expect(args).to.be.deep.equal(['test']); 84 | }); 85 | 86 | it('throws if undefined is passed as 1st arg', () => { 87 | class Super {} 88 | 89 | const SubClass = createActionSubClass({ name: 'test' }, Super); 90 | 91 | expect(() => getInstance(SubClass, undefined)).to.throw( 92 | Error, 93 | "Can't work with passed undefined as the 1st arg" 94 | ); 95 | }); 96 | 97 | it('works fine if 1 arg is { param1: null } (set it into 1st parameter)', () => { 98 | class Super {} 99 | 100 | const SubClass = createActionSubClass( 101 | { name: 'test', params: ['param1', 'param2'] }, 102 | Super 103 | ); 104 | 105 | const instance = getInstance(SubClass, { param1: null }); 106 | 107 | expect(instance).to.have.own.property('param1', null); 108 | }); 109 | 110 | it('works fine if 1 arg is { param1: some string } (set it into 1st parameter)', () => { 111 | class Super {} 112 | 113 | const SubClass = createActionSubClass( 114 | { name: 'test', params: ['param1', 'param2'] }, 115 | Super 116 | ); 117 | 118 | const instance = getInstance(SubClass, { param1: 'null' }); 119 | 120 | expect(instance).to.have.own.property('param1', 'null'); 121 | }); 122 | 123 | it('use defaults if no extra arguments passed', () => { 124 | class Super {} 125 | 126 | const SubClass = createActionSubClass( 127 | { 128 | name: 'test', 129 | params: ['param1', 'param2'], 130 | defaults: { param1: 'value1', param2: 'value2' }, 131 | }, 132 | Super 133 | ); 134 | 135 | const instance = getInstance(SubClass); 136 | 137 | expect(instance).to.have.own.property('param1', 'value1'); 138 | expect(instance).to.have.own.property('param2', 'value2'); 139 | }); 140 | } 141 | 142 | describe('in case of `new Class(...args)`', () => { 143 | sharedTests((Class, ...rest) => new Class(...rest)); 144 | 145 | it('works fine if 1 arg is null (set it into 1st parameter)', () => { 146 | class Super {} 147 | 148 | const SubClass = createActionSubClass( 149 | { name: 'test', params: ['param1', 'param2'] }, 150 | Super 151 | ); 152 | 153 | const instance = new SubClass(null); 154 | 155 | expect(instance).to.have.own.property('param1', null); 156 | }); 157 | 158 | it('works fine if 1 arg is some string (set it into 1st parameter)', () => { 159 | class Super {} 160 | 161 | const SubClass = createActionSubClass( 162 | { name: 'test', params: ['param1', 'param2'] }, 163 | Super 164 | ); 165 | 166 | const instance = new SubClass('null'); 167 | 168 | expect(instance).to.have.own.property('param1', 'null'); 169 | }); 170 | }); 171 | 172 | describe('in case of `Class.from(...args)`', () => { 173 | sharedTests((Class, ...rest) => Class.from(...rest)); 174 | 175 | it('throws as error if 1 arg is null', () => { 176 | class Super {} 177 | 178 | const SubClass = createActionSubClass( 179 | { name: 'test', params: ['param1', 'param2'] }, 180 | Super 181 | ); 182 | 183 | expect(() => SubClass.from(null)).to.throw( 184 | Error, 185 | 'test.from() can be called only without arguments or with named arguments (pass plain object)' 186 | ); 187 | }); 188 | 189 | it('throws an error if 1 arg is some string', () => { 190 | class Super {} 191 | 192 | const SubClass = createActionSubClass( 193 | { name: 'test', params: ['param1', 'param2'] }, 194 | Super 195 | ); 196 | 197 | expect(() => SubClass.from('null')).to.throw( 198 | Error, 199 | 'test.from() can be called only without arguments or with named arguments (pass plain object)' 200 | ); 201 | }); 202 | }); 203 | }); 204 | }); 205 | -------------------------------------------------------------------------------- /test/event.js: -------------------------------------------------------------------------------- 1 | const Message = require('../lib/message.js'); 2 | const Event = require('../lib/event.js'); 3 | const expect = require('chai').expect; 4 | 5 | describe('AmiIo.Event', () => { 6 | describe('#constructor()', () => { 7 | it('creates instance of Event', () => { 8 | expect(new Event('')).to.be.instanceOf(Event); 9 | }); 10 | 11 | it('has Message as prototype', () => { 12 | expect(new Event('')).to.be.instanceOf(Message); 13 | }); 14 | 15 | it('spawns .parse() function from prototype', () => { 16 | let spawned = false; 17 | const old = Event.prototype.parse; 18 | 19 | Event.prototype.parse = function() { 20 | spawned = true; 21 | }; 22 | new Event(''); 23 | expect(spawned).to.be.equal(true); 24 | Event.prototype.parse = old; 25 | }); 26 | 27 | it('has #parse() === Message#parse()', () => { 28 | expect(Event.prototype.parse).to.be.equal(Message.prototype.parse); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NumminorihSF/ami-io/e269b4f9445329884cb1cfa86725d51957088ff0/test/index.js -------------------------------------------------------------------------------- /test/logger.js: -------------------------------------------------------------------------------- 1 | const Logger = require('../lib/loggers/logger.js'); 2 | const expect = require('chai').expect; 3 | 4 | describe('AmiIo.Logger', function() { 5 | describe('#constructor()', () => { 6 | it ('creates instance of Logger', function(){ 7 | expect(new Logger()).to.be.instanceOf(Logger); 8 | }); 9 | 10 | it ('sets minimalLogLevel', function(){ 11 | expect(new Logger('some').minimalLogLevel).to.be.equal('some'); 12 | }) 13 | 14 | }); 15 | 16 | describe('#setMinimalLogLevel()', () => { 17 | it('sets minimalLogLevel\'s value', function(){ 18 | const logger = new Logger(); 19 | logger.setMinimalLogLevel('123de'); 20 | expect(logger.minimalLogLevel).to.be.equal('123de'); 21 | }); 22 | 23 | }); 24 | 25 | ['trace', 'debug', 'info', 'warn', 'error', 'fatal'].forEach((level) => { 26 | describe(`#${level}()`, function(){ 27 | it('does not throw', function(){ 28 | const logger = new Logger(); 29 | expect(() => logger[level]()).to.not.throw(Error); 30 | }); 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /test/message.js: -------------------------------------------------------------------------------- 1 | const Message = require('../lib/message.js'); 2 | const expect = require('chai').expect; 3 | const { EOL, EOM } = require('../lib/constants'); 4 | 5 | describe('AmiIo.Message', () => { 6 | describe('#constructor()', () => { 7 | it('creates instance of Message', done => { 8 | expect(new Message()).to.be.instanceOf(Message); 9 | done(); 10 | }); 11 | }); 12 | 13 | describe('#format()', () => { 14 | it('format empty message to EOL success', done => { 15 | const message = new Message(); 16 | 17 | expect(message.format()).to.be.equal(EOL); 18 | done(); 19 | }); 20 | 21 | it('format many variables success', done => { 22 | const message = new Message(); 23 | 24 | message.variables = { 25 | first: 'first', 26 | SeConD: 'second', 27 | third: '3', 28 | }; 29 | expect(message.format()).to.be.equal( 30 | `Variable: first=first${EOL}Variable: SeConD=second${EOL}Variable: third=3${EOM}` 31 | ); 32 | done(); 33 | }); 34 | 35 | it('format some message without variables success', () => { 36 | const message = new Message(); 37 | 38 | Object.assign(message, { 39 | first: 'first', 40 | SeConD: 'second', 41 | third: '3', 42 | }); 43 | expect(message.format()).to.be.equal( 44 | `first: first${EOL}SeConD: second${EOL}third: 3${EOM}` 45 | ); 46 | }); 47 | 48 | it('format any massage success', () => { 49 | const message = new Message(); 50 | 51 | Object.assign(message, { 52 | first: 'first', 53 | SeConD: 'second', 54 | third: '3', 55 | variables: { 56 | first: 'first', 57 | SeConD: 'second', 58 | third: '3', 59 | }, 60 | }); 61 | expect(message.format()).to.be.equal( 62 | `first: first${EOL}SeConD: second${EOL}third: 3${EOL}Variable: first=first${EOL}Variable: SeConD=second${EOL}Variable: third=3${EOM}` 63 | ); 64 | }); 65 | 66 | it('ignore functions at fields', done => { 67 | const message = new Message(); 68 | 69 | Object.assign(message, { 70 | variables: { 71 | first: 'first', 72 | SeConD: 'second', 73 | third: '3', 74 | }, 75 | f() {}, 76 | }); 77 | expect(message.format()).to.be.equal( 78 | `Variable: first=first${EOL}Variable: SeConD=second${EOL}Variable: third=3${EOM}` 79 | ); 80 | done(); 81 | }); 82 | }); 83 | 84 | describe('#parse()', () => { 85 | let message; 86 | 87 | beforeEach(() => { 88 | message = new Message(); 89 | }); 90 | 91 | it('has correct field .incomingData with split strings', done => { 92 | message.parse(`My: message${EOL}Second: value`); 93 | expect(message.incomingData).to.deep.equal([ 94 | 'My: message', 95 | 'Second: value', 96 | ]); 97 | done(); 98 | }); 99 | 100 | it('set field correct', done => { 101 | message.parse(`My: message${EOL}Second: value`); 102 | expect(message.my).to.be.equal('message'); 103 | done(); 104 | }); 105 | 106 | it('set variables correct', done => { 107 | message.parse( 108 | `Variable: My${EOL}Value: message${EOL}Variable: SecOnD${EOL}Value: const` 109 | ); 110 | expect(message.variables).to.deep.equal({ 111 | My: 'message', 112 | SecOnD: 'const', 113 | }); 114 | done(); 115 | }); 116 | 117 | it('parse message with multiple ChanVariableKey correctly', () => { 118 | const mes = [ 119 | 'Event: Bridge', 120 | 'Privilege: call,all', 121 | 'Timestamp: 1469112499.321389', 122 | 'Bridgestate: Unlink', 123 | 'Bridgetype: core', 124 | 'Channel1: SIP/with-TSE-LIM2-0000a194', 125 | 'Channel2: Local/5842@from-queue-0000af99;1', 126 | 'Uniqueid1: 1469111531.131272', 127 | 'Uniqueid2: 1469111552.131275', 128 | 'CallerID1: 4959810606', 129 | 'CallerID2: 5836', 130 | 'ChanVariable(SIP/with-TSE-LIM2-0000a194): CDR(linkedid)=1469111531.131272', 131 | 'ChanVariable(SIP/with-TSE-LIM2-0000a194): CDR(dst)=5899', 132 | 'ChanVariable(Local/5842@from-queue-0000af99;1): CDR(linkedid)=1469111531.131272', 133 | 'ChanVariable(Local/5842@from-queue-0000af99;1): CDR(dst)=5842', 134 | ].join('\r\n'); 135 | 136 | message.parse(mes); 137 | expect(JSON.stringify(message)).to.deep.equal( 138 | JSON.stringify({ 139 | variables: {}, 140 | event: 'Bridge', 141 | privilege: 'call,all', 142 | timestamp: '1469112499.321389', 143 | bridgestate: 'Unlink', 144 | bridgetype: 'core', 145 | channel1: 'SIP/with-TSE-LIM2-0000a194', 146 | channel2: 'Local/5842@from-queue-0000af99;1', 147 | uniqueid1: '1469111531.131272', 148 | uniqueid2: '1469111552.131275', 149 | callerid1: '4959810606', 150 | callerid2: '5836', 151 | 'chanvariable(sip/with_tse_lim2_0000a194)': { 152 | 'cdr(linkedid)': '1469111531.131272', 153 | 'cdr(dst)': '5899', 154 | }, 155 | 'chanvariable(local/5842@from_queue_0000af99;1)': { 156 | 'cdr(linkedid)': '1469111531.131272', 157 | 'cdr(dst)': '5842', 158 | }, 159 | incomingData: [ 160 | 'Event: Bridge', 161 | 'Privilege: call,all', 162 | 'Timestamp: 1469112499.321389', 163 | 'Bridgestate: Unlink', 164 | 'Bridgetype: core', 165 | 'Channel1: SIP/with-TSE-LIM2-0000a194', 166 | 'Channel2: Local/5842@from-queue-0000af99;1', 167 | 'Uniqueid1: 1469111531.131272', 168 | 'Uniqueid2: 1469111552.131275', 169 | 'CallerID1: 4959810606', 170 | 'CallerID2: 5836', 171 | 'ChanVariable(SIP/with-TSE-LIM2-0000a194): CDR(linkedid)=1469111531.131272', 172 | 'ChanVariable(SIP/with-TSE-LIM2-0000a194): CDR(dst)=5899', 173 | 'ChanVariable(Local/5842@from-queue-0000af99;1): CDR(linkedid)=1469111531.131272', 174 | 'ChanVariable(Local/5842@from-queue-0000af99;1): CDR(dst)=5842', 175 | ], 176 | }) 177 | ); 178 | }); 179 | }); 180 | 181 | describe('.parse()', () => { 182 | it('has correct field .incomingData with split strings', done => { 183 | const message = Message.parse(`My: message${EOL}Second: value`); 184 | 185 | expect(message.incomingData).to.deep.equal([ 186 | 'My: message', 187 | 'Second: value', 188 | ]); 189 | done(); 190 | }); 191 | 192 | it('set field correct', done => { 193 | const message = Message.parse(`My: message${EOL}Second: value`); 194 | 195 | expect(message.my).to.be.equal('message'); 196 | done(); 197 | }); 198 | 199 | it('set variables correct', done => { 200 | const message = Message.parse( 201 | `Variable: My${EOL}Value: message${EOL}Variable: SecOnD${EOL}Value: const` 202 | ); 203 | 204 | expect(message.variables).to.deep.equal({ 205 | My: 'message', 206 | SecOnD: 'const', 207 | }); 208 | done(); 209 | }); 210 | 211 | it('parse message with multiple ChanVariableKey correctly', () => { 212 | const mes = [ 213 | 'Event: Bridge', 214 | 'Privilege: call,all', 215 | 'Timestamp: 1469112499.321389', 216 | 'Bridgestate: Unlink', 217 | 'Bridgetype: core', 218 | 'Channel1: SIP/with-TSE-LIM2-0000a194', 219 | 'Channel2: Local/5842@from-queue-0000af99;1', 220 | 'Uniqueid1: 1469111531.131272', 221 | 'Uniqueid2: 1469111552.131275', 222 | 'CallerID1: 4959810606', 223 | 'CallerID2: 5836', 224 | 'ChanVariable(SIP/with-TSE-LIM2-0000a194): CDR(linkedid)=1469111531.131272', 225 | 'ChanVariable(SIP/with-TSE-LIM2-0000a194): CDR(dst)=5899', 226 | 'ChanVariable(Local/5842@from-queue-0000af99;1): CDR(linkedid)=1469111531.131272', 227 | 'ChanVariable(Local/5842@from-queue-0000af99;1): CDR(dst)=5842', 228 | ].join('\r\n'); 229 | const message = Message.parse(mes); 230 | 231 | expect(JSON.stringify(message)).to.deep.equal( 232 | JSON.stringify({ 233 | variables: {}, 234 | event: 'Bridge', 235 | privilege: 'call,all', 236 | timestamp: '1469112499.321389', 237 | bridgestate: 'Unlink', 238 | bridgetype: 'core', 239 | channel1: 'SIP/with-TSE-LIM2-0000a194', 240 | channel2: 'Local/5842@from-queue-0000af99;1', 241 | uniqueid1: '1469111531.131272', 242 | uniqueid2: '1469111552.131275', 243 | callerid1: '4959810606', 244 | callerid2: '5836', 245 | 'chanvariable(sip/with_tse_lim2_0000a194)': { 246 | 'cdr(linkedid)': '1469111531.131272', 247 | 'cdr(dst)': '5899', 248 | }, 249 | 'chanvariable(local/5842@from_queue_0000af99;1)': { 250 | 'cdr(linkedid)': '1469111531.131272', 251 | 'cdr(dst)': '5842', 252 | }, 253 | incomingData: [ 254 | 'Event: Bridge', 255 | 'Privilege: call,all', 256 | 'Timestamp: 1469112499.321389', 257 | 'Bridgestate: Unlink', 258 | 'Bridgetype: core', 259 | 'Channel1: SIP/with-TSE-LIM2-0000a194', 260 | 'Channel2: Local/5842@from-queue-0000af99;1', 261 | 'Uniqueid1: 1469111531.131272', 262 | 'Uniqueid2: 1469111552.131275', 263 | 'CallerID1: 4959810606', 264 | 'CallerID2: 5836', 265 | 'ChanVariable(SIP/with-TSE-LIM2-0000a194): CDR(linkedid)=1469111531.131272', 266 | 'ChanVariable(SIP/with-TSE-LIM2-0000a194): CDR(dst)=5899', 267 | 'ChanVariable(Local/5842@from-queue-0000af99;1): CDR(linkedid)=1469111531.131272', 268 | 'ChanVariable(Local/5842@from-queue-0000af99;1): CDR(dst)=5842', 269 | ], 270 | }) 271 | ); 272 | }); 273 | }); 274 | }); 275 | -------------------------------------------------------------------------------- /test/response.js: -------------------------------------------------------------------------------- 1 | const Message = require('../lib/message.js'); 2 | const Response = require('../lib/response.js'); 3 | const expect = require('chai').expect; 4 | 5 | describe('AmiIo.Response', () => { 6 | describe('#constructor()', () => { 7 | it('creates instance of Response', done => { 8 | expect(new Response('')).to.be.instanceOf(Response); 9 | done(); 10 | }); 11 | 12 | it('has Message as prototype', done => { 13 | expect(new Response('')).to.be.instanceOf(Message); 14 | done(); 15 | }); 16 | 17 | it('spawns .parse() function from prototype', done => { 18 | let spawned = false; 19 | const old = Response.prototype.parse; 20 | 21 | Response.prototype.parse = function() { 22 | spawned = true; 23 | }; 24 | new Response(''); 25 | expect(spawned).to.be.equal(true); 26 | Response.prototype.parse = old; 27 | done(); 28 | }); 29 | }); 30 | }); 31 | --------------------------------------------------------------------------------