├── .gitignore ├── .jshintrc ├── LICENSE ├── README.md ├── app.js ├── config.json ├── data ├── db.js └── postgres │ └── source.js ├── lib ├── confbridge.js └── helpers │ ├── ari.js │ ├── bridgesetup.js │ ├── fsm │ ├── bridgefsm.js │ ├── modules │ │ ├── bridgedriver.js │ │ ├── channelmedia.js │ │ ├── pinauth.js │ │ └── recordingdriver.js │ └── userfsm.js │ ├── groupsetup.js │ └── usersetup.js ├── package.json └── scripts ├── addbridge.js ├── addgroup.js ├── adduser.js └── defaultdatabase.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "immed": true, 5 | "latedef": false, 6 | "newcap": true, 7 | "noarg": true, 8 | "sub": true, 9 | "undef": true, 10 | "unused": false, 11 | "boss": true, 12 | "eqnull": true, 13 | "node": true, 14 | "quotmark": true, 15 | "camelcase": true, 16 | "maxlen": 80, 17 | "laxbreak": true 18 | } 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | This is a basic configuration for a Conference Bridge Application using ARI and 4 | Node.js. 5 | 6 | # Installation 7 | 8 | ## Node.js 9 | 10 | This application requires Node.js 0.10.X. 11 | 12 | ## Install confbridge application 13 | ```bash 14 | $git clone https://github.com/semidy/node-confbridge-js.git 15 | $npm install 16 | ``` 17 | 18 | ## Install Postgres database driver 19 | ```bash 20 | $npm install pg 21 | ``` 22 | 23 | ## Create and configure your database 24 | 25 | Once you have created and configured your database, you will want to modify 26 | the dbConnection in the config.json to point to your database. For example: 27 | ```json 28 | postgres://user:secret@localhost/user 29 | ``` 30 | 31 | ## Create or reset to default data 32 | 33 | Run the following commands to create and/or reset all default tables, indexes, 34 | user profile, group profile, and bridge profile settings: 35 | ```bash 36 | $cd scripts 37 | $node defaultdatabase.js 38 | ``` 39 | 40 | ## Adding new data 41 | 42 | After you have created and configured your specific database and run the 43 | defaultdatabase.js script, you are now able to setup your own specific user, 44 | group, and bridge profiles, within the scripts folder, that are to be added to the 45 | database. 46 | 47 | Run the following commands to add a new user to the database: 48 | ```bash 49 | $node adduser.js 50 | ``` 51 | Run the following commands to add a new group to the database: 52 | ```bash 53 | $node addgroup.js 54 | ``` 55 | Run the following commands to add a new bridge to the database: 56 | ```bash 57 | $node addbridge.js 58 | ``` 59 | 60 | ## Asterisk configuration 61 | 62 | Modify the ariConnection in the config.json to point to your Asterisk Instance. 63 | 64 | Add the following to your dialplan to test the default connection: 65 | ```bash 66 | exten => 8888,1,NoOp() 67 | same => n,Stasis(confbridge) 68 | same => n,Hangup() 69 | ``` 70 | Also you can add the name of your user_type and group_type in the stasis 71 | application if you have changed it from default. For example: 72 | ```bash 73 | exten => 9999,1,NoOp() 74 | same => n,Stasis(confbridge, usertypename, grouptypename) 75 | same => n,Hangup() 76 | ``` 77 | 78 | # ConfBridge application 79 | 80 | Start the application: 81 | ```bash 82 | $node app.js 83 | ``` 84 | 85 | ## Join the conference 86 | 87 | Dial 8888 88 | 89 | ## Normal menu 90 | 91 | * Press 1 to toggle mute on and off 92 | * Press 2 to toggle deafmute on and off 93 | * Press 3 to leave the conference and execute dialplan(which can have a 94 | specified context, extension, and priority within the config.json) 95 | * Press 4 to decrease the speakers volume 96 | * Press 5 to reset the speakers volume 97 | * press 6 to increase the speakers volume 98 | * Press 7 to decrease microphone volume 99 | * Press 8 to reset microphone volume 100 | * Press 9 to increase microphone volume 101 | * Press 0 to change microphone pitch from normal to low to high 102 | * Press # to access the admin menu(if you have admin set to true in user 103 | profile) 104 | 105 | ## Admin menu 106 | 107 | * Press 1 to toggle the bridge lock on and off 108 | * Press 2 to kick the last user that joined the conference 109 | * Press 3 to record the conference, if it isn't already recording, but if the 110 | conference is already recording then it toggles pausing and unpausing the 111 | recording 112 | * Press # to return to the normal menu 113 | 114 | # Database profile options 115 | 116 | These are the various options that are specified in the database for users, 117 | groups, and bridges. 118 | 119 | ## Bridge profile options 120 | 121 | #### bridge_type 122 | The name of the bridge profile. 123 | * type: varchar(50) 124 | * default: default 125 | 126 | #### join_sound 127 | Determines what sound is to be played when a user joins the conference. The 128 | user profile option quiet must be set to false. 129 | * type: varchar(50) 130 | * default: confbridge-join 131 | 132 | #### leave_sound 133 | Determines what sound is to be played when a user leaves the conference. The 134 | user profile option quiet must be set to false. 135 | * type: varchar(50) 136 | * default: confbridge-leave 137 | 138 | #### pin_number 139 | The PIN number that users must enter in order to join the conference. The 140 | user profile option pin_auth must be set to true. 141 | * type: integer 142 | * default: 1234 143 | 144 | #### pin_retries 145 | Determines the number of retries a user is allowed while entering the PIN. The 146 | user profile option pin_auth must be set to true. 147 | * type: integer 148 | * default: 3 149 | 150 | #### enter_pin_sound 151 | Determines what sound is played when the user is prompted to enter in a PIN 152 | number. The user profile option pin_auth must be set to true. 153 | * type: varchar(50) 154 | * default: conf-getpin 155 | 156 | #### bad_pin_sound 157 | Determines what sound is played if the user enters an invalid PIN number. the 158 | user profile option pin_auth must be set to true. 159 | * type: varchar(50) 160 | * default: conf-invalidpin 161 | 162 | #### locked_sound 163 | Determines what sound is played if the user attempts to join a locked 164 | conference. 165 | * type: varchar(50) 166 | * default: confbridge-lock-no-join 167 | 168 | #### now_locked_sound 169 | Determines what sound is played when an admin locks the conference. 170 | * type: varchar(50) 171 | * default: confbridge-locked 172 | 173 | #### now_unlocked_sound 174 | Determines what sound is played when an admin unlocks the conference. 175 | * type: varchar(50) 176 | * default: confbridge-unlocked 177 | 178 | #### now_muted_sound 179 | Determines what sound is played when a channel is muted. 180 | * type: varchar(50) 181 | * default: confbridge-muted 182 | 183 | #### now_unmuted_sound 184 | Determines what sound is played when a channel is unmuted. 185 | * type: varchar(50) 186 | * default: confbridge-unmuted 187 | 188 | #### kicked_sound 189 | Determines what sound is played when a user is kicked out of the conference. 190 | * type: varchar(50) 191 | * default: confbridge-removed 192 | 193 | #### record_conference 194 | Determines whether or not the conference starts off recording after the first 195 | user is present within the conference. 196 | * type: boolean 197 | * default: false 198 | 199 | #### recording_sound 200 | Determines what sound is played when the conference starts or resumes recording 201 | * type: varchar(50) 202 | * default: conf-now-recording 203 | 204 | #### wait_for_leader_sound 205 | Determines what sound is played when a follower is waiting for a leader to 206 | join the conference. 207 | * type: varchar(50) 208 | * default: conf-waitforleader 209 | 210 | ## User profile options 211 | 212 | #### user_type 213 | The name of the user profile. 214 | * type: varchar(50) 215 | * default: default 216 | 217 | #### admin 218 | Determines whether or not a user is assigned admin functionality within the 219 | conference, such as access to the admin menu. 220 | * type: boolean 221 | * default: false 222 | 223 | #### moh 224 | plays music on hold when there is one participant in the conference. 225 | * type: boolean 226 | * default: true 227 | 228 | #### quiet 229 | Determines whether there is a sound to be played when a user enters or leaves 230 | the conference. 231 | * type: boolean 232 | * default: false 233 | 234 | #### pin_auth 235 | Determines whether or not a user is required to have to enter in a PIN number 236 | in order to join the conference. 237 | * type: boolean 238 | * default: false 239 | 240 | ## Group profile options 241 | 242 | #### group_type 243 | The name of the group profile. 244 | * type: varchar(50) 245 | * default: default 246 | 247 | #### group_behavior 248 | Determines how a group shall behave within the conference. There are three 249 | types of group behavior options that can be specified in the group profile and 250 | each of them have differing functional conferencing purposes. 251 | * type: varchar(50) 252 | * default: participant 253 | 254 | ###### Available Options 255 | * __participant__: the group in which the user can join the conference as a normal 256 | user. If no group is specified, this becomes the default group. 257 | * __follower__: the group that receives music on hold until a leader joins, once 258 | a leader joins the conference they enter the conference as well. 259 | * __leader__: the group that the followers wait on in order to join the conference 260 | 261 | #### max_members 262 | Determines how many users are allowed in each specified group. 263 | * type: integer 264 | * default: 100 265 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var ari = require('./lib/helpers/ari.js'); 4 | var ConfBridge = require('./lib/confbridge.js'); 5 | 6 | ari.then(function(client) { 7 | console.log('initializing confbridge'); 8 | 9 | var confbridge = new ConfBridge(client); 10 | }).done(); 11 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "ariConnection": { 3 | "url": "http://localhost:8088", 4 | "user": "user", 5 | "pass": "secret" 6 | }, 7 | "dbConnection": "postgres://user:secret@localhost/user", 8 | "bridgeProfile": "default", 9 | "menuInput": { 10 | "mute": "1", 11 | "deafMute": "2", 12 | "contInDialplan": "3", 13 | "decLisVol": "4", 14 | "resetLisVol": "5", 15 | "incLisVol": "6", 16 | "decTalkVol": "7", 17 | "resetTalkVol": "8", 18 | "incTalkVol": "9", 19 | "pitchChange": "0", 20 | "admin": "#" 21 | }, 22 | "waitingInput": { 23 | "verify": "#" 24 | }, 25 | "adminInput": { 26 | "toggleLock": "1", 27 | "kick": "2", 28 | "toggleRecord": "3", 29 | "menu": "#" 30 | }, 31 | "leaveConf": { 32 | "context": "", 33 | "extension": "", 34 | "priority": "" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /data/db.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var config = require('../config.json'); 4 | var util = require('util'); 5 | var sourceName = config.dbConnection.split(':')[0]; 6 | var Source = require(util.format('./%s/source.js', sourceName)); 7 | var db = new Source(config); 8 | 9 | module.exports = db; 10 | -------------------------------------------------------------------------------- /data/postgres/source.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var pg = require('pg'); 4 | var Q = require('q'); 5 | 6 | function PostgresDB(dbConfig) { 7 | 8 | var connect = Q.denodeify(pg.connect.bind(pg)); 9 | 10 | /** 11 | * Retrieves the bridge profile. 12 | * 13 | * @return {Q} result - a promise containing the row where the bridge 14 | * settings are stored 15 | */ 16 | this.getBridgeProfile = function() { 17 | 18 | return connect(dbConfig.dbConnection) 19 | .then(function (values) { 20 | var profile = dbConfig.bridgeProfile; 21 | var client = values[0]; 22 | var done = values[1]; 23 | var query = Q.denodeify(client.query.bind(client)); 24 | 25 | return query('SELECT exists(SELECT 1 FROM bridge_profile WHERE ' 26 | + 'bridge_type = $1)', [profile]) 27 | .then(function (result) { 28 | if (!result.rows[0].exists) { 29 | profile = 'default'; 30 | } 31 | return query('SELECT * FROM bridge_profile WHERE bridge_type = $1', 32 | [profile]); 33 | }) 34 | .then(function (result) { 35 | console.log('Fetched bridge profile', profile); 36 | return result.rows[0]; 37 | }) 38 | .catch(function (err) { 39 | console.error(err); 40 | }) 41 | .finally(function () { 42 | done(); 43 | }); 44 | }) 45 | .catch(function (err) { 46 | console.error(err); 47 | }); 48 | 49 | }; 50 | 51 | /** 52 | * Retrieves a user profile. 53 | * 54 | * @param {String} userType - the type of user to retrieve 55 | * @return {Q} result - a promise containing the row where the user 56 | * profile is stored 57 | */ 58 | this.getUserProfile = function(userType) { 59 | 60 | return connect(dbConfig.dbConnection) 61 | .then(function (values) { 62 | var client = values[0]; 63 | var done = values[1]; 64 | var query = Q.denodeify(client.query.bind(client)); 65 | 66 | return query('SELECT exists(SELECT 1 FROM user_profile WHERE ' 67 | + 'user_type = $1)', [userType]) 68 | .then(function (result) { 69 | if (!result.rows[0].exists) { 70 | userType = 'default'; 71 | } 72 | return query('SELECT * FROM user_profile WHERE user_type = ' 73 | + '$1', [userType]); 74 | }) 75 | .then(function (result) { 76 | return result.rows[0]; 77 | }) 78 | .catch(function (err) { 79 | console.error(err); 80 | }) 81 | .finally(function () { 82 | done(); 83 | }); 84 | }) 85 | .catch(function (err) { 86 | console.error(err); 87 | }); 88 | 89 | }; 90 | 91 | /** 92 | * Retrieves a group profile. 93 | * 94 | * @param {String} groupType - the type of group to retrieve 95 | * @return {Q} result - a promise containing the row where the group 96 | * profile is stored 97 | */ 98 | this.getGroupProfile = function(groupType) { 99 | 100 | return connect(dbConfig.dbConnection) 101 | .then(function (values) { 102 | var client = values[0]; 103 | var done = values[1]; 104 | var query = Q.denodeify(client.query.bind(client)); 105 | 106 | return query('SELECT exists(SELECT 1 FROM group_profile WHERE ' 107 | + 'group_type = $1)', [groupType]) 108 | .then(function (result) { 109 | if (!result.rows[0].exists) { 110 | groupType = 'default'; 111 | } 112 | return query('SELECT * FROM group_profile WHERE group_type = ' 113 | + '$1', [groupType]); 114 | }) 115 | .then(function (result) { 116 | return result.rows[0]; 117 | }) 118 | .catch(function (err) { 119 | console.error(err); 120 | }) 121 | .finally(function () { 122 | done(); 123 | }); 124 | }) 125 | .catch(function (err) { 126 | console.error(err); 127 | }); 128 | 129 | }; 130 | 131 | } 132 | 133 | module.exports = PostgresDB; 134 | -------------------------------------------------------------------------------- /lib/confbridge.js: -------------------------------------------------------------------------------- 1 | 'use-strict'; 2 | 3 | var BridgeSetup = require('./helpers/bridgesetup.js'); 4 | 5 | /** 6 | * ConfBridge constructor. 7 | * 8 | * @param {ari-client~Client} ari - ARI client 9 | */ 10 | function ConfBridge(ari) { 11 | var self = this; 12 | 13 | // Sets up the driver class to initialize the conference. 14 | var bridgeSetup = new BridgeSetup(ari); 15 | bridgeSetup.init(); 16 | 17 | /** 18 | * Handles StasisStart event to initialize bridge. 19 | * 20 | * @param {Object} event - the event object 21 | * @param {ari-client~Channel} incoming - the channel entering Stasis 22 | */ 23 | this.start = function(event, incoming) { 24 | incoming.answer(function (err) { 25 | bridgeSetup.registerUser(event, incoming); 26 | }); 27 | }; 28 | 29 | ari.on('StasisStart', self.start); 30 | } 31 | 32 | module.exports = ConfBridge; 33 | -------------------------------------------------------------------------------- /lib/helpers/ari.js: -------------------------------------------------------------------------------- 1 | 'use-strict'; 2 | 3 | var Q = require('q'); 4 | var ariClient = require('ari-client'); 5 | var config = require('../../config.json'); 6 | var connect = Q.denodeify(ariClient.connect); 7 | 8 | module.exports = connect(config.ariConnection.url, config.ariConnection.user, 9 | config.ariConnection.pass) 10 | .then(function(client) { 11 | client.start(['confbridge']); 12 | return client; 13 | }) 14 | .catch(function(err) { 15 | console.error(err); 16 | }); 17 | -------------------------------------------------------------------------------- /lib/helpers/bridgesetup.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Q = require('q'); 4 | var util = require('util'); 5 | var db = require('../../data/db.js'); 6 | var UserSetup = require('./usersetup.js'); 7 | var bridgeFsm = require('./fsm/bridgefsm.js'); 8 | var GroupSetup = require('./groupsetup.js'); 9 | 10 | function BridgeSetup(ari) { 11 | var self = this; 12 | 13 | var bridge = ari.Bridge(); 14 | var groups = new GroupSetup(); 15 | var users = new UserSetup(ari, db, groups); 16 | 17 | /** 18 | * Sets up the bridge for the conference. 19 | */ 20 | this.init = function() { 21 | var createBridge = Q.denodeify(bridge.create.bind(bridge)); 22 | createBridge({type: 'mixing,dtmf_events'}) 23 | .then(function () { 24 | self.setBridgeDefaults(); 25 | self.registerEvents(bridge); 26 | }) 27 | .then(function () { 28 | return db.getBridgeProfile(); 29 | }) 30 | .then(function (result) { 31 | bridge.settings = result; 32 | }) 33 | .then(function () { 34 | bridge.fsm = bridgeFsm(ari, bridge, users); 35 | }) 36 | .catch(function (err) { 37 | console.error(err); 38 | }) 39 | .done(); 40 | }; 41 | 42 | /** 43 | * Stores the user and creates a finite state machine for them. 44 | * 45 | * @param {Object} event - the StasisStart event 46 | * @param {Object} channel - the channel to store 47 | */ 48 | this.registerUser = function(event, channel) { 49 | users.storeUser(event, channel, bridge); 50 | }; 51 | 52 | /** 53 | * Registers event listeners to the bridge. 54 | * 55 | * @param {Object} bridge - the bridge to register events to 56 | */ 57 | this.registerEvents = function(bridge) { 58 | bridge.on('ChannelEnteredBridge', function (event, instances) { 59 | self.bridgeEnterHandleBridge(instances); 60 | self.bridgeEnterHandleGroups(instances); 61 | self.bridgeEnterHandleUsers(instances); 62 | }); 63 | bridge.on('ChannelLeftBridge', function (event, instances) { 64 | self.bridgeLeaveHandleBridge(instances); 65 | self.bridgeLeaveHandleGroups(instances); 66 | self.bridgeLeaveHandleUsers(instances); 67 | }); 68 | }; 69 | 70 | /** 71 | * Returns the size of the object. 72 | * 73 | * @param {Object} obj - the object to get the size of 74 | * @return {Integer} size - the size of the object 75 | */ 76 | this.size = function(obj) { 77 | return Object.keys(obj).length; 78 | }; 79 | 80 | /** 81 | * Initializes some default variables needed for the bridge. 82 | */ 83 | this.setBridgeDefaults = function() { 84 | bridge.lastJoined = []; 85 | bridge.channels = []; 86 | bridge.recordingEnabled = false; 87 | bridge.recordingPaused = true; 88 | }; 89 | 90 | /** 91 | * Handles bridge related events when a user enters the bridge. 92 | * 93 | * @param {Object} instances - contains objects related to the event 94 | */ 95 | this.bridgeEnterHandleBridge = function(instances) { 96 | var channelId = instances.channel.id; 97 | bridge.lastJoined.push(channelId); 98 | bridge.channels[channelId] = instances.channel; 99 | bridge.fsm.handle('userJoin', {channelId: instances.channel.id}); 100 | }; 101 | 102 | /** 103 | * Handles group related events when a user enters the bridge. 104 | * 105 | * @param {Object} instances - contains objects related to the event 106 | */ 107 | this.bridgeEnterHandleGroups = function(instances) { 108 | var channelId = instances.channel.id; 109 | var userList = users.getUsers(); 110 | if (groups.isLeader(userList,channelId)) { 111 | 112 | groups.addLeader(instances.channel); 113 | var followers = groups.getFollowers(); 114 | for (var chanId in followers) { 115 | userList[chanId].fsm.handle('leaderJoined'); 116 | } 117 | 118 | } 119 | }; 120 | 121 | /** 122 | * Handles user related events when a user enters the bridge. 123 | * 124 | * @param {Object} instances - contains objects related to the event 125 | */ 126 | this.bridgeEnterHandleUsers = function(instances) { 127 | var userList = users.getUsers(); 128 | for (var chanId in userList) { 129 | 130 | if (!userList[chanId].settings.quiet && 131 | !groups.isFollower(userList,chanId)) { 132 | 133 | var soundToPlay = util.format('sound:%s', bridge.settings.join_sound); 134 | var play = Q.denodeify(ari.channels.play.bind(ari)); 135 | play({channelId: chanId, media: soundToPlay}) 136 | .catch(function (err) { 137 | console.error(err); 138 | }) 139 | .done(); 140 | 141 | } 142 | 143 | } 144 | }; 145 | 146 | /** 147 | * Handles bridge related events when a user leaves the bridge, and also 148 | * places the user in an inactive state. 149 | * 150 | * @param {Object} instances - contains objects related to the event 151 | */ 152 | this.bridgeLeaveHandleBridge = function(instances) { 153 | var channelId = instances.channel.id; 154 | delete bridge.channels[channelId]; 155 | 156 | var userList = users.getUsers(); 157 | if (!groups.isFollower(userList,channelId)) { 158 | users.handleDone(instances.channel); 159 | } 160 | 161 | bridge.fsm.handle('userExit', {confBridge: instances.bridge}); 162 | bridge.lastJoined = bridge.lastJoined.filter(function(candidate) { 163 | return candidate !== channelId; 164 | }); 165 | }; 166 | 167 | /** 168 | * Handles group related events when a user leaves the bridge. 169 | * 170 | * @param {Object} instances - contains objects related to the event 171 | */ 172 | this.bridgeLeaveHandleGroups = function(instances) { 173 | var channelId = instances.channel.id; 174 | var userList = users.getUsers(); 175 | if (groups.isLeader(userList,channelId)) { 176 | 177 | groups.removeLeader(instances.channel); 178 | if (!groups.containsLeaders()) { 179 | 180 | var followers = groups.getFollowers(); 181 | for (var chanId in followers) { 182 | userList[chanId].fsm.handle('noLeaders'); 183 | } 184 | 185 | } 186 | 187 | } 188 | }; 189 | 190 | /** 191 | * Handles user related events when a user leaves the bridge. 192 | * 193 | * @param {Object} instances - contains objects related to the event 194 | */ 195 | this.bridgeLeaveHandleUsers = function(instances) { 196 | var userList = users.getUsers(); 197 | for (var chanId in userList) { 198 | 199 | if (!userList[chanId].settings.quiet && 200 | userList[chanId].fsm.isActive() && 201 | !groups.isFollower(userList,chanId)) { 202 | 203 | var soundToPlay = util.format('sound:%s', 204 | bridge.settings.leave_sound); 205 | var play = Q.denodeify(ari.channels.play.bind(ari)); 206 | play({channelId: chanId, media: soundToPlay}) 207 | .catch(function (err) { 208 | console.error(err); 209 | }) 210 | .done(); 211 | 212 | } 213 | 214 | } 215 | }; 216 | 217 | } 218 | 219 | module.exports = BridgeSetup; 220 | -------------------------------------------------------------------------------- /lib/helpers/fsm/bridgefsm.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var machina = require('machina'); 4 | var util = require('util'); 5 | var uuid = require('node-uuid'); 6 | var Q = require('q'); 7 | var BridgeDriverModule = require('./modules/bridgedriver.js'); 8 | var ChannelMediaModule = require('./modules/channelmedia.js'); 9 | var RecordingDriverModule = require('./modules/recordingdriver.js'); 10 | 11 | /** 12 | * Creates an fsm for a bridge and returns it. 13 | * 14 | * @returns fsm - the fsm to return 15 | */ 16 | function createFsm(ari, bridge, users) { 17 | 18 | var bridgeDriver = new BridgeDriverModule(); 19 | var channelMedia = new ChannelMediaModule(); 20 | var recordingDriver = new RecordingDriverModule(); 21 | 22 | var fsm = new machina.Fsm({ 23 | 24 | initialState: 'empty', 25 | 26 | printState: function() { 27 | console.log('Bridge entered state', this.state); 28 | }, 29 | 30 | states: { 31 | 32 | // The state where no channels are in the bridge. 33 | 'empty': { 34 | _onEnter: function() { 35 | this.printState(); 36 | bridgeDriver.setToDefault(bridge); 37 | }, 38 | 39 | 'userJoin': function(data) { 40 | this.transition('single'); 41 | }, 42 | 43 | _onExit: function() { 44 | var recordingName = util.format('confbridge-rec %s', uuid.v4()); 45 | bridge.currentRecording = ari.LiveRecording({name: recordingName}); 46 | if (bridge.settings.record_conference) { 47 | var record = Q.denodeify(bridge.record.bind(bridge)); 48 | recordingDriver.startRecording(bridge) 49 | .then(function () { 50 | var userList = users.getUsers(); 51 | for (var chanId in userList) { 52 | channelMedia.announceRecording(ari, chanId, bridge); 53 | } 54 | }) 55 | .catch(function (err) { 56 | console.error(err); 57 | }) 58 | .done(); 59 | } 60 | } 61 | }, 62 | 63 | // The state where only a single user is in the bridge. 64 | 'single': { 65 | _onEnter: function() { 66 | this.printState(); 67 | var userList = users.getUsers(); 68 | for (var chanId in userList) { 69 | if (userList[chanId].settings.moh) { 70 | channelMedia.startMoh(ari, chanId); 71 | } 72 | } 73 | }, 74 | 75 | _onExit: function() { 76 | var userList = users.getUsers(); 77 | for (var chanId in userList) { 78 | if (userList[chanId].settings.moh && 79 | userList[chanId].fsm.isActive()) { 80 | channelMedia.stopMoh(ari, chanId); 81 | } 82 | } 83 | }, 84 | 85 | 'userJoin': function(data) { 86 | this.transition('multi'); 87 | channelMedia.announceRecording(ari, data.channelId, bridge); 88 | }, 89 | 90 | 'userExit': function() { 91 | this.transition('empty'); 92 | } 93 | }, 94 | 95 | // The state when multiple users are in the bridge. 96 | 'multi': { 97 | _onEnter: function() { 98 | this.printState(); 99 | }, 100 | 101 | 'userJoin': function(data) { 102 | channelMedia.announceRecording(ari, data.channelId, bridge); 103 | }, 104 | 105 | 'userExit': function(data) { 106 | if (data.confBridge.channels.length === 1 && 107 | data.confBridge.id === bridge.id) { 108 | this.transition('single'); 109 | } 110 | } 111 | } 112 | } 113 | 114 | }); 115 | 116 | return fsm; 117 | } 118 | 119 | module.exports = createFsm; 120 | -------------------------------------------------------------------------------- /lib/helpers/fsm/modules/bridgedriver.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Q = require('q'); 4 | var util = require('util'); 5 | 6 | /** 7 | * A module for finite state machines that handles bridge operations. 8 | */ 9 | function BridgeDriverModule() { 10 | 11 | var currentPlayback = null; 12 | 13 | /** 14 | * Sets the bridge to its default settings. 15 | * 16 | * @param {Object} bridge - the bridge to reset 17 | */ 18 | this.setToDefault = function(bridge) { 19 | bridge.locked = false; 20 | bridge.recordingPaused = true; 21 | bridge.recordingEnabled = false; 22 | console.log('Bridge auto-unlocked'); 23 | }; 24 | 25 | /** 26 | * Places the channel into the bridge. 27 | * 28 | * @param {Object} channel - the channel being placed in the bridge 29 | * @param {Object} bridge - the bridge to place the channel in 30 | */ 31 | this.addToBridge = function(channel, bridge) { 32 | var add = Q.denodeify(bridge.addChannel.bind(bridge)); 33 | return add({channel: channel.id}) 34 | .catch(function(err) { 35 | console.error(err); 36 | }); 37 | }; 38 | 39 | /** 40 | * Removes a channel from the bridge. 41 | * 42 | * @param {Object} channel - the channel being removed from the bridge 43 | * @param {Object} bridge - the bridge to remove the channel from 44 | */ 45 | this.removeFromBridge = function(channel, bridge) { 46 | var remove = Q.denodeify(bridge.removeChannel.bind(bridge)); 47 | return remove({channel: channel.id}) 48 | .catch(function(err) { 49 | console.error(err); 50 | }); 51 | }; 52 | 53 | /** 54 | * Lets a channel know the bridge is locked, then hangs up the channel. 55 | * 56 | * @param {Object} ari - the ARI client 57 | * @param {Object} channel - the channel attempting to join the bridge 58 | * @param {Object} bridge - the bridge the channel is in 59 | */ 60 | this.bridgeIsLocked = function(ari, channel, bridge) { 61 | var playback = ari.Playback(); 62 | var soundToPlay = util.format('sound:%s', 63 | bridge.settings.locked_sound); 64 | var play = Q.denodeify(channel.play.bind(channel)); 65 | play({media: soundToPlay}, playback) 66 | .catch(function(err) { 67 | console.error(err); 68 | }) 69 | .done(); 70 | playback.once('PlaybackFinished', function (event, completedPlayback) { 71 | var hangup = Q.denodeify(channel.hangup.bind(channel)); 72 | hangup() 73 | .catch(function (err) { 74 | console.error(err); 75 | }) 76 | .done(); 77 | }); 78 | }; 79 | 80 | /** 81 | * Kicks the last user that joined the conference from the bridge. 82 | * 83 | * @param {Object} ari - the ARI client 84 | * @param {Object} bridge - the bridge the channel is in 85 | * @param {Object} users - an array of users in the application 86 | */ 87 | this.kickLast = function(ari, bridge, users) { 88 | if (bridge.lastJoined) { 89 | var chanId = bridge.lastJoined.pop(); 90 | users[chanId].fsm.transition('inactive'); 91 | var soundToPlay = util.format('sound:%s', 92 | bridge.settings.kicked_sound); 93 | var remove = Q.denodeify(bridge.removeChannel.bind(bridge)); 94 | var playback = ari.Playback(); 95 | remove({channel: chanId}) 96 | .then(function () { 97 | var play = Q.denodeify(ari.channels.play.bind(ari)); 98 | return play({channelId: chanId, media: soundToPlay, 99 | playbackId: playback.id}); 100 | }) 101 | .catch(function (err) { 102 | console.error(err); 103 | }) 104 | .done(); 105 | playback.once('PlaybackFinished', function (event, 106 | completedPlayback) { 107 | var hangup = Q.denodeify(ari.channels.hangup.bind(ari)); 108 | hangup({channelId: chanId}) 109 | .catch(function (err) { 110 | console.error(err); 111 | }) 112 | .done(); 113 | }); 114 | } 115 | }; 116 | 117 | /** 118 | * Toggles the lock on the bridge, determining whether or not any more users 119 | * can join. 120 | * 121 | * @param {Object} ari - the ARI client 122 | * @param {Object} bridge - the bridge being locked / unlocked 123 | */ 124 | this.toggleLock = function(ari, bridge) { 125 | if (currentPlayback) { 126 | var stopPlayback = Q.denodeify(currentPlayback.stop.bind( 127 | currentPlayback)); 128 | stopPlayback() 129 | .catch(function (err) { 130 | return; 131 | }) 132 | .done(); 133 | } 134 | currentPlayback = ari.Playback(); 135 | if (!bridge.locked) { 136 | var soundToPlay = util.format('sound:%s', 137 | bridge.settings.now_locked_sound); 138 | var play = Q.denodeify(bridge.play.bind(bridge)); 139 | play({media: soundToPlay}, currentPlayback) 140 | .catch(function (err) { 141 | console.error(err); 142 | }) 143 | .done(); 144 | } 145 | else { 146 | var soundToPlay = util.format('sound:%s', 147 | bridge.settings.now_unlocked_sound); 148 | var play = Q.denodeify(bridge.play.bind(bridge)); 149 | play({media: soundToPlay}, currentPlayback) 150 | .catch(function (err) { 151 | console.error(err); 152 | }) 153 | .done(); 154 | } 155 | bridge.locked = !bridge.locked; 156 | }; 157 | 158 | } 159 | 160 | module.exports = BridgeDriverModule; 161 | -------------------------------------------------------------------------------- /lib/helpers/fsm/modules/channelmedia.js: -------------------------------------------------------------------------------- 1 | 'use-strict'; 2 | 3 | var Q = require('q'); 4 | var util = require('util'); 5 | 6 | var LISTEN_VOLUME = 'VOLUME(TX)'; 7 | var TALK_VOLUME = 'VOLUME(RX)'; 8 | var PITCH_SHIFT = 'PITCH_SHIFT(RX)'; 9 | var USER_TO_BRIDGE = 'in'; 10 | 11 | /** 12 | * A module for finite state machines that handles media operations for users. 13 | */ 14 | function ChannelMediaModule() { 15 | 16 | var self = this; 17 | 18 | var currentPlayback = null; 19 | var listenCounter = 1; 20 | var talkCounter = 1; 21 | var pitchChanger = 0; 22 | var muted = false; 23 | var deafMuted = false; 24 | 25 | /** 26 | * Mutes and unmutes the channel. 27 | * 28 | * @param {Object} ari - the ARI client 29 | * @param {Object} bridge - the bridge the channel is in 30 | * @param {Object} channel - the channel to mute / unmute 31 | */ 32 | this.muteChannel = function(ari, bridge, channel) { 33 | if (currentPlayback) { 34 | var stopPlayback = Q.denodeify(currentPlayback.stop.bind( 35 | currentPlayback)); 36 | stopPlayback() 37 | .catch(function (err) { 38 | return; 39 | }) 40 | .done(); 41 | } 42 | currentPlayback = ari.Playback(); 43 | if (!muted) { 44 | var soundToPlay = util.format('sound:%s', 45 | bridge.settings.now_muted_sound); 46 | var mute = Q.denodeify(channel.mute.bind(channel)); 47 | mute({direction: USER_TO_BRIDGE}) 48 | .then(function () { 49 | muted = true; 50 | var play = Q.denodeify(channel.play.bind(channel)); 51 | return play({media: soundToPlay}, currentPlayback); 52 | }) 53 | .catch(function (err) { 54 | console.error(err); 55 | }) 56 | .done(); 57 | } 58 | else { 59 | var soundToPlay = util.format('sound:%s', 60 | bridge.settings.now_unmuted_sound); 61 | var unmute = Q.denodeify(channel.unmute.bind(channel)); 62 | unmute({direction: USER_TO_BRIDGE}) 63 | .then(function () { 64 | muted = false; 65 | var play = Q.denodeify(channel.play.bind(channel)); 66 | return play({media: soundToPlay}, currentPlayback); 67 | }) 68 | .catch(function (err) { 69 | console.error(err); 70 | }) 71 | .done(); 72 | } 73 | }; 74 | 75 | /** 76 | * Deaf mutes and undeaf mutes the channel. 77 | * 78 | * @param {Object} channel - the channel to deaf mute / undeaf mute 79 | */ 80 | this.deafMuteChannel = function(channel) { 81 | if (!deafMuted) { 82 | var mute = Q.denodeify(channel.mute.bind(channel)); 83 | mute({direction: 'both'}) 84 | .then(function () { 85 | deafMuted = true; 86 | console.log('Channel is deaf muted'); 87 | }) 88 | .catch(function (err) { 89 | console.error(err); 90 | }) 91 | .done(); 92 | } 93 | else { 94 | var unmute = Q.denodeify(channel.unmute.bind(channel)); 95 | unmute({direction: 'both'}) 96 | .then(function () { 97 | deafMuted = false; 98 | console.log('Channel is no longer deaf muted'); 99 | }) 100 | .catch(function (err) { 101 | console.error(err); 102 | }) 103 | .done(); 104 | } 105 | }; 106 | 107 | /** 108 | * Increases the volume of the audio the channel hears. 109 | * 110 | * @param {Object} channel - the channel to change the volume of 111 | */ 112 | this.incrementListenVolume = function(channel) { 113 | if (listenCounter < 10) { 114 | listenCounter++; 115 | var setVar = Q.denodeify(channel.setChannelVar.bind(channel)); 116 | setVar({variable: LISTEN_VOLUME, value: listenCounter}) 117 | .catch(function (err) { 118 | console.error(err); 119 | }) 120 | .done(); 121 | } 122 | }; 123 | 124 | /** 125 | * Decreases the volume of the audio the channel hears. 126 | * 127 | * @param {Object} channel - the channel to change the volume of 128 | */ 129 | this.decrementListenVolume = function(channel) { 130 | if (listenCounter > -10) { 131 | listenCounter--; 132 | var setVar = Q.denodeify(channel.setChannelVar.bind(channel)); 133 | setVar({variable: LISTEN_VOLUME, value: listenCounter}) 134 | .catch(function (err) { 135 | console.error(err); 136 | }) 137 | .done(); 138 | } 139 | }; 140 | 141 | /** 142 | * Sets the volume of the audio the channel hears back to its default. 143 | * 144 | * @param {Object} channel - the channel to change the volume of 145 | */ 146 | this.resetListenVolume = function(channel) { 147 | listenCounter = 1; 148 | var setVar = Q.denodeify(channel.setChannelVar.bind(channel)); 149 | setVar({variable: LISTEN_VOLUME, value: listenCounter}) 150 | .catch(function (err) { 151 | console.error(err); 152 | }) 153 | .done(); 154 | }; 155 | 156 | /** 157 | * Increases the volume of the audio the channel outputs. 158 | * 159 | * @param {Object} channel - the channel to change the volume of 160 | */ 161 | this.incrementTalkVolume = function(channel) { 162 | if (talkCounter < 10) { 163 | talkCounter++; 164 | var setVar = Q.denodeify(channel.setChannelVar.bind(channel)); 165 | setVar({variable: TALK_VOLUME, value: talkCounter}) 166 | .catch(function (err) { 167 | console.error(err); 168 | }) 169 | .done(); 170 | } 171 | }; 172 | 173 | /** 174 | * Decreases the volume of the audio the channel outputs. 175 | * 176 | * @param {Object} channel - the channel to change the volume of 177 | */ 178 | this.decrementTalkVolume = function(channel) { 179 | if (talkCounter > -10) { 180 | talkCounter--; 181 | var setVar = Q.denodeify(channel.setChannelVar.bind(channel)); 182 | setVar({variable: TALK_VOLUME, value: talkCounter}) 183 | .catch(function (err) { 184 | console.error(err); 185 | }) 186 | .done(); 187 | } 188 | }; 189 | 190 | /** 191 | * Sets the volume of the audio the channel outputs back to its default. 192 | * 193 | * @param {Object} channel - the channel to change the volume of 194 | */ 195 | this.resetTalkVolume = function(channel) { 196 | talkCounter = 1; 197 | var setVar = Q.denodeify(channel.setChannelVar.bind(channel)); 198 | setVar({variable: TALK_VOLUME, value: talkCounter}) 199 | .catch(function (err) { 200 | console.error(err); 201 | }) 202 | .done(); 203 | }; 204 | 205 | /** 206 | * Changes the pitch of the audio the channel outputs. 207 | * 208 | * @param {Object} channel - the channel to change the pitch of 209 | */ 210 | this.pitchChange = function(channel) { 211 | if (pitchChanger === 0) { 212 | var setVar = Q.denodeify(channel.setChannelVar.bind(channel)); 213 | setVar({variable: PITCH_SHIFT, value: 0.7}) 214 | .then(function () { 215 | pitchChanger++; 216 | }) 217 | .catch(function (err) { 218 | console.error(err); 219 | }) 220 | .done(); 221 | } 222 | else if (pitchChanger === 1) { 223 | var setVar = Q.denodeify(channel.setChannelVar.bind(channel)); 224 | setVar({variable: PITCH_SHIFT, value: 'higher'}) 225 | .then(function () { 226 | pitchChanger++; 227 | }) 228 | .catch(function (err) { 229 | console.error(err); 230 | }) 231 | .done(); 232 | } 233 | else { 234 | var setVar = Q.denodeify(channel.setChannelVar.bind(channel)); 235 | setVar({variable: PITCH_SHIFT, value: 1.0}) 236 | .then(function () { 237 | pitchChanger = 0; 238 | }) 239 | .catch(function (err) { 240 | console.error(err); 241 | }) 242 | .done(); 243 | } 244 | }; 245 | 246 | /** 247 | * Starts music on hold for a channel. 248 | * 249 | * @param {Object} ari - the ARI client 250 | * @param {Number} chanId - the id of the channel to start moh for 251 | */ 252 | this.startMoh = function(ari, chanId) { 253 | var startMoh = Q.denodeify(ari.channels.startMoh.bind(ari)); 254 | startMoh({channelId: chanId}) 255 | .catch(function (err) { 256 | console.error(err); 257 | }) 258 | .done(); 259 | }; 260 | 261 | /** 262 | * Stops music on hold for a channel. 263 | * 264 | * @param {Object} ari - the ARI client 265 | * @param {Number} chanId - the id of the channel to stop moh for 266 | */ 267 | this.stopMoh = function(ari, chanId) { 268 | var stopMoh = Q.denodeify(ari.channels.stopMoh.bind(ari)); 269 | stopMoh({channelId: chanId}) 270 | .catch(function (err) { 271 | console.error(err); 272 | }) 273 | .done(); 274 | }; 275 | 276 | /** 277 | * Lets the follower know that it is waiting for a leader. 278 | * 279 | * @param {Object} ari - the ARI client 280 | * @param {Object} channel - the channel to notify 281 | * @param {Object} bridge - the bridge the channel is in 282 | */ 283 | this.waitingForLeader = function(ari, channel, bridge) { 284 | var playback = ari.Playback(); 285 | var soundToPlay = util.format('sound:%s', 286 | bridge.settings.wait_for_leader_sound); 287 | var play = Q.denodeify(channel.play.bind(channel)); 288 | play({media: soundToPlay}, playback) 289 | .catch(function (err) { 290 | console.error(err); 291 | }) 292 | .done(); 293 | playback.once('PlaybackFinished', function (event, completedPlayback) { 294 | self.startMoh(ari, channel.id); 295 | }); 296 | }; 297 | 298 | /** 299 | * Lets a user know that the conference is being recorded. 300 | * 301 | * @param {Object} ari - the ARI client 302 | * @param {Number} chanId - the channel id of the user 303 | * @param {Object} bridge - the bridge the channel is in 304 | */ 305 | this.announceRecording = function(ari, chanId, bridge) { 306 | if (!bridge.recordingPaused) { 307 | var soundToPlay = util.format('sound:%s',bridge.settings.recording_sound); 308 | var play = Q.denodeify(ari.channels.play.bind(ari)); 309 | play({media: soundToPlay, channelId: chanId}) 310 | .catch(function (err) { 311 | console.error(err); 312 | }) 313 | .done(); 314 | } 315 | }; 316 | 317 | } 318 | 319 | module.exports = ChannelMediaModule; 320 | -------------------------------------------------------------------------------- /lib/helpers/fsm/modules/pinauth.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Q = require('q'); 4 | var util = require('util'); 5 | 6 | /** 7 | * A module for finite state machines that handles PIN authorization. 8 | * 9 | * @return fsm - the fsm to return 10 | */ 11 | function PinAuthModule() { 12 | 13 | var currentPlayback = null; 14 | var retries = 0; 15 | var digits = ''; 16 | 17 | /** 18 | * Checks the entered PIN with the bridge PIN. 19 | * 20 | * @param {Integer} pin - the bridge PIN 21 | * @return {Boolean} result - true if the PIN matches 22 | */ 23 | this.checkPin = function(pin) { 24 | if (currentPlayback) { 25 | var stopPlayback = Q.denodeify(currentPlayback.stop.bind( 26 | currentPlayback)); 27 | stopPlayback() 28 | .catch(function (err) { 29 | return; 30 | }) 31 | .done(); 32 | } 33 | return parseInt(digits) === pin; 34 | }; 35 | 36 | /** 37 | * Concatenates a given digit to digits. 38 | * 39 | * @param {Integer} digit - the digit to add 40 | */ 41 | this.addDigit = function(digit) { 42 | digits += digit; 43 | }; 44 | 45 | /** 46 | * Asks the user to enter the PIN to the conference. 47 | * 48 | * @param {Object} ari - the ARI client 49 | * @param {Object} channel - the channel entering the PIN 50 | * @param {Object} bridge - the bridge the channel is trying to join 51 | */ 52 | this.enterPin = function(ari, channel, bridge) { 53 | currentPlayback = ari.Playback(); 54 | var soundToPlay = util.format('sound:%s', 55 | bridge.settings.enter_pin_sound); 56 | var play = Q.denodeify(channel.play.bind(channel)); 57 | play({media: soundToPlay}, currentPlayback) 58 | .catch(function(err) { 59 | console.error(err); 60 | }) 61 | .done(); 62 | }; 63 | 64 | /** 65 | * Lets the user know they entered an invallid PIN. If the maximum amount 66 | * of retries is reached, the channel will be hung up. 67 | * 68 | * @param {Object} ari - the ARI client 69 | * @param {Object} channel - the channel entering the PIN 70 | * @param {Object} bridge - the bridge the channel is trying to join 71 | */ 72 | this.invalidPin = function(ari, channel, bridge) { 73 | retries++; 74 | currentPlayback = ari.Playback(); 75 | var soundToPlay = util.format('sound:%s', 76 | bridge.settings.bad_pin_sound); 77 | var play = Q.denodeify(channel.play.bind(channel)); 78 | play({media: soundToPlay}, currentPlayback) 79 | .catch(function(err) { 80 | console.error(err); 81 | }) 82 | .done(); 83 | if (retries > bridge.settings.pin_retries) { 84 | currentPlayback.once('PlaybackFinished', function (err, completedPlayback) { 85 | var hangup = Q.denodeify(channel.hangup.bind(channel)); 86 | hangup() 87 | .catch(function (err) { 88 | console.error(err); 89 | }) 90 | .done(); 91 | }); 92 | } 93 | digits = ''; 94 | }; 95 | 96 | } 97 | 98 | module.exports = PinAuthModule; 99 | -------------------------------------------------------------------------------- /lib/helpers/fsm/modules/recordingdriver.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Q = require('q'); 4 | var util = require('util'); 5 | 6 | /** 7 | * A module for finite state machines that handles recording operations. 8 | */ 9 | function RecordingDriverModule() { 10 | 11 | var self = this; 12 | 13 | var currentPlayback = null; 14 | 15 | /** 16 | * Handles recording logic. 17 | * 18 | * @param {Object} ari - the ARI client 19 | * @param {Object} bridge - the bridge being recorded 20 | */ 21 | this.handleRecording = function(ari, bridge) { 22 | if (!bridge.recordingEnabled) { 23 | this.enableRecording(ari, bridge); 24 | } 25 | else if (!bridge.recordingPaused) { 26 | this.pauseRecording(ari, bridge); 27 | } 28 | else { 29 | this.unpauseRecording(ari, bridge); 30 | } 31 | }; 32 | 33 | /** 34 | * Enables recording for the bridge and starts it. 35 | * 36 | * @param {Object} ari - the ARI client 37 | * @param {Object} bridge - the bridge to record 38 | */ 39 | this.enableRecording = function(ari, bridge) { 40 | if (currentPlayback) { 41 | var stopPlayback = Q.denodeify(currentPlayback.stop.bind( 42 | currentPlayback)); 43 | stopPlayback() 44 | .catch(function (err) { 45 | return; 46 | }) 47 | .done(); 48 | } 49 | var currentPlayback = ari.Playback(); 50 | var soundToPlay = util.format('sound:%s', bridge.settings.recording_sound); 51 | var record = Q.denodeify(bridge.record.bind(bridge)); 52 | self.startRecording(bridge) 53 | .then(function () { 54 | var play = Q.denodeify(bridge.play.bind(bridge)); 55 | return play({media: soundToPlay}, currentPlayback); 56 | }) 57 | .catch(function (err) { 58 | console.error(err); 59 | }) 60 | .done(); 61 | }; 62 | 63 | /** 64 | * Pauses the bridge recording. 65 | * 66 | * @param {Object} ari - the ARI client 67 | * @param {Object} bridge - the bridge being recorded 68 | */ 69 | this.pauseRecording = function(ari, bridge) { 70 | var pause = Q.denodeify(ari.recordings.pause.bind(ari)); 71 | pause({recordingName: bridge.currentRecording.name}) 72 | .then(function () { 73 | bridge.recordingPaused = true; 74 | console.log('Recording paused'); 75 | }) 76 | .catch(function (err) { 77 | console.error(err); 78 | }) 79 | .done(); 80 | }; 81 | 82 | /** 83 | * Unpauses the bridge recording. 84 | * 85 | * @param {Object} ari - the ARI client 86 | * @param {Object} bridge - the bridge being recorded 87 | */ 88 | this.unpauseRecording = function(ari, bridge) { 89 | if (currentPlayback) { 90 | var stopPlayback = Q.denodeify(currentPlayback.stop.bind( 91 | currentPlayback)); 92 | stopPlayback() 93 | .catch(function (err) { 94 | return; 95 | }) 96 | .done(); 97 | } 98 | currentPlayback = ari.Playback(); 99 | var soundToPlay = util.format('sound:%s', bridge.settings.recording_sound); 100 | var unpause = Q.denodeify(ari.recordings.unpause.bind(ari)); 101 | unpause({recordingName: bridge.currentRecording.name}) 102 | .then(function () { 103 | bridge.recordingPaused = false; 104 | console.log('Recording unpaused'); 105 | var play = Q.denodeify(bridge.play.bind(bridge)); 106 | return play({media: soundToPlay}, currentPlayback); 107 | }) 108 | .catch(function (err) { 109 | console.error(err); 110 | }) 111 | .done(); 112 | }; 113 | 114 | /** 115 | * Starts recording for the bridge. 116 | * 117 | * @param {Object} bridge - the bridge to start the recording for 118 | */ 119 | this.startRecording = function(bridge) { 120 | var record = Q.denodeify(bridge.record.bind(bridge)); 121 | return record({format: 'wav', name: bridge.currentRecording.name, 122 | terminateOn: 'none'}) 123 | .then(function() { 124 | bridge.recordingPaused = false; 125 | bridge.recordingEnabled = true; 126 | }) 127 | .catch(function (err) { 128 | console.error(err); 129 | }); 130 | }; 131 | 132 | } 133 | 134 | module.exports = RecordingDriverModule; 135 | -------------------------------------------------------------------------------- /lib/helpers/fsm/userfsm.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var machina = require('machina'); 4 | var util = require('util'); 5 | var Q = require('q'); 6 | var config = require('../../../config.json'); 7 | var PinAuthModule = require('./modules/pinauth.js'); 8 | var BridgeDriverModule = require('./modules/bridgedriver.js'); 9 | var ChannelMediaModule = require('./modules/channelmedia.js'); 10 | var RecordingDriverModule = require('./modules/recordingdriver.js'); 11 | 12 | /** 13 | * Removes the user from the conference and sends them to the configured 14 | * context, extension, and priority in the dialplan. 15 | */ 16 | var continueInDialplan = function(self, channel) { 17 | var dialplanContinue = Q.denodeify(channel.continueInDialplan.bind( 18 | channel)); 19 | dialplanContinue({context: config.leaveConf.context, 20 | extension: config.leaveConf.extension, 21 | priority: config.leaveConf.priority}) 22 | .then(function () { 23 | console.log('Continuing in the dialplan'); 24 | self.transition('inactive'); 25 | }) 26 | .catch(function (err) { 27 | console.error(err); 28 | }) 29 | .done(); 30 | }; 31 | 32 | /** 33 | * Creates an fsm for a user and returns it. 34 | * 35 | * @param {Object} channel - the channel the fsm is being created for 36 | * @param {Object} ari - the ARI client 37 | * @param {Object} userSettings - the settings for the user 38 | * @param {Object} users - the users present in the conference 39 | * @param {Object} bridge - the bridge the user is a part of 40 | * @param {Object} groups - the groups instance that handles leaders/followers 41 | * @return fsm - the fsm to return 42 | */ 43 | function createFsm(channel, ari, userSettings, users, bridge, groups) { 44 | 45 | // Different modules needed by the fsm. 46 | var pinAuth = new PinAuthModule(); 47 | var bridgeDriver = new BridgeDriverModule(); 48 | var channelMedia = new ChannelMediaModule(); 49 | var recordingDriver = new RecordingDriverModule(); 50 | 51 | var fsm = new machina.Fsm({ 52 | 53 | initialState: 'inactive', 54 | 55 | printState: function() { 56 | console.log('Channel entered state', this.state); 57 | }, 58 | 59 | isActive: function() { 60 | return this.state === 'active' || this.state === 'admin'; 61 | }, 62 | 63 | isInactive: function() { 64 | return this.state === 'inactive' || this.state === 'auth' || 65 | this.state === 'waiting'; 66 | }, 67 | 68 | states: { 69 | 70 | /** 71 | * The channel waits in this state until the application 72 | * is ready to interact with it. 73 | */ 74 | 'inactive': { 75 | _onEnter: function() { 76 | this.printState(); 77 | }, 78 | 79 | ready: function() { 80 | this.transition('auth'); 81 | } 82 | }, 83 | 84 | /** 85 | * The channel waits in this state until the application lets it know 86 | * that the conference is locked, or if a PIN code is required. Stays in 87 | * this state until a correct PIN is entered, or if no PIN is required, 88 | * it is immediately placed in the conference. 89 | */ 90 | 'auth': { 91 | _onEnter: function() { 92 | var self = this; 93 | this.printState(); 94 | if (bridge.locked) { 95 | bridgeDriver.bridgeIsLocked(ari, channel); 96 | } 97 | else { 98 | if (!userSettings.pin_auth) { 99 | if (groups.isFollower(users, channel.id) && 100 | !groups.containsLeaders()) { 101 | this.transition('waiting'); 102 | } 103 | else { 104 | bridgeDriver.addToBridge(channel, bridge) 105 | .then(function() { 106 | self.transition('active'); 107 | }) 108 | .catch(function (err) { 109 | console.error(err); 110 | }) 111 | .done(); 112 | } 113 | } 114 | else { 115 | pinAuth.enterPin(ari, channel, bridge); 116 | } 117 | } 118 | }, 119 | 120 | dtmf: function(data) { 121 | var self = this; 122 | if (data.digit === config.waitingInput.verify) { 123 | if (pinAuth.checkPin(bridge.settings.pin_number)) { 124 | if (groups.isFollower(users, channel.id) && 125 | !groups.containsLeaders()) { 126 | this.transition('waiting'); 127 | } 128 | else { 129 | bridgeDriver.addToBridge(channel, bridge) 130 | .then(function() { 131 | self.transition('active'); 132 | }) 133 | .catch(function (err) { 134 | console.error(err); 135 | }) 136 | .done(); 137 | } 138 | } 139 | else { 140 | pinAuth.invalidPin(ari, channel, bridge); 141 | } 142 | } 143 | else { 144 | pinAuth.addDigit(data.digit); 145 | } 146 | }, 147 | 148 | done: function() { 149 | this.transition('inactive'); 150 | } 151 | }, 152 | 153 | /** 154 | * This state stores all followers when there is not a leader present in 155 | * the conference. 156 | */ 157 | 'waiting': { 158 | _onEnter: function() { 159 | this.printState(); 160 | channelMedia.waitingForLeader(ari, channel, bridge); 161 | }, 162 | 163 | leaderJoined: function() { 164 | var self = this; 165 | bridgeDriver.addToBridge(channel, bridge) 166 | .then(function() { 167 | self.transition('active'); 168 | }) 169 | .catch(function (err) { 170 | console.error(err); 171 | }) 172 | .done(); 173 | }, 174 | 175 | done: function() { 176 | this.transition('inactive'); 177 | }, 178 | 179 | _onExit: function() { 180 | channelMedia.stopMoh(ari, channel.id); 181 | } 182 | }, 183 | 184 | /** 185 | * While the channel is in this state, it can interact with other users in 186 | * the conference. 187 | */ 188 | 'active': { 189 | _onEnter: function() { 190 | this.printState(); 191 | }, 192 | 193 | dtmf: function(data) { 194 | var self = this; 195 | switch (data.digit) { 196 | 197 | // Transition to admin menu if user has admin flag 198 | case config.menuInput.admin: 199 | if (userSettings.admin) { 200 | self.transition('admin'); 201 | } 202 | break; 203 | 204 | // Mutes the channel 205 | case config.menuInput.mute: 206 | channelMedia.muteChannel(ari, bridge, channel); 207 | break; 208 | 209 | // Deaf mutes the channel 210 | case config.menuInput.deafMute: 211 | channelMedia.deafMuteChannel(channel); 212 | break; 213 | 214 | // Leaves the conference and executes configured dialplan 215 | case config.menuInput.contInDialplan: 216 | continueInDialplan(self, channel); 217 | break; 218 | 219 | // Decreases audio volume the channel hears 220 | case config.menuInput.decLisVol: 221 | channelMedia.decrementListenVolume(channel); 222 | break; 223 | 224 | // Resets audio volume the channel hears 225 | case config.menuInput.resetLisVol: 226 | channelMedia.resetListenVolume(channel); 227 | break; 228 | 229 | // Increases audio volume the channel hears 230 | case config.menuInput.incLisVol: 231 | channelMedia.incrementListenVolume(channel); 232 | break; 233 | 234 | // Decreases audio volume the channel outputs 235 | case config.menuInput.decTalkVol: 236 | channelMedia.decrementTalkVolume(channel); 237 | break; 238 | 239 | // Resets audio volume the channel outputs 240 | case config.menuInput.resetTalkVol: 241 | channelMedia.resetTalkVolume(channel); 242 | break; 243 | 244 | // Increases audio volume the channel outputs 245 | case config.menuInput.incTalkVol: 246 | channelMedia.incrementTalkVolume(channel); 247 | break; 248 | 249 | // Changes the pitch of the audio the channel outputs 250 | case config.menuInput.pitchChange: 251 | channelMedia.pitchChange(channel); 252 | break; 253 | 254 | // DTMF has no specified functionality 255 | default: 256 | console.log(util.format('%s is not a recognized DTMF keybind', 257 | data.digit)); 258 | break; 259 | } 260 | }, 261 | 262 | noLeaders: function() { 263 | var self = this; 264 | bridgeDriver.removeFromBridge(channel, bridge) 265 | .then(function() { 266 | self.transition('waiting'); 267 | }) 268 | .catch(function (err) { 269 | console.error(err); 270 | }) 271 | .done(); 272 | }, 273 | 274 | done: function() { 275 | this.transition('inactive'); 276 | } 277 | }, 278 | 279 | /** 280 | * While the channel is in this state, it can interact with other users 281 | * and the bridge in a way unique to an admin. Only users with the admin 282 | * flag set to true in the database can access this state. 283 | */ 284 | 'admin': { 285 | _onEnter: function() { 286 | this.printState(); 287 | }, 288 | 289 | dtmf: function(data) { 290 | var self = this; 291 | switch (data.digit) { 292 | 293 | // Transition back to active menu 294 | case config.adminInput.menu: 295 | self.transition('active'); 296 | break; 297 | 298 | // Lock or unlock the conference 299 | case config.adminInput.toggleLock: 300 | bridgeDriver.toggleLock(ari, bridge); 301 | break; 302 | 303 | // Kick the last user that joined the conference 304 | case config.adminInput.kick: 305 | bridgeDriver.kickLast(ari, bridge, users); 306 | break; 307 | 308 | // Start recording, pause recording, or unpause recording 309 | case config.adminInput.toggleRecord: 310 | recordingDriver.handleRecording(ari, bridge); 311 | break; 312 | 313 | // DTMF has no specified functionality 314 | default: 315 | console.log(util.format('%s is not a recognized DTMF keybind', 316 | data.digit)); 317 | break; 318 | } 319 | }, 320 | 321 | noLeaders: function() { 322 | var self = this; 323 | bridgeDriver.removeFromBridge(channel, bridge) 324 | .then(function() { 325 | self.transition('waiting'); 326 | }) 327 | .catch(function (err) { 328 | console.error(err); 329 | }) 330 | .done(); 331 | }, 332 | 333 | done: function() { 334 | this.transition('inactive'); 335 | } 336 | }, 337 | } 338 | 339 | }); 340 | 341 | return fsm; 342 | } 343 | 344 | module.exports = createFsm; 345 | -------------------------------------------------------------------------------- /lib/helpers/groupsetup.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var util = require('util'); 4 | 5 | function GroupSetup() { 6 | 7 | var groups = {}; 8 | var followers = {}; 9 | var leaders = {}; 10 | 11 | /** 12 | * Returns the followers array. 13 | * 14 | * @return {Object} followers - the array of followers 15 | */ 16 | this.getFollowers = function() { 17 | return followers; 18 | }; 19 | 20 | /** 21 | * Adds a channel to the follower array. 22 | * 23 | * @param {Object} channel - the channel to add 24 | */ 25 | this.addFollower = function(channel) { 26 | followers[channel.id] = channel; 27 | }; 28 | 29 | /** 30 | * Removes a channel from the follower array. 31 | * 32 | * @param {Object} channel - the channel to remove 33 | */ 34 | this.removeFollower = function(channel) { 35 | delete followers[channel.id]; 36 | }; 37 | 38 | /** 39 | * Checks to see if a given user is a follower or not. 40 | * 41 | * @param {Object} userList - the list of users 42 | * @param {Integer} chanId - the users channel id 43 | * @return {Boolean} result - true if the user is a follower 44 | */ 45 | this.isFollower = function(userList, chanId) { 46 | return userList[chanId].group.group_behavior === 'follower'; 47 | }; 48 | 49 | /** 50 | * Adds a channel to the leader array. 51 | * 52 | * @param {Object} channel - the channel to add 53 | */ 54 | this.addLeader = function(channel) { 55 | leaders[channel.id] = channel; 56 | }; 57 | 58 | /** 59 | * Removes a channel from the leader array. 60 | * 61 | * @param {Object} channel - the channel to remove 62 | */ 63 | this.removeLeader = function(channel) { 64 | delete leaders[channel.id]; 65 | }; 66 | 67 | /** 68 | * Determines if there are leaders in the conference or not. 69 | * 70 | * @return {Boolean} result - true if there are leaders 71 | */ 72 | this.containsLeaders = function() { 73 | return Object.keys(leaders).length > 0; 74 | }; 75 | 76 | /** 77 | * Checks to see if a given user is a leader or not. 78 | * 79 | * @param {Object} userList - the list of users 80 | * @param {Integer} chanId - the users channel id 81 | * @return {Boolean} result - true if the user is a leader 82 | */ 83 | this.isLeader = function(userList, chanId) { 84 | return userList[chanId].group.group_behavior === 'leader'; 85 | }; 86 | 87 | /** 88 | * Adds one to this group type. 89 | * 90 | * @param {String} groupType - the type of group to add to 91 | */ 92 | this.addToGroup = function(groupType) { 93 | groups[groupType] = groups[groupType] || 0; 94 | groups[groupType] += 1; 95 | console.log(util.format('%s: %s', groupType, groups[groupType])); 96 | }; 97 | 98 | /** 99 | * Removes one from this group type. 100 | * 101 | * @param {String} groupType - the type of group to remove from 102 | */ 103 | this.removeFromGroup = function(groupType) { 104 | if (groups[groupType] > 0) { 105 | groups[groupType] -= 1; 106 | } 107 | console.log(util.format('%s: %s', groupType, groups[groupType])); 108 | }; 109 | 110 | /** 111 | * Checks to see if the given group is full. 112 | * 113 | * @param {String} groupType - the group to check 114 | * @param {Integer} groupMax - the maximum amount of members allowed 115 | * @return {Boolean} result - true if the group is full 116 | */ 117 | this.groupIsFull = function(groupType, groupMax) { 118 | // Make sure our group already exists! 119 | groups[groupType] = groups[groupType] || 0; 120 | return groups[groupType] >= groupMax; 121 | }; 122 | 123 | } 124 | 125 | module.exports = GroupSetup; 126 | -------------------------------------------------------------------------------- /lib/helpers/usersetup.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Q = require('q'); 4 | var userFsm = require('./fsm/userfsm.js'); 5 | var util = require('util'); 6 | 7 | /** 8 | * This class keeps up with users that join the conference. 9 | * 10 | * @param {Object} ari - the ARI client 11 | * @param {Object} db - the database module 12 | * @param {Object} groups - the group setup module 13 | */ 14 | function UserSetup(ari, db, groups) { 15 | var self = this; 16 | 17 | /** 18 | * Contains all users that join the conference, where the index is the 19 | * channel id. 20 | */ 21 | var userList = {}; 22 | 23 | /** 24 | * Takes care of clean up when a channel leaves the applicaton. 25 | * 26 | * @param {Object} event - the StasisEnd event 27 | * @param {Object} channel - the channel leaving Stasis 28 | */ 29 | ari.on('StasisEnd', function (event, channel) { 30 | if (userList[channel.id]) { 31 | var groupType = userList[channel.id].group.group_type; 32 | groups.removeFromGroup(groupType); 33 | if (groups.isFollower(userList, channel.id)) { 34 | groups.removeFollower(channel); 35 | } 36 | self.deleteUser(channel); 37 | self.unregisterEvents(channel); 38 | } 39 | }); 40 | 41 | /** 42 | * Stores a user and their configuration in the users array. 43 | * 44 | * @param {Object} event - the event object 45 | * @param {Object} channel - the channel to add 46 | * @param {Object} bridge - the bridge the channel is entering 47 | */ 48 | this.storeUser = function(event, channel, bridge) { 49 | var chanID = channel.id; 50 | var userType = 'default'; 51 | var groupType = 'default'; 52 | if (event.args[0]) { 53 | userType = event.args[0]; 54 | } 55 | if (event.args[1]) { 56 | groupType = event.args[1]; 57 | } 58 | var userSettings = null; 59 | var groupSettings = null; 60 | db.getUserProfile(userType) 61 | .then(function (result) { 62 | userSettings = result; 63 | return db.getGroupProfile(groupType); 64 | }) 65 | .then(function (result) { 66 | groupSettings = result; 67 | }) 68 | .then(function () { 69 | var fsm = userFsm(channel, ari, userSettings, userList, bridge, 70 | groups); 71 | userList[chanID] = { channel: channel, settings: userSettings, 72 | fsm: fsm, group: groupSettings }; 73 | if (groups.isFollower(userList, channel.id)) { 74 | groups.addFollower(channel); 75 | } 76 | }) 77 | .then(function () { 78 | self.registerEvents(channel); 79 | }) 80 | .then(function () { 81 | var group = userList[chanID].group.group_type; 82 | var groupMax = userList[chanID].group.max_members; 83 | if (groups.groupIsFull(group, groupMax)) { 84 | self.hangupChannel(channel, groupType); 85 | } 86 | else { 87 | groups.addToGroup(group); 88 | userList[chanID].fsm.handle('ready'); 89 | } 90 | }) 91 | .catch(function (err) { 92 | console.error(err); 93 | }) 94 | .done(); 95 | }; 96 | 97 | /** 98 | * Deletes a user from the users array. 99 | * 100 | * @param {Object} channel - the channel to delete 101 | */ 102 | this.deleteUser = function(channel) { 103 | var chanID = channel.id; 104 | if (userList[chanID]) { 105 | delete(userList[chanID]); 106 | } 107 | }; 108 | 109 | /** 110 | * Registers event listeners to the channel. 111 | * 112 | * @param {Object} channel - the channel to register events to 113 | */ 114 | this.registerEvents = function(channel) { 115 | channel.on('ChannelDtmfReceived', this.dtmfHandler); 116 | }; 117 | 118 | /** 119 | * Unregisters event listeners to the channel. 120 | * 121 | * @param {Object} channel - the channel to unregister events for 122 | */ 123 | this.unregisterEvents = function(channel) { 124 | channel.removeListener('ChannelDtmfReceived', this.dtmfHandler); 125 | }; 126 | 127 | /** 128 | * Returns the list of users, indexed by channel id. 129 | * 130 | * @return {Object} userList - the list of users 131 | */ 132 | this.getUsers = function() { 133 | return userList; 134 | }; 135 | 136 | /** 137 | * The function to call when a DTMF is received. 138 | * 139 | * @param {Object} event - the DTMF event object 140 | */ 141 | this.dtmfHandler = function(event) { 142 | userList[event.channel.id].fsm.handle('dtmf', { digit: event.digit }); 143 | }; 144 | 145 | /** 146 | * Called when a channel is considered done in the application. 147 | * 148 | * @param {Object} channel - the channel done with the application 149 | */ 150 | this.handleDone = function(channel) { 151 | userList[channel.id].fsm.handle('done'); 152 | }; 153 | 154 | /** 155 | * Hangs up a given channel. Used for users that attempt to enter a group 156 | * that is aleady full. 157 | * 158 | * @param {Object} channel - the channel to hang up 159 | * @param {String} groupType - the group that is full 160 | */ 161 | this.hangupChannel = function(channel, groupType) { 162 | var hangup = Q.denodeify(channel.hangup.bind(channel)); 163 | hangup() 164 | .then(function() { 165 | console.log(util.format('Group \'%s\' is full', groupType)); 166 | }) 167 | .catch(function(err) { 168 | console.error(err); 169 | }) 170 | .done(); 171 | }; 172 | 173 | } 174 | 175 | module.exports = UserSetup; 176 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "asterisk-confbridge", 3 | "version": "0.0.1", 4 | "description": "ConfBridge using ARI", 5 | "private": true, 6 | "main": "app.js", 7 | "keywords": [ 8 | "ConfBridge", 9 | "ARI", 10 | "Asterisk" 11 | ], 12 | "author": [ 13 | "Scott Emidy", 14 | "Ben Ford" 15 | ], 16 | "license": "Apache-2.0", 17 | "dependencies": { 18 | "sql": "^0.39.0", 19 | "q": "^1.0.1", 20 | "ari-client": "^0.1.1", 21 | "machina": "^0.4.0-1", 22 | "node-uuid": "1.4.0" 23 | }, 24 | "devDependencies": { 25 | "mocha": "^1.20.1", 26 | "grunt": "^0.4.5" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /scripts/addbridge.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var pg = require('pg'); 4 | var Q = require('q'); 5 | var prompt = require('prompt'); 6 | var config = require('../config.json'); 7 | 8 | var get = Q.denodeify(prompt.get.bind(prompt)); 9 | var connect = Q.denodeify(pg.connect.bind(pg)); 10 | var bridgeProfile = ''; 11 | 12 | console.log('Enter the name of the bridge profile to create:'); 13 | get('name') 14 | .then(function (result) { 15 | bridgeProfile = result.name; 16 | console.log('Connecting to database...'); 17 | return connect(config.dbConnection); 18 | }) 19 | .then(function (result) { 20 | var client = result[0]; 21 | var done = result[1]; 22 | var query = Q.denodeify(client.query.bind(client)); 23 | 24 | return query('SELECT exists(SELECT 1 FROM bridge_profile where bridge_type ' 25 | + '= $1)', [bridgeProfile]) 26 | .then(function (result) { 27 | if (!result.rows[0].exists) { 28 | console.log('...inserting bridge into database'); 29 | return query('INSERT INTO bridge_profile (bridge_type,join_sound,' 30 | + 'leave_sound,pin_number,pin_retries,enter_pin_sound,' 31 | + 'bad_pin_sound,locked_sound,now_locked_sound,' 32 | + 'now_unlocked_sound,now_muted_sound,' 33 | + 'now_unmuted_sound,kicked_sound,record_conference,' 34 | + 'recording_sound,wait_for_leader_sound) VALUES ' 35 | + '($1,\'confbridge-join\',\'confbridge-leave\',1234,' 36 | + '3,\'confbridge-pin\',\'conf-invalidpin\',' 37 | + '\'confbridge-lock-no-join\',\'confbridge-locked\',' 38 | + '\'confbridge-unlocked\',\'confbridge-muted\',' 39 | + '\'confbridge-unmuted\',\'confbridge-removed\',' 40 | + 'false,\'conf-now-recording\',\'conf-waitforleader\')', 41 | [bridgeProfile]); 42 | } 43 | else { 44 | console.log('...bridge already exists. Aborting.'); 45 | process.exit(0); 46 | } 47 | }) 48 | .then(function () { 49 | console.log('...disconnecting from database.'); 50 | }) 51 | .catch(function (err) { 52 | console.error(err); 53 | }) 54 | .finally(function () { 55 | done(); 56 | }); 57 | }) 58 | .catch(function (err) { 59 | console.error(err); 60 | }) 61 | .finally(function () { 62 | process.exit(0); 63 | }) 64 | .done(); 65 | -------------------------------------------------------------------------------- /scripts/addgroup.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var pg = require('pg'); 4 | var Q = require('q'); 5 | var prompt = require('prompt'); 6 | var config = require('../config.json'); 7 | 8 | var groupValues = []; 9 | 10 | var get = Q.denodeify(prompt.get.bind(prompt)); 11 | var connect = Q.denodeify(pg.connect.bind(pg)); 12 | 13 | console.log('Enter the name of the group to create:'); 14 | get('name') 15 | .then(function (result) { 16 | groupValues[0] = result.name; 17 | console.log('Behavior? (leader/follower/participant)'); 18 | return get('behavior'); 19 | }) 20 | .then(function (result) { 21 | groupValues[1] = result.behavior; 22 | console.log('Max members?'); 23 | return get('max_members'); 24 | }) 25 | .then(function (result) { 26 | groupValues[2] = result.max_members; 27 | console.log('Connecting to database...'); 28 | return connect(config.dbConnection); 29 | }) 30 | .then(function (result) { 31 | var client = result[0]; 32 | var done = result[1]; 33 | var query = Q.denodeify(client.query.bind(client)); 34 | 35 | return query('SELECT exists(SELECT 1 FROM group_profile where group_type = ' 36 | + '$1)', [groupValues[0]]) 37 | .then(function (result) { 38 | if (!result.rows[0].exists) { 39 | console.log('...inserting group into database'); 40 | return query('INSERT INTO group_profile (group_type,group_behavior,' 41 | + 'max_members) VALUES ($1,$2,$3)', [groupValues[0], 42 | groupValues[1],groupValues[2]]); 43 | } 44 | else { 45 | console.log('...group already exists. Aborting.'); 46 | process.exit(0); 47 | } 48 | }) 49 | .then(function () { 50 | console.log('...disconnecting from database.'); 51 | }) 52 | .catch(function (err) { 53 | console.error(err); 54 | }) 55 | .finally(function () { 56 | done(); 57 | }); 58 | }) 59 | .catch(function (err) { 60 | console.error(err); 61 | }) 62 | .finally(function () { 63 | process.exit(0); 64 | }) 65 | .done(); 66 | -------------------------------------------------------------------------------- /scripts/adduser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var pg = require('pg'); 4 | var Q = require('q'); 5 | var prompt = require('prompt'); 6 | var config = require('../config.json'); 7 | 8 | var userValues = []; 9 | 10 | var get = Q.denodeify(prompt.get.bind(prompt)); 11 | var connect = Q.denodeify(pg.connect.bind(pg)); 12 | 13 | console.log('Enter the name of the user to create:'); 14 | get('name') 15 | .then(function (result) { 16 | userValues[0] = result.name; 17 | console.log('Admin? (true/false)'); 18 | return get('admin'); 19 | }) 20 | .then(function (result) { 21 | userValues[1] = result.admin; 22 | console.log('MOH? (true/false)'); 23 | return get('moh'); 24 | }) 25 | .then(function (result) { 26 | userValues[2] = result.moh; 27 | console.log('Quiet? (true/false)'); 28 | return get('quiet'); 29 | }) 30 | .then(function (result) { 31 | userValues[3] = result.quiet; 32 | console.log('Pin authorization? (true/false)'); 33 | return get('pin_auth'); 34 | }) 35 | .then(function (result) { 36 | userValues[4] = result.pin_auth; 37 | console.log('Connecting to database...'); 38 | return connect(config.dbConnection); 39 | }) 40 | .then(function (result) { 41 | var client = result[0]; 42 | var done = result[1]; 43 | var query = Q.denodeify(client.query.bind(client)); 44 | 45 | return query('SELECT exists(SELECT 1 FROM user_profile WHERE user_type = ' 46 | + '$1)', [userValues[0]]) 47 | .then(function (result) { 48 | if (!result.rows[0].exists) { 49 | console.log('...inserting user into database'); 50 | return query('INSERT INTO user_profile (user_type,admin,moh,quiet,' 51 | + 'pin_auth) VALUES ($1,$2,$3,$4,$5)', 52 | [userValues[0],userValues[1],userValues[2],userValues[3], 53 | userValues[4]]); 54 | } 55 | else { 56 | console.log('...user already exists. Aborting.'); 57 | process.exit(0); 58 | } 59 | }) 60 | .then(function () { 61 | console.log('...disconnecting from database.'); 62 | }) 63 | .catch(function (err) { 64 | console.error(err); 65 | }) 66 | .finally(function () { 67 | done(); 68 | }); 69 | }) 70 | .catch(function (err) { 71 | console.error(err); 72 | }) 73 | .finally(function () { 74 | process.exit(0); 75 | }) 76 | .done(); 77 | -------------------------------------------------------------------------------- /scripts/defaultdatabase.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var pg = require('pg'); 4 | var Q = require('q'); 5 | var config = require('../config.json'); 6 | 7 | var connect = Q.denodeify(pg.connect.bind(pg)); 8 | 9 | console.log('Preparing database...'); 10 | connect(config.dbConnection) 11 | .then(function (values) { 12 | console.log('...connected to database'); 13 | var client = values[0]; 14 | var done = values[1]; 15 | var query = Q.denodeify(client.query.bind(client)); 16 | 17 | return query('SELECT exists(SELECT * FROM information_schema.tables where' 18 | + ' table_name = \'bridge_profile\')') 19 | .then(function (result) { 20 | if (result.rows[0].exists) { 21 | console.log('...deleting bridge_profile'); 22 | return query('DROP TABLE bridge_profile'); 23 | } 24 | }) 25 | .then(function () { 26 | console.log('...creating bridge_profile'); 27 | return query('CREATE TABLE bridge_profile (' 28 | + 'bridge_type varchar (50) PRIMARY KEY,' 29 | + 'join_sound varchar (50) NOT NULL,' 30 | + 'leave_sound varchar (50) NOT NULL,' 31 | + 'pin_number integer NOT NULL,' 32 | + 'pin_retries integer NOT NULL,' 33 | + 'enter_pin_sound varchar (50) NOT NULL,' 34 | + 'bad_pin_sound varchar (50) NOT NULL,' 35 | + 'locked_sound varchar (50) NOT NULL,' 36 | + 'now_locked_sound varchar (50) NOT NULL,' 37 | + 'now_unlocked_sound varchar (50) NOT NULL,' 38 | + 'now_muted_sound varchar (50) NOT NULL,' 39 | + 'now_unmuted_sound varchar (50) NOT NULL,' 40 | + 'kicked_sound varchar (50) NOT NULL,' 41 | + 'record_conference boolean NOT NULL,' 42 | + 'recording_sound varchar (50) NOT NULL,' 43 | + 'wait_for_leader_sound varchar (50) NOT NULL)'); 44 | }) 45 | .then(function () { 46 | console.log('...inserting data into bridge_profile'); 47 | return query('INSERT INTO bridge_profile (' 48 | + 'bridge_type,join_sound,leave_sound,pin_number,' 49 | + 'pin_retries,enter_pin_sound,bad_pin_sound,' 50 | + 'locked_sound,now_locked_sound,now_unlocked_sound,' 51 | + 'now_muted_sound,now_unmuted_sound,kicked_sound,' 52 | + 'record_conference,recording_sound,' 53 | + 'wait_for_leader_sound) VALUES (' 54 | + '\'default\',\'confbridge-join\',' 55 | + '\'confbridge-leave\',1234,3,\'confbridge-pin\',' 56 | + '\'conf-invalidpin\',\'confbridge-lock-no-join\',' 57 | + '\'confbridge-locked\',\'confbridge-unlocked\',' 58 | + '\'confbridge-muted\',\'confbridge-unmuted\',' 59 | + '\'confbridge-removed\',false,\'conf-now-recording\',' 60 | + '\'conf-waitforleader\')'); 61 | }) 62 | .then(function () { 63 | return query('SELECT exists(SELECT * FROM information_schema.tables ' 64 | + 'where table_name = \'user_profile\')'); 65 | }) 66 | .then(function (result) { 67 | if (result.rows[0].exists) { 68 | console.log('...deleting user_profile'); 69 | return query('DROP TABLE user_profile'); 70 | } 71 | }) 72 | .then(function () { 73 | console.log('...creating user_profile'); 74 | return query('CREATE TABLE user_profile (' 75 | + 'user_type varchar (50) PRIMARY KEY,' 76 | + 'admin boolean NOT NULL,' 77 | + 'moh boolean NOT NULL,' 78 | + 'quiet boolean NOT NULL,' 79 | + 'pin_auth boolean NOT NULL)'); 80 | }) 81 | .then(function () { 82 | console.log('...inserting data into user_profile'); 83 | return query('INSERT INTO user_profile (user_type,admin,moh,quiet,' 84 | + 'pin_auth) VALUES (\'default\',false,true,false,' 85 | + 'false)'); 86 | }) 87 | .then(function () { 88 | return query('SELECT exists(SELECT * FROM information_schema.tables ' 89 | + 'where table_name = \'group_profile\')'); 90 | }) 91 | .then(function (result) { 92 | if (result.rows[0].exists) { 93 | console.log('...deleting group_profile'); 94 | return query('DROP TABLE group_profile'); 95 | } 96 | }) 97 | .then(function () { 98 | console.log('...creating group_profile'); 99 | return query('CREATE TABLE group_profile (' 100 | + 'group_type varchar (50) PRIMARY KEY,' 101 | + 'group_behavior varchar (50) NOT NULL,' 102 | + 'max_members integer NOT NULL)'); 103 | }) 104 | .then(function () { 105 | console.log('...inserting data into group_profile'); 106 | return query('INSERT INTO group_profile (group_type,group_behavior,' 107 | + 'max_members) VALUES (\'default\',\'participant\',' 108 | + '100)'); 109 | }) 110 | .catch(function (err) { 111 | console.error(err); 112 | }) 113 | .finally(function () { 114 | done(); 115 | }); 116 | }) 117 | .then(function () { 118 | console.log('...disconnected from database.'); 119 | }) 120 | .catch(function (err) { 121 | console.error(err); 122 | }) 123 | .finally(function () { 124 | process.exit(0); 125 | }) 126 | .done(); 127 | --------------------------------------------------------------------------------