├── .cfignore
├── .gitignore
├── .travis.yml
├── test
├── unit
│ ├── eslint.js
│ ├── slackbot.js
│ ├── call_manager.js
│ └── phonebot.js
└── integration
│ └── phonebot.js
├── app.js
├── .eslintrc
├── manifest.yml
├── LICENSE
├── package.json
├── routes
└── index.js
├── admin.js
├── lib
├── slackbot.js
├── translate.js
├── call_manager.js
└── phonebot.js
└── README.md
/.cfignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "0.12"
4 |
--------------------------------------------------------------------------------
/test/unit/eslint.js:
--------------------------------------------------------------------------------
1 | var lint = require('mocha-eslint');
2 |
3 | // Array of paths to lint
4 | // Note: a seperate Mocha test will be run for each path and each file which
5 | // matches a glob pattern
6 | var paths = [
7 | 'lib',
8 | 'routes',
9 | 'app.js'
10 | ];
11 |
12 | // Specify style of output
13 | var options = {};
14 |
15 | // Run the tests
16 | lint(paths, options);
17 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var express = require('express'),
4 | cfenv = require('cfenv'),
5 | log = require('loglevel')
6 |
7 | log.setLevel(process.env.LOG_LEVEL || 'info')
8 |
9 | var app = express()
10 | require('./routes')(app)
11 |
12 | var server = app.listen(cfenv.getAppEnv().port, function () {
13 | var host = server.address().address
14 | var port = server.address().port
15 |
16 | log.info('Phonebot now listening at http://%s:%s', host, port)
17 | })
18 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "camelcase": 0,
4 | "curly": 0,
5 | "indent": [
6 | 2,
7 | 2
8 | ],
9 | "quotes": [
10 | 2,
11 | "single"
12 | ],
13 | "linebreak-style": [
14 | 2,
15 | "unix"
16 | ],
17 | "semi": [
18 | 2,
19 | "never"
20 | ]
21 | },
22 | "env": {
23 | "node": true
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/manifest.yml:
--------------------------------------------------------------------------------
1 | ---
2 | declared-services:
3 | twilio:
4 | label: Twilio
5 | plan: 'user-provided'
6 | slack_webhooks:
7 | label: slack_webhooks
8 | plan: 'user-provided'
9 | speech_to_text:
10 | label: speech_to_text
11 | plan: free
12 | applications:
13 | - name: phonebot
14 | memory: 256M
15 | command: node app.js
16 | buildpack: https://github.com/jthomas/nodejs-buildpack.git
17 | services:
18 | - twilio
19 | - speech_to_text
20 | - slack_webhooks
21 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 James Thomas
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.
22 |
23 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "phonebot",
3 | "version": "1.0.0",
4 | "description": "Slackbot to make phonecalls using Twilio and IBM Watson",
5 | "main": "app.js",
6 | "directories": {
7 | "test": "test"
8 | },
9 | "dependencies": {
10 | "async": "^0.9.0",
11 | "cfenv": "^1.0.0",
12 | "loglevel": "^1.2.0",
13 | "request": "^2.55.0",
14 | "express-xml-bodyparser": "^0.0.7",
15 | "promise": "^7.0.1",
16 | "body-parser": "^1.12.3",
17 | "express": "^4.12.3",
18 | "twilio": "^2.2.0",
19 | "watson-developer-cloud": "^0.9.9",
20 | "sox": "^0.1.0",
21 | "tmp": "^0.0.25",
22 | "commander": "^2.6.0",
23 | "http-post": "^0.1.1"
24 | },
25 | "devDependencies": {
26 | "mocha": "^2.2.5",
27 | "mocha-eslint": "^0.1.7",
28 | "mockery": "^1.4.0",
29 | "requestbin": "^1.1.1"
30 | },
31 | "scripts": {
32 | "test": "mocha test/unit",
33 | "integration-test": "mocha test/integration",
34 | "install": "node admin.js track",
35 | "start": "node app.js"
36 | },
37 | "repository": {
38 | "type": "git",
39 | "url": "https://github.com/IBM-Bluemix/phonebot.git"
40 | },
41 | "author": "James Thomas",
42 | "license": "MIT",
43 | "bugs": {
44 | "url": "https://github.com/IBM-Bluemix/phonebot/issues"
45 | },
46 | "homepage": "https://github.com/IBM-Bluemix/phonebot"
47 | }
48 |
--------------------------------------------------------------------------------
/routes/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var xmlparser = require('express-xml-bodyparser'),
4 | bodyParser = require('body-parser'),
5 | twilio = require('twilio'),
6 | watson = require('watson-developer-cloud'),
7 | phonebot = require('../lib/phonebot.js'),
8 | cfenv = require('cfenv'),
9 | log = require('loglevel')
10 |
11 | var service_credentials = function (name) {
12 | var service = cfenv.getAppEnv().getService(name)
13 | if (!service) {
14 | throw new Error('FATAL: Missing service credentials: ' + name)
15 | }
16 |
17 | return service.credentials
18 | }
19 |
20 | var twilio_account = service_credentials('twilio'),
21 | host = twilio_account.url.replace(/http(s)?:\/\//, ''),
22 | client = twilio(twilio_account.accountSID, twilio_account.authToken, {host: host})
23 |
24 | var s2t = service_credentials('speech_to_text')
25 | var speech_to_text = watson.speech_to_text({
26 | username: s2t.username,
27 | password: s2t.password,
28 | url: s2t.url,
29 | version: 'v1'
30 | })
31 |
32 | var bot = phonebot(client, speech_to_text, service_credentials('slack_webhooks'), cfenv.getAppEnv().url + '/recording')
33 |
34 | module.exports = function (app) {
35 | app.use(bodyParser.urlencoded({ extended: true }))
36 | app.use(xmlparser())
37 |
38 | app.post('/recording/:channel', function (req, res) {
39 | log.debug('HTTP POST /recording/' + req.params.channel + '@ ' + (new Date()).toISOString())
40 | log.trace(JSON.stringify(req.body))
41 |
42 | res.send(bot.phone_message(req.params.channel, req))
43 | })
44 |
45 | /*eslint-disable no-unused-vars*/
46 | app.post('/slackbot', function (req, res) {
47 | /*eslint-enable no-unused-vars*/
48 | log.debug('HTTP POST /slackbot/ (#' + req.body.channel_name + ')' + ' @ ' + (new Date()).toISOString())
49 | log.trace(req.body)
50 |
51 | bot.slack_message(req.body.channel_name, req.body)
52 | res.sendStatus(200)
53 | })
54 | }
55 |
--------------------------------------------------------------------------------
/admin.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | // Licensed under the Apache 2.0 License. See footer for details.
3 |
4 | var express = require("express"),
5 | http = require("http"),
6 | path = require("path"),
7 | program = require("commander"),
8 | pkg = require(path.join(__dirname, "package.json"));
9 |
10 | http.post = require("http-post");
11 |
12 |
13 | var app = express();
14 |
15 | if (process.env.VCAP_APPLICATION) {
16 | var vcapApplication = JSON.parse(process.env.VCAP_APPLICATION);
17 | app.set("vcapApplication", vcapApplication);
18 | }
19 |
20 |
21 |
22 | program
23 | .command("track")
24 | .description("Track application deployments")
25 | .action(function(options) {
26 | var vcapApplication = app.get("vcapApplication");
27 | if (vcapApplication) {
28 | var event = {
29 | date_sent: new Date().toJSON()
30 | };
31 | if (pkg.version) {
32 | event.code_version = pkg.version;
33 | }
34 | if (pkg.repository && pkg.repository.url) {
35 | event.repository_url = pkg.repository.url;
36 | }
37 | if (vcapApplication.application_name) {
38 | event.application_name = vcapApplication.application_name;
39 | }
40 | if (vcapApplication.space_id) {
41 | event.space_id = vcapApplication.space_id;
42 | }
43 | if (vcapApplication.application_version) {
44 | event.application_version = vcapApplication.application_version;
45 | }
46 | if (vcapApplication.application_uris) {
47 | event.application_uris = vcapApplication.application_uris;
48 | }
49 | // TODO: Make this work over HTTPS
50 | http.post("http://deployment-tracker.mybluemix.net/", event);
51 | }
52 | }).on("--help", function() {
53 | console.log(" Examples:");
54 | console.log();
55 | console.log(" $ track");
56 | console.log();
57 | });
58 |
59 | program.parse(process.argv);
60 |
61 | //-------------------------------------------------------------------------------
62 | // Copyright IBM Corp. 2015
63 | //
64 | // Licensed under the Apache License, Version 2.0 (the "License");
65 | // you may not use this file except in compliance with the License.
66 | // You may obtain a copy of the License at
67 | //
68 | // http://www.apache.org/licenses/LICENSE-2.0
69 | //
70 | // Unless required by applicable law or agreed to in writing, software
71 | // distributed under the License is distributed on an "AS IS" BASIS,
72 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
73 | // See the License for the specific language governing permissions and
74 | // limitations under the License.
75 | //-------------------------------------------------------------------------------
76 |
--------------------------------------------------------------------------------
/lib/slackbot.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var request = require('request'),
4 | util = require('util'),
5 | EventEmitter = require('events').EventEmitter,
6 | log = require('loglevel')
7 |
8 | function Slackbot (outgoing) {
9 | this.COMMANDS = {
10 | 'help': 'Hello! This is Phonebot. You can make telephone calls using the \'call\' command.',
11 | 'call': this.call,
12 | 'say': this.say,
13 | 'duration': this.duration,
14 | 'verbose': this.verbose,
15 | 'hangup': this.emit.bind(this, 'hangup')
16 | }
17 |
18 | this.outgoing = outgoing
19 | }
20 |
21 | util.inherits(Slackbot, EventEmitter)
22 |
23 | Slackbot.prototype.post = function (text) {
24 | log.info('Slackbot.post (' + this.outgoing + '): ' + text)
25 | request.post({
26 | url: this.outgoing,
27 | body: {text: text, username: 'phonebot', icon_emoji: ':phone:'},
28 | json: true
29 | })
30 | }
31 |
32 | Slackbot.prototype.call = function (text) {
33 | var message = 'Phonebot Command: call PHONE_NUMBER <-- Dials the phone number.'
34 | var numbers = text.match(/\d+/g)
35 |
36 | if (numbers) {
37 | message = ':phone: Let me try to put you through...'
38 | this.emit('call', numbers.join(''))
39 | }
40 | return message
41 | }
42 |
43 | Slackbot.prototype.say = function (text) {
44 | var message = 'Phonebot Command: say TEXT...<-- Sends text as speech to the call.'
45 | if (text.length) {
46 | this.emit('say', text)
47 | message = ''
48 | }
49 | return message
50 | }
51 |
52 | Slackbot.prototype.duration = function (text) {
53 | var message = 'Phonebot Command: duration NUMBER<-- Modify audio recording duration' +
54 | 'for translation. Smaller durations mean faster translations but greater audio gaps.'
55 |
56 | var number = text.match(/\d+/)
57 | if (number) {
58 | message = ''
59 | this.emit('duration', parseInt(number[0], 10))
60 | }
61 |
62 | return message
63 | }
64 |
65 | Slackbot.prototype.verbose = function (text) {
66 | var message = 'Phonebot Command: verbose {on|off}<-- Enable/disable verbose mode. ' +
67 | 'When enabled, channel messages sent to notify users when call audio has been received but not translated.'
68 |
69 | var toggle = text.split(' ')[0]
70 |
71 | if (toggle === 'on' || toggle === 'off') {
72 | message = ''
73 | this.emit('verbose', toggle === 'on')
74 | }
75 |
76 | return message
77 | }
78 |
79 | Slackbot.prototype.channel_message = function (message) {
80 | var response = 'What\'s up?'
81 |
82 | var words = message.text.split(' '),
83 | command = this.COMMANDS[words[1]]
84 |
85 | if (typeof command === 'string') {
86 | response = command
87 | } else if (typeof command === 'function') {
88 | response = command.call(this, words.slice(2).join(' '))
89 | }
90 |
91 | if (typeof response === 'string') this.post(response)
92 | }
93 |
94 | module.exports = function (outgoing) {
95 | return new Slackbot(outgoing)
96 | }
97 |
--------------------------------------------------------------------------------
/lib/translate.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var tmp = require('tmp'),
4 | fs = require('fs'),
5 | util = require('util'),
6 | Promise = require('promise'),
7 | sox = require('sox'),
8 | request = require('request'),
9 | log = require('loglevel')
10 |
11 | var EventEmitter = require('events').EventEmitter
12 |
13 | var temp_wav_file = Promise.denodeify(tmp.file)
14 |
15 | var Translate = function (speech_to_text, location) {
16 | this.transcript = null
17 | this.location = location
18 | this.speech_to_text = speech_to_text
19 | }
20 |
21 | util.inherits(Translate, EventEmitter)
22 |
23 | Translate.prototype.error = function (message) {
24 | if (message) log.error(message)
25 | this.failed = true
26 | this.emit('failed')
27 | }
28 |
29 | Translate.prototype.transcode_to_16k = function () {
30 | var that = this
31 | var promise = new Promise(function (resolve, reject) {
32 | var job = sox.transcode(that.source, that.upsample, {
33 | sampleRate: 16000,
34 | format: 'wav',
35 | channelCount: 1
36 | })
37 | job.on('error', function (err) {
38 | that.error(err)
39 | reject(err)
40 | })
41 | job.on('end', function () {
42 | log.debug('Translation Audio Converted (' + that.source + '): ' + that.upsample)
43 | resolve()
44 | })
45 | job.start()
46 | })
47 |
48 | return promise
49 | }
50 |
51 | Translate.prototype.download_source = function () {
52 | log.debug('Retrieving Audio Recording Source: ' + this.source)
53 | var that = this
54 |
55 | var promise = new Promise(function (resolve, reject) {
56 | var source_stream = fs.createWriteStream(that.source)
57 | source_stream.on('finish', resolve)
58 |
59 | request(that.location)
60 | .on('error', reject)
61 | .pipe(source_stream)
62 | })
63 |
64 | return promise
65 | }
66 |
67 | Translate.prototype.translate_to_text = function () {
68 | log.debug('Translating audio recording (upsampled): ' + this.upsample)
69 | var that = this
70 |
71 | var params = {
72 | audio: fs.createReadStream(this.upsample),
73 | content_type: 'audio/l16; rate=16000'
74 | }
75 |
76 | this.speech_to_text.recognize(params, function (err, res) {
77 | log.trace('Watson Service Response: ' + JSON.stringify(res))
78 | if (err) {
79 | that.error(err)
80 | return
81 | }
82 |
83 | var result = res.results[res.result_index]
84 | if (result) {
85 | that.transcript = result.alternatives[0].transcript
86 | that.emit('available')
87 | } else {
88 | that.error('Missing speech recognition result.')
89 | }
90 | })
91 | }
92 |
93 | Translate.prototype.tmp_files = function () {
94 | var that = this
95 | var temp_files = [temp_wav_file({postfix: '.wav'}), temp_wav_file({postfix: '.wav'})]
96 |
97 | return Promise.all(temp_files).then(function (results) {
98 | that.source = results[0]
99 | that.upsample = results[1]
100 | }, this.error.bind(this))
101 | }
102 |
103 | Translate.prototype.start = function () {
104 | var err = this.error.bind(this)
105 | this.tmp_files()
106 | .then(this.download_source.bind(this), err)
107 | .then(this.transcode_to_16k.bind(this), err)
108 | .then(this.translate_to_text.bind(this), err)
109 | }
110 |
111 | module.exports = function (speech_to_text, location) {
112 | return new Translate(speech_to_text, location)
113 | }
114 |
--------------------------------------------------------------------------------
/lib/call_manager.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var twilio = require('twilio'),
4 | util = require('util'),
5 | EventEmitter = require('events').EventEmitter,
6 | log = require('loglevel')
7 |
8 | function CallManager (client, channel) {
9 | this.client = client
10 | this.channel = channel
11 | this.active_call = null
12 | this.outgoing = []
13 |
14 | this.defaults = {
15 | duration: 5,
16 | verbose: true
17 | }
18 |
19 | // Find first account number to use as outgoing number
20 | var that = this
21 | client.incomingPhoneNumbers.list(function (err, data) {
22 | var err_message = 'ERROR: Unable to find outgoing phone number from Twilio'
23 | if (err) throw new Error(err_message + '\n' + JSON.stringify(err))
24 |
25 | var first = data.incoming_phone_numbers[0]
26 | if (!first) throw new Error(err_message)
27 |
28 | that.from = first.phone_number
29 | })
30 | }
31 |
32 | util.inherits(CallManager, EventEmitter)
33 |
34 | CallManager.prototype.default_greeting = 'Hello this is Phonebot'
35 |
36 | CallManager.prototype.update_call_status = function (details) {
37 | // NodeJS Twilio clients wraps TwiML responses with different
38 | // parameter names than XML POSTs. Handle both types of message name.
39 | var status = details.CallStatus || details.status,
40 | sid = details.CallSid || details.sid,
41 | number = details.To || details.to
42 |
43 | // Ensure message sid matches active call sid
44 | if (!this.is_message_valid(status, sid)) {
45 | log.error('ERROR: Invalid message received for call (' + this.active_call.sid + '): ' + details)
46 | return null
47 | }
48 |
49 | switch (status) {
50 | // Store new call reference when outbound call is queued.
51 | case 'queued':
52 | this.active_call = {sid: sid, status: status, number: number}
53 | this.emit(status)
54 | break
55 | case 'ringing':
56 | case 'in-progress':
57 | var old = this.active_call.status
58 | this.active_call.status = status
59 | // Don't emit the same event multiple times
60 | if (old !== status) this.emit(status)
61 | break
62 | // When the phone call ends, for any reason, remove
63 | // the active call reference so we can make another.
64 | case 'completed':
65 | case 'busy':
66 | case 'failed':
67 | case 'no-answer':
68 | case 'canceled':
69 | this.active_call = null
70 | this.emit(status)
71 | break
72 | default:
73 | log.error('ERROR: Unknown call state encountered (' + status + '): ' + JSON.stringify(details))
74 | status = null
75 | }
76 |
77 | return status
78 | }
79 |
80 | CallManager.prototype.call = function (number, route) {
81 | // We can't make a call until the line is free...
82 | if (this.call_active()) return
83 |
84 | var that = this
85 | this.outgoing = [this.default_greeting]
86 | this.active_call = {sid: null, status: null, number: number}
87 |
88 | this.client.makeCall({
89 | to: number,
90 | from: this.from,
91 | url: route
92 | }, function (err, responseData) {
93 | if (err) {
94 | that.request_fail('Failed To Start Call: ' + number + '(' + route + ') ', err)
95 | return
96 | }
97 |
98 | that.request_success('New Call Started: ' + number + ' (' + route + '): ' + responseData.sid, responseData)
99 | })
100 | }
101 |
102 | CallManager.prototype.hangup = function () {
103 | var that = this
104 |
105 | if (!this.call_active()) return
106 |
107 | this.client.calls(this.active_call.sid).update({
108 | status: 'completed'
109 | }, function (err, call) {
110 | if (err) {
111 | that.request_fail('Failed To Hangup Call (' + that.active_call.sid + ')', err)
112 | return
113 | }
114 |
115 | that.request_success('Call Terminated: ' + that.active_call, call)
116 | })
117 | }
118 |
119 | CallManager.prototype.say = function (text) {
120 | // If we're waiting for the outgoing call to connect,
121 | // let user override the default greeting.
122 | if (this.outgoing[0] === this.default_greeting) {
123 | this.outgoing[0] = text
124 | return
125 | }
126 |
127 | this.outgoing.push(text)
128 | }
129 |
130 | CallManager.prototype.options = function (opts) {
131 | if (typeof opts.duration === 'number') {
132 | this.defaults.duration = opts.duration
133 | }
134 | if (opts.hasOwnProperty('verbose')) {
135 | this.defaults.verbose = opts.verbose
136 | }
137 | }
138 |
139 | CallManager.prototype.call_active = function () {
140 | return !!this.active_call
141 | }
142 |
143 | CallManager.prototype.stats = function () {
144 | }
145 |
146 | CallManager.prototype.process = function (req) {
147 | var twiml = ''
148 |
149 | var current_status = this.update_call_status(req.body)
150 | if (current_status) {
151 | if (current_status === 'in-progress') {
152 | twiml = new twilio.TwimlResponse()
153 |
154 | // Do we have text to send down the active call?
155 | if (this.outgoing.length) {
156 | var user_speech = this.outgoing.join(' ')
157 | this.outgoing = []
158 | twiml.say(user_speech)
159 | }
160 |
161 | twiml.record({playBeep: false, trim: 'do-not-trim', maxLength: this.defaults.duration, timeout: 60})
162 | }
163 |
164 | if (req.body && req.body.RecordingUrl) {
165 | this.emit('recording', req.body.RecordingUrl)
166 | log.info('New Recording Available: ' + req.body.RecordingUrl)
167 | }
168 | }
169 |
170 | return twiml
171 | }
172 |
173 | CallManager.prototype.request_fail = function (message, err) {
174 | log.error(message)
175 | log.error(err)
176 | this.update_call_status({CallStatus: 'failed', sid: this.active_call.sid})
177 | }
178 |
179 | CallManager.prototype.request_success = function (message, call) {
180 | log.info(message)
181 | log.trace(call)
182 | this.update_call_status(call)
183 | }
184 |
185 | CallManager.prototype.is_message_valid = function (status, sid) {
186 | return (status === 'queued' || sid === this.active_call.sid)
187 | }
188 |
189 | module.exports = function (client, channel) {
190 | return new CallManager(client, channel)
191 | }
192 |
--------------------------------------------------------------------------------
/test/unit/slackbot.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert')
2 | var mockery = require('mockery')
3 |
4 | var log = require('loglevel')
5 | log.disableAll()
6 |
7 | var param
8 | var request = function (req) {
9 | param = req
10 | }
11 | request.post = function (req) {
12 | param = req
13 | }
14 |
15 | describe('Slackbot', function(){
16 | before(function() {
17 | mockery.enable({useCleanCache: true}); // Enable mockery at the start of your test suite
18 | mockery.registerMock('request', request);
19 | mockery.registerAllowables(['loglevel', 'events', '../../lib/slackbot.js', 'util']);
20 | slackbot = require('../../lib/slackbot.js')
21 | })
22 |
23 | after(function() {
24 | mockery.disable(); // Disable Mockery after tests are completed
25 | })
26 |
27 | describe('#post', function(){
28 | it('should send text as HTTP POST to url parameter', function(){
29 | var url = "some_url",
30 | text = "testing 1 2 3",
31 | bot = slackbot(url)
32 |
33 | bot.post(text)
34 | assert.equal(param.url, url)
35 | assert.equal(param.json, true)
36 | assert.deepEqual(param.body, {text: text, username: 'phonebot', icon_emoji: ':phone:'})
37 | })
38 | })
39 |
40 | describe('#call', function(){
41 | it('should notify listeners of new call requests', function(done){
42 | var bot = slackbot()
43 |
44 | bot.on('call', function(number){
45 | assert.equal('111', number)
46 | done()
47 | })
48 | bot.call('111')
49 | })
50 | it('should parse all digits into a phone number', function(done){
51 | var bot = slackbot()
52 |
53 | bot.on('call', function(number){
54 | assert.equal('111222333', number)
55 | done()
56 | })
57 |
58 | bot.call('111 222 333')
59 | })
60 | it('should return channel message about new phone call', function(){
61 | var bot = slackbot()
62 | var message = bot.call('111')
63 | assert.equal(message, ':phone: Let me try to put you through...')
64 | })
65 | it('should show help text for command without number', function(){
66 | var bot = slackbot()
67 | var message = bot.call('')
68 | assert.ok(message.indexOf('Phonebot Command') === 0)
69 | })
70 | })
71 |
72 | describe('#say', function(){
73 | it('should notify listeners of new speech replies', function(done){
74 | var bot = slackbot()
75 |
76 | bot.on('say', function(text){
77 | assert.equal('111', text)
78 | done()
79 | })
80 | var response = bot.say('111')
81 | assert.equal(response, '')
82 | })
83 | it('should show help text for command without number', function(){
84 | var bot = slackbot()
85 | var message = bot.say('')
86 | assert.ok(message.indexOf('Phonebot Command') === 0)
87 | })
88 | })
89 |
90 | describe('#duration', function(){
91 | it('should notify listeners of duration change', function(done){
92 | var bot = slackbot()
93 |
94 | bot.on('duration', function(_){
95 | assert.equal('111', _)
96 | done()
97 | })
98 | bot.duration('111')
99 | })
100 | it('should use first number as duration', function(done){
101 | var bot = slackbot()
102 |
103 | bot.on('duration', function(_){
104 | assert.equal('111', _)
105 | done()
106 | })
107 | bot.duration('111 222')
108 | })
109 | it('should show help text for command without duration', function(){
110 | var bot = slackbot()
111 | var message = bot.duration('')
112 | assert.ok(message.indexOf('Phonebot Command') === 0)
113 | })
114 | })
115 |
116 | describe('#verbose', function(){
117 | it('should ignore invalid settings', function(){
118 | var bot = slackbot()
119 |
120 | bot.on('verbose', function(_){
121 | assert.ok(false)
122 | })
123 |
124 | bot.duration('random text')
125 | })
126 | it('should turn verbose on', function(done){
127 | var bot = slackbot()
128 |
129 | bot.on('verbose', function(_){
130 | assert.equal(true, _)
131 | done()
132 | })
133 |
134 | bot.verbose('on')
135 | })
136 | it('should turn verbose off', function(done){
137 | var bot = slackbot()
138 |
139 | bot.on('verbose', function(_){
140 | assert.equal(false, _)
141 | done()
142 | })
143 |
144 | bot.verbose('off')
145 | })
146 | it('should show help text for command without duration', function(){
147 | var bot = slackbot()
148 | var message = bot.verbose('')
149 | assert.ok(message.indexOf('Phonebot Command') === 0)
150 | })
151 | })
152 |
153 | describe('#channel_message', function(){
154 | it('should ignore commands not registered', function(){
155 | var bot = slackbot(),
156 | response
157 | bot.post = function (_) {
158 | response = _
159 | }
160 | bot.channel_message({text:'@slackbot hello'})
161 | assert.equal(response, 'What\'s up?')
162 | })
163 | it('should return string commands as channel message', function(){
164 | var bot = slackbot(),
165 | response
166 | bot.post = function (_) {
167 | response = _
168 | }
169 | bot.COMMANDS.hello = "testing"
170 | bot.channel_message({text:'@slackbot hello'})
171 | assert.equal(response, bot.COMMANDS.hello)
172 | })
173 | it('should execute and return function commands as message', function(){
174 | var bot = slackbot(),
175 | response, arg
176 | bot.post = function (_) {
177 | response = _
178 | }
179 | bot.COMMANDS.hello = function (_) {
180 | arg = _
181 | return 'testing'
182 | }
183 | bot.channel_message({text:'@slackbot hello 1 2 3'})
184 | assert.equal(response, 'testing')
185 | assert.equal(arg, '1 2 3')
186 |
187 | bot.COMMANDS.hello = function (_) {
188 | arg = _
189 | }
190 |
191 | response = null
192 | bot.channel_message({text:'@slackbot hello 1 2 3'})
193 | assert.equal(response, null)
194 | })
195 | })
196 | })
197 |
--------------------------------------------------------------------------------
/lib/phonebot.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | var slackbot = require('./slackbot.js'),
4 | call_manager = require('./call_manager.js'),
5 | translate = require('./translate.js'),
6 | async = require('async'),
7 | log = require('loglevel')
8 |
9 | var PhoneBot = function (client, watson, channels, base_url) {
10 | this.channels = {}
11 | this.base_url = base_url
12 | this.watson = watson
13 |
14 | for (var key in channels) {
15 | this.channels[key] = {
16 | bot: this.create_channel_bot(channels[key], key),
17 | phone: this.create_call_manager(client, key),
18 | queue: this.create_translation_queue(key)
19 | }
20 | log.info('Registering #' + key + ' (' + channels[key] + ')')
21 | }
22 | }
23 |
24 | PhoneBot.prototype.create_channel_bot = function (webhook, channel) {
25 | var bot = slackbot(webhook)
26 | var that = this
27 |
28 | bot.on('call', function (number) {
29 | var phone = that.channels[channel].phone
30 |
31 | if (phone.call_active()) {
32 | bot.post('The line is busy, you have to hang up first...!')
33 | return
34 | }
35 |
36 | phone.call(number, that.base_url + '/' + channel)
37 | log.info('#' + channel + ': call ' + number)
38 | })
39 |
40 | bot.on('say', function (text) {
41 | var phone = that.channels[channel].phone
42 | phone.say(text)
43 | log.info('#' + channel + ': say ' + text)
44 | })
45 |
46 | bot.on('duration', function (duration) {
47 | var phone = that.channels[channel].phone
48 | phone.options({duration: duration})
49 | log.info('#' + channel + ': duration ' + duration)
50 | })
51 |
52 | bot.on('verbose', function (enabled) {
53 | var phone = that.channels[channel].phone
54 | phone.options({verbose: enabled})
55 | log.info('#' + channel + ': verbose ' + enabled)
56 | })
57 |
58 | bot.on('hangup', function () {
59 | var phone = that.channels[channel].phone
60 |
61 | if (!phone.call_active()) {
62 | bot.post('There isn\'t a phone call to hang up...')
63 | return
64 | }
65 |
66 | phone.hangup()
67 | log.info('#' + channel + ': hangup')
68 | })
69 |
70 | bot.post('_Phonebot is here!_')
71 |
72 | return bot
73 | }
74 |
75 | PhoneBot.prototype.create_call_manager = function (client, channel) {
76 | var phone = call_manager(client, channel)
77 | var that = this
78 |
79 | phone.on('recording', function (location) {
80 | if (phone.defaults.verbose) {
81 | that.channels[channel].bot.post(':speech_balloon: _waiting for translation_')
82 | }
83 | var req = translate(that.watson, location)
84 | req.start()
85 | that.channels[channel].queue.push(req)
86 | })
87 |
88 | phone.on('queued', function () {
89 | that.channels[channel].bot.post(':phone: Connecting to ' + phone.active_call.number)
90 | })
91 |
92 | phone.on('ringing', function () {
93 | that.channels[channel].bot.post(':phone: Still ringing...')
94 | })
95 |
96 | phone.on('in-progress', function () {
97 | that.channels[channel].bot.post(':phone: You\'re connected! :+1:')
98 | })
99 |
100 | phone.on('completed', function () {
101 | // If we have translation tasks outstanding, wait until we have finished
102 | // before posting the call finished message
103 | if (!that.channels[channel].queue.idle()) {
104 | that.channels[channel].queue.drain = function () {
105 | setTimeout(function () {
106 | that.channels[channel].bot.post(':phone: That\'s it, call over!')
107 | }, 1000)
108 | that.channels[channel].queue.drain = null
109 | }
110 | } else {
111 | that.channels[channel].bot.post(':phone: That\'s it, call over!')
112 | }
113 | })
114 |
115 | phone.on('canceled', function () {
116 | that.channels[channel].bot.post(':phone: That\'s it, call over!')
117 | })
118 |
119 | phone.on('busy', function () {
120 | that.channels[channel].bot.post(':phone: They were busy, sorry :unamused:')
121 | })
122 |
123 | phone.on('no-answer', function () {
124 | that.channels[channel].bot.post(':phone: Oh no, they didn\'t answer :sleeping:')
125 | })
126 |
127 | phone.on('failed', function () {
128 | that.channels[channel].bot.post(':phone: Whoops, something failed. My bad. Try again? :see_no_evil:')
129 | })
130 |
131 | return phone
132 | }
133 |
134 | PhoneBot.prototype.create_translation_queue = function (channel) {
135 | var that = this
136 |
137 | return async.queue(function (task, callback) {
138 | var done = function (message) {
139 | if (message) that.channels[channel].bot.post(':speech_balloon: ' + message)
140 | callback()
141 | return true
142 | }
143 |
144 | var process = function () {
145 | log.info('Transcription Task Result (' + channel + '): ' + task.location)
146 | log.info('Transcription Task Result (' + channel + '): ' + task.transcript)
147 | return done(task.transcript)
148 | }
149 |
150 | var failed = function () {
151 | log.error('Transcription Task Failed(' + channel + '): ' + task.location)
152 | return done(that.channels[channel].phone.defaults.verbose ? '_unable to recognise speech_' : '')
153 | }
154 |
155 | if (task.transcript && process()) return
156 | if (task.failed && failed()) return
157 |
158 | log.info('Transcription Task Queued(' + channel + '): ' + task.location)
159 | task.on('available', process)
160 | task.on('failed', failed)
161 | }, 1)
162 | }
163 |
164 | // Need to handle unknown channel messages.
165 | PhoneBot.prototype.phone_message = function (channel, message) {
166 | var response = null,
167 | lookup = this.channels[channel]
168 |
169 | if (lookup) {
170 | response = lookup.phone.process(message).toString()
171 | log.trace(response)
172 | } else {
173 | log.error('Phone message received for unknown channel: ' + channel)
174 | }
175 | return response
176 | }
177 |
178 | PhoneBot.prototype.slack_message = function (channel, message) {
179 | var lookup = this.channels[channel]
180 |
181 | if (lookup) {
182 | lookup.bot.channel_message(message)
183 | } else {
184 | log.error('Slack message received for unknown channel: ' + channel)
185 | }
186 | }
187 |
188 | module.exports = function (client, watson, channels, base_url) {
189 | return new PhoneBot(client, watson, channels, base_url)
190 | }
191 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # phonebot
2 |
3 | Slackbot that lets users make phone calls within a Slack channel.
4 | Users can dial a phone number, with the phone call audio converted to text and sent to the channel.
5 | Channel message replies are converted to speech and sent over the phone call.
6 |
7 | Twilio is used to make phone calls and capture call audio.
8 | IBM Watson's "Speech To Text" service is used to translate the audio into text.
9 | NodeJS web application handles incoming messages from Slack, IBM Watson and Twilio.
10 |
11 |
12 |
13 |
14 |
15 | [](https://api.travis-ci.org/IBM-Bluemix/phonebot.svg?branch=master)
16 | [](https://github.com/feross/standard)
17 |
18 | 
19 |
20 | ## Usage
21 |
22 | Once the Phonebot application is registered for a Slack channel, the following
23 | commands are available:
24 |
25 | ```
26 | @phonebot call PHONE_NUMBER <-- Dials the phone number
27 | @phonebot say TEXT <-- Sends text as speech to the call
28 | @phonebot hangup <-- Ends the active call
29 | @phonebot verbose {on|off}<-- Toggle verbose mode
30 | @phonebot duration NUMBER <-- Set recording duration
31 | @phonebot help <-- Show all commands usage information
32 | ```
33 |
34 | ## Deployment Instructions
35 |
36 | Phonebot is a NodeJS application designed for deployment to IBM Bluemix, a Cloud Foundry
37 | Platform-as-a-Service instance.
38 |
39 | Before deploying the application, we need to...
40 |
41 | * Register [Slack webhooks](https://api.slack.com/) to allow the bot to send and receive channel messages.
42 | * Register for [Twilio](http://twilio.com) and [IBM Watson](http://www.ibm.com/smarterplanet/us/en/ibmwatson/developercloud/) API credentials.
43 |
44 | ### Slack Webhooks
45 |
46 | Phonebot can be registered on multiple channels.
47 |
48 | Every channel you want Phonebot to join needs both an outgoing incoming and outgoing webhook.
49 |
50 | #### Set up Outgoing Slack Webhooks
51 |
52 | [Outgoing Webhooks](https://api.slack.com/outgoing-webhooks) are used to notify
53 | Phonebot when messages for the bot (_@phonebot command_) are sent to the channel.
54 | Visiting the [Service Integration](https://my.slack.com/services/new/outgoing-webhook) page will allow
55 | you to create a new webhooks that posts all messages starting with a keyword to
56 | an external URL.
57 |
58 | The following parameters must be configured:
59 | * **Channel** - Channel for Phonebot to listen on
60 | * **Trigger Words** - @phonebot
61 | * **URL** - http://.mybluemix.net/slackbot
62 |
63 | 
64 |
65 | #### Set up Incoming Slack Webhooks
66 |
67 | [Incoming Webhooks](https://api.slack.com/incoming-webhooks) are used by
68 | Phonebot to post the translated call audio as channel messages. We need an incoming webhook for each channel
69 | with a registered outgoing webhook. Visiting the [Service Integration](https://my.slack.com/services/new/incoming-webhook) page will allow
70 | you to create a new incoming webhook URL to post channel messages.
71 |
72 | 
73 |
74 | Copy the generated URL that exposes the webhook, you will need to pass this into
75 | the application as explained in the section below.
76 |
77 | ### Deploy to IBM Bluemix
78 |
79 | Before we can deploy the application, we need to create the service
80 | credentials the application relies on within IBM Bluemix.
81 |
82 | These credentials will be bound to the application at runtime.
83 |
84 | #### Twilio
85 |
86 | Register for a developer account at [Twilio](https://www.twilio.com/try-twilio).
87 |
88 | Connect an [external phone number](https://www.twilio.com/user/account/phone-numbers/incoming) to
89 | the account, this will be used as the caller id when making phone calls.
90 |
91 | Authentication credentials for Phonebot will be available [here](https://www.twilio.com/user/account/settings).
92 |
93 | Run the following CF CLI command to expose your developer account credentials to
94 | the platform. Replace the ACCOUNT_SID and TOKEN values with credentials from
95 | your [account settings page](https://www.twilio.com/user/account/settings).
96 |
97 | ```
98 | $ cf cups twilio -p '{"accountSID":"ACCOUNT_SID","authToken":"TOKEN", "url": "https://api.twilio.com"}'
99 | ```
100 |
101 | *Note: Phonebot will work with a Twilio trial account, however outgoing calls are
102 | only allowed to verified numbers. See [here](https://www.twilio.com/user/account/phone-numbers/verified) for more details.*
103 |
104 | #### IBM Watson
105 |
106 | This ["Speech To Text"](http://www.ibm.com/smarterplanet/us/en/ibmwatson/developercloud/speech-to-text.html)
107 | service will be automatically created using the _"Free Plan"_ when the
108 | application is deployed. To change the service instance the application uses,
109 | modify the [manifest.yml](http://docs.cloudfoundry.org/devguide/deploy-apps/manifest.html).
110 |
111 | #### Slack channels
112 |
113 | Run the following [CF CLI](http://docs.cloudfoundry.org/devguide/installcf/)
114 | command to register your Slack channel webhooks within Phonebot.
115 |
116 | ```
117 | $ cf cups slack_webhooks -p '{"channel_name":"incoming_webhook_url",...}'
118 | ```
119 |
120 | Replace the *channel_name* and *incoming_webhook_url* with the actual values for the channel name and
121 | incoming webhooks found at your [Slack integrations page](https://myslack.slack.com/services).
122 |
123 | You have to register each channel as a separate property.
124 |
125 | #### Deploy!
126 |
127 | Phew, you're now got everything configured! Deploying the application is easy,
128 | just run this command:
129 |
130 | ```
131 | $ cf push --name your_random_identifier
132 | ```
133 |
134 | Modify *your_random_identifier* to a name relevant for your Phonebot instance.
135 |
136 | Once the application has finished deploying, Phonebot will be listening to
137 | requests at http://your_random_identifier.mybluemix.net.
138 |
139 | *This address must match the URL registered with the outgoing webhooks.*
140 |
141 | **_... and you're done! Phonebot should post a message to each registered channel
142 | to confirm it's ready for action._**
143 |
144 | ### Development Mode
145 |
146 | Running Phonebot on your development machine is possible provided you follow
147 | these steps...
148 |
149 | * Copy VCAP_SERVICES and VCAP_APPLICATION to local development environment.
150 | * Run *node app.js* to start application
151 | * Ensure application running on localhost is available at external URL, e.g. ngrok
152 | * Modify Slack outgoing webhooks to point to new URL
153 |
--------------------------------------------------------------------------------
/test/integration/phonebot.js:
--------------------------------------------------------------------------------
1 | var request = require('request'),
2 | assert = require('assert')
3 |
4 | var stub_server = process.env.STUB_SERVER
5 | if (!stub_server) throw new Error('Unable to find stub server from process.env')
6 |
7 | var test_server = process.env.TEST_SERVER
8 | if (!test_server) throw new Error('Unable to find test server from process.env')
9 |
10 | describe('PhoneBot', function(){
11 | after(function(done){
12 | request.get({url: stub_server + '/requests/clear'}, function () {
13 | done()
14 | })
15 | })
16 | describe('#startup', function(){
17 | var requests
18 | before(function (done) {
19 | request.get({url: stub_server + '/requests', json: true}, function (err, resp, body) {
20 | if (err) assert.ok(false)
21 | if (!body) assert.ok(false)
22 | requests = body.requests
23 | done()
24 | })
25 | })
26 | it('should post new channel message to each registered channel', function(){
27 | var channel_message = requests[0]
28 |
29 | assert.deepEqual({
30 | text: '_Phonebot is here!_',
31 | username: 'phonebot',
32 | icon_emoji: ':phone:'
33 | }, channel_message.body)
34 | assert.equal('/slack/testing', channel_message.url)
35 | assert.equal('POST', channel_message.method)
36 | })
37 | it('should request incoming list of account phone numbers', function(){
38 | var channel_message = requests[1]
39 |
40 | assert.equal('/2010-04-01/Accounts/xxx/IncomingPhoneNumbers.json', channel_message.url)
41 | assert.equal('GET', channel_message.method)
42 | })
43 | })
44 |
45 | describe('#slackbot', function(){
46 | it('should respond to channel messages', function(done){
47 | this.timeout(5000)
48 | request.post({url: test_server + '/slackbot', form: {channel_name: 'testing', text: '@phonebot hello'}}, function (err, resp, body) {
49 | if (err) {
50 | console.log(err)
51 | assert.ok(false)
52 | }
53 |
54 | setTimeout(function () {
55 | request.get({url: stub_server + '/requests', json: true}, function (err, resp, body) {
56 | if (err) assert.ok(false)
57 |
58 | message = body.requests.pop()
59 |
60 | assert.deepEqual({
61 | text: 'What\'s up?',
62 | username: 'phonebot',
63 | icon_emoji: ':phone:'
64 | }, message.body)
65 | assert.equal('/slack/testing', message.url)
66 | assert.equal('POST', message.method)
67 |
68 | done()
69 | })
70 | }, 2000)
71 | })
72 | })
73 |
74 | it('should make a call from user command', function(done){
75 | this.timeout(10000);
76 | request.post({url: test_server + '/slackbot', form: {channel_name: 'testing', text: '@phonebot call 1234567890'}}, function (err) {
77 | if (err) assert.ok(false)
78 |
79 | setTimeout(function () {
80 | request.get({url: stub_server + '/requests', json: true}, function (err, resp, body) {
81 | if (err) assert.ok(false)
82 |
83 | var slack_message = body.requests.pop(),
84 | twilio_message = body.requests.pop()
85 |
86 | assert.deepEqual({
87 | text: ':phone: Connecting to 1234567890',
88 | username: 'phonebot',
89 | icon_emoji: ':phone:'
90 | }, slack_message.body)
91 | assert.equal('/slack/testing', slack_message.url)
92 | assert.equal('POST', slack_message.method)
93 |
94 | assert.deepEqual({
95 | To: '1234567890',
96 | From: '+440123456789',
97 | Url: test_server.replace('http', 'https') + '/recording/testing'
98 | }, twilio_message.body)
99 | assert.equal('/2010-04-01/Accounts/xxx/Calls.json', twilio_message.url)
100 | assert.equal('POST', twilio_message.method)
101 |
102 | done()
103 | })
104 | }, 5000)
105 | })
106 | })
107 |
108 | it('should ignore new calls for active line', function(done){
109 | this.timeout(10000);
110 | request.post({url: test_server + '/slackbot', form: {channel_name: 'testing', text: '@phonebot call 1234567890'}}, function (err) {
111 | if (err) assert.ok(false)
112 |
113 | setTimeout(function () {
114 | request.get({url: stub_server + '/requests', json: true}, function (err, resp, body) {
115 | if (err) assert.ok(false)
116 |
117 | var slack_message = body.requests.pop()
118 |
119 | assert.deepEqual({
120 | text: 'The line is busy, you have to hang up first...!',
121 | username: 'phonebot',
122 | icon_emoji: ':phone:'
123 | }, slack_message.body)
124 | assert.equal('/slack/testing', slack_message.url)
125 | assert.equal('POST', slack_message.method)
126 |
127 | done()
128 | })
129 | }, 5000)
130 | })
131 | })
132 |
133 | it('should hang up active calls', function(done){
134 | this.timeout(10000);
135 | request.post({url: test_server + '/slackbot', form: {channel_name: 'testing', text: '@phonebot hangup'}}, function (err) {
136 | if (err) assert.ok(false)
137 |
138 | setTimeout(function () {
139 | request.get({url: stub_server + '/requests', json: true}, function (err, resp, body) {
140 | if (err) assert.ok(false)
141 |
142 | var slack_message = body.requests.pop()
143 |
144 | assert.deepEqual({
145 | text: ':phone: That\'s it, call over!',
146 | username: 'phonebot',
147 | icon_emoji: ':phone:'
148 | }, slack_message.body)
149 | assert.equal('/slack/testing', slack_message.url)
150 | assert.equal('POST', slack_message.method)
151 |
152 | done()
153 | })
154 | }, 5000)
155 | })
156 | })
157 | })
158 |
159 | describe('#call transcribing', function(){
160 | it('should send audio descriptions back to channels', function(done){
161 | this.timeout(10000);
162 | // start the call...
163 | request.post({url: test_server + '/slackbot', form: {channel_name: 'testing', text: '@phonebot call 1234567890'}}, function (err) {
164 | if (err) assert.ok(false)
165 |
166 | setTimeout(function () {
167 | request.post({url: test_server + '/recording/testing', form: {sid: 'testing_sid', status: 'in-progress', RecordingUrl: stub_server + '/audio.wav'}}, function (err, resp, body) {
168 |
169 | setTimeout(function () {
170 | request.get({url: stub_server + '/requests', json: true}, function (err, resp, body) {
171 | if (err) assert.ok(false)
172 |
173 | message = body.requests.pop()
174 |
175 | assert.deepEqual({
176 | text: ':speech_balloon: Hello World',
177 | username: 'phonebot',
178 | icon_emoji: ':phone:'
179 | }, message.body)
180 | assert.equal('/slack/testing', message.url)
181 | assert.equal('POST', message.method)
182 | done()
183 | })
184 | }, 5000)
185 | })
186 | }, 2000)
187 | })
188 | })
189 | })
190 | // AFTER CLEAR DOWN TEST RESULTS
191 | })
192 |
--------------------------------------------------------------------------------
/test/unit/call_manager.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert')
2 | var CallManager = require('../../lib/call_manager.js')
3 | var twilio = require('twilio')
4 | var log = require('loglevel')
5 | log.disableAll()
6 |
7 | var args = null
8 | var cb_arg = {}
9 | var client = {
10 | makeCall: function (options, cb) {
11 | args = options
12 | cb(null, cb_arg)
13 | },
14 | incomingPhoneNumbers: { list: function (cb) {
15 | cb(null, {incoming_phone_numbers: [{phone_number: 1234}]})
16 | }}
17 | }
18 |
19 | var failing = {
20 | makeCall: function (options, cb) {
21 | cb('Error')
22 | },
23 | incomingPhoneNumbers: { list: function () {}},
24 | calls: function () {
25 | return {
26 | update: function (opts, cb) {
27 | cb('Error', null)
28 | }
29 | }
30 | }
31 | }
32 |
33 | describe('CallManager', function(){
34 | describe('#constructor', function(){
35 | it('should retrieve active phone number for outgoing call', function(){
36 | var cm = CallManager({incomingPhoneNumbers: {
37 | list: function (cb) {
38 | cb(null, {incoming_phone_numbers: [{phone_number: 1234}]})
39 | }
40 | }})
41 | assert.ok(1234, cm.from)
42 | })
43 | it('should throw an error if we cannot access outbound number', function(){
44 | var called = false
45 | try {
46 | var cm = CallManager({incomingPhoneNumbers: {
47 | list: function (cb) {
48 | cb(true, {incoming_phone_numbers: [{phone_number: 1234}]})
49 | }
50 | }})
51 | } catch (e) {
52 | called = true
53 | }
54 | assert.ok(called)
55 | })
56 | })
57 | describe('#call', function(){
58 | it('should not make a call if there is an active call', function(){
59 | var channel = 'some_url',
60 | number = '111 222 333'
61 | cm = CallManager({
62 | makeCall: function () {
63 | assert.ok(false)
64 | },
65 | incomingPhoneNumbers: {
66 | list: function (){}
67 | }
68 | }, channel)
69 |
70 | var route = 'localhost'
71 | cm.active_call = {sid: null, status: null}
72 | cm.call(number, route)
73 | })
74 | it('should make a call and save the sid as the active call reference', function(){
75 | var channel = 'some_url',
76 | number = '111 222 333'
77 | cm = CallManager(client, channel)
78 |
79 | cb_arg = {sid: 111, CallStatus: 'queued'}
80 | var route = 'localhost'
81 | cm.call(number, route)
82 | assert.equal(args.to, number)
83 | assert.equal(args.from, 1234)
84 | assert.equal(args.url, route)
85 | assert.equal(cm.active_call.sid, 111)
86 | assert.equal(cm.active_call.status, 'queued')
87 | assert.deepEqual(cm.outgoing, [cm.default_greeting])
88 | })
89 | it('should remove active call when initial request fails', function(done){
90 | var channel = 'some_url',
91 | number = '111 222 333'
92 | cm = CallManager(failing, channel)
93 |
94 | cm.on('failed', function (arg) {
95 | done()
96 | })
97 | var route = 'localhost'
98 | cm.call(number, route)
99 | assert.equal(cm.active_call, null)
100 | })
101 | })
102 | describe('#hangup', function(){
103 | it('should not hang up without an active call', function(){
104 | var channel = 'some_url',
105 | number = '111 222 333'
106 |
107 | cm = CallManager({
108 | makeCall: function () {
109 | assert.ok(false)
110 | },
111 | incomingPhoneNumbers: {
112 | list: function (){}
113 | }
114 | }, channel)
115 |
116 | cm.hangup()
117 | })
118 | it('should hang up the active call', function(done){
119 | var channel = 'some_url',
120 | number = '111 222 333'
121 |
122 | var called = false
123 | cm = CallManager({calls: function () {
124 | called = true
125 | return {update: function (opts, cb) { cb(null, {CallStatus: 'completed', CallSid: 111})} }
126 | }, incomingPhoneNumbers: {list: function () {}}}, channel)
127 |
128 | cm.on('completed', function (arg) {
129 | done()
130 | })
131 | cm.active_call = {sid: 111, status: null}
132 | cm.hangup()
133 | assert.ok(called)
134 | assert.equal(null, cm.active_call)
135 | })
136 | it('should handle request failure when hanging up the active call', function(done){
137 | var channel = 'some_url',
138 | number = '111 222 333'
139 |
140 | cm = CallManager(failing, channel)
141 |
142 | cm.on('failed', function (arg) {
143 | done()
144 | })
145 |
146 | cm.active_call = {sid: null, status: null}
147 | cm.hangup()
148 | assert.equal(null, cm.active_call)
149 | })
150 | })
151 | describe('#say', function(){
152 | it('should push user text into queue of outgoing messages', function(){
153 | var channel = 'some_url',
154 | number = '111 222 333'
155 |
156 | cm = CallManager({incomingPhoneNumbers: {list: function () {}}}, channel)
157 | cm.outgoing = []
158 | cm.say('Hello world')
159 | cm.say('Hello world')
160 | cm.say('Hello world')
161 | assert.deepEqual(cm.outgoing, ['Hello world', 'Hello world', 'Hello world'])
162 | })
163 | it('should let user override the default greeting', function(){
164 | var channel = 'some_url',
165 | number = '111 222 333'
166 |
167 | cm = CallManager({incomingPhoneNumbers: {list: function () {}}}, channel)
168 | cm.outgoing = [cm.default_greeting]
169 | cm.say('Hello world')
170 | assert.deepEqual(cm.outgoing, ['Hello world'])
171 | })
172 | })
173 | describe('#options', function(){
174 | it('should let the user change the call duration', function(){
175 | cm = CallManager({incomingPhoneNumbers: {list: function () {}}})
176 | cm.options({duration: 100})
177 | assert.equal(cm.defaults.duration, 100)
178 | })
179 | })
180 | describe('#process', function(){
181 | it('should record calls when they reach in-progress state', function(){
182 | cm = CallManager({incomingPhoneNumbers: {list: function () {}}})
183 | cm.outgoing = []
184 | cm.active_call = {sid: 111}
185 | var response = cm.process({body: {CallStatus: 'in-progress', CallSid: 111}})
186 | var twiml = new twilio.TwimlResponse()
187 | twiml.record({playBeep: false, trim: 'do-not-trim', maxLength: cm.defaults.duration, timeout: 60})
188 |
189 | assert.equal(response.toString(), twiml.toString())
190 | })
191 | it('should add all outgoing speech text to response', function(){
192 | cm = CallManager({incomingPhoneNumbers: {list: function () {}}})
193 | cm.outgoing = ['Hello', 'World', '!']
194 | cm.active_call = {sid: 111}
195 | var response = cm.process({body: {CallStatus: 'in-progress', CallSid: 111}})
196 | var twiml = new twilio.TwimlResponse()
197 | twiml.say('Hello World !')
198 | twiml.record({playBeep: false, trim: 'do-not-trim', maxLength: cm.defaults.duration, timeout: 60})
199 |
200 | assert.equal(response.toString(), twiml.toString())
201 | })
202 | it('should notify listeners recording is available', function(done){
203 | cm = CallManager({incomingPhoneNumbers: {list: function () {}}})
204 | cm.outgoing = []
205 | cm.active_call = {sid: 111}
206 | cm.on('recording', function (arg) {
207 | assert.ok(arg)
208 | done()
209 | })
210 | var response = cm.process({body: {RecordingUrl: 'testing', CallStatus: 'in-progress', CallSid: 111}})
211 | })
212 | })
213 | describe('#is_message_valid', function(){
214 | it('should ignore queued messages', function(){
215 | cm = CallManager({incomingPhoneNumbers: {list: function () {}}})
216 | assert.ok(cm.is_message_valid('queued', null))
217 | })
218 | it('should match sid against current sid', function(){
219 | cm = CallManager({incomingPhoneNumbers: {list: function () {}}})
220 | cm.active_call = {sid: 1}
221 | assert.ok(cm.is_message_valid(null, 1))
222 | assert.ok(!cm.is_message_valid(null, 2))
223 | })
224 | })
225 | describe('#update_call_status', function(){
226 | it('should stop processing when invalid sid encountered', function(){
227 | cm = CallManager({incomingPhoneNumbers: {list: function () {}}})
228 | var msg = {'CallStatus': 'in-progress', sid: 2222}
229 | cm.active_call = {sid: 1111}
230 | assert.equal(null, cm.update_call_status(msg))
231 | })
232 | it('should create the new active call reference when call is started', function(done){
233 | cm = CallManager({incomingPhoneNumbers: {list: function () {}}})
234 | var msg = {'CallStatus': 'queued', sid: 'sid'}
235 | cm.on('queued', function () {
236 | done()
237 | })
238 | cm.update_call_status(msg)
239 |
240 | assert.equal(cm.active_call.sid, 'sid')
241 | assert.equal(cm.active_call.status, 'queued')
242 | })
243 | it('should notify listeners when call starts ringing', function(done){
244 | cm = CallManager({incomingPhoneNumbers: {list: function () {}}})
245 | var msg = {'CallStatus': 'ringing'}
246 | cm.on('ringing', function () {
247 | done()
248 | })
249 | cm.active_call = {}
250 | cm.update_call_status(msg)
251 |
252 | assert.equal(cm.active_call.status, 'ringing')
253 | })
254 | it('should notify listeners when call is connected', function(done){
255 | cm = CallManager({incomingPhoneNumbers: {list: function () {}}})
256 | var msg = {'CallStatus': 'in-progress'}
257 | cm.on('in-progress', function () {
258 | done()
259 | })
260 | cm.active_call = {}
261 |
262 | cm.update_call_status(msg)
263 | assert.equal(cm.active_call.status, 'in-progress')
264 | })
265 | it('should not notify listeners unless status changes', function(done){
266 | cm = CallManager({incomingPhoneNumbers: {list: function () {}}})
267 | var msg = {'CallStatus': 'in-progress'}
268 | cm.on('in-progress', function () {
269 | done()
270 | })
271 | cm.active_call = {}
272 |
273 | cm.update_call_status(msg)
274 | cm.update_call_status(msg)
275 | assert.equal(cm.active_call.status, 'in-progress')
276 | })
277 | it('should notify listeners when call is completed normally', function(done){
278 | cm = CallManager({incomingPhoneNumbers: {list: function () {}}})
279 | var msg = {'CallStatus': 'completed'}
280 | cm.on('completed', function () {
281 | done()
282 | })
283 | cm.active_call = {}
284 |
285 | cm.update_call_status(msg)
286 | assert.equal(null, cm.active_call)
287 | })
288 | it('should notify listeners when call could not connect', function(done){
289 | cm = CallManager({incomingPhoneNumbers: {list: function () {}}})
290 | var msg = {'CallStatus': 'failed'}
291 | cm.on('failed', function () {
292 | done()
293 | })
294 | cm.active_call = {}
295 |
296 | cm.update_call_status(msg)
297 | assert.equal(cm.active_call, null)
298 | })
299 | it('should notify listeners when call was not answered', function(done){
300 | cm = CallManager({incomingPhoneNumbers: {list: function () {}}})
301 | var msg = {'CallStatus': 'no-answer', 'CallSid': 111}
302 | cm.active_call = {sid: 111}
303 | cm.on('no-answer', function () {
304 | done()
305 | })
306 |
307 | cm.update_call_status(msg)
308 | assert.equal(cm.active_call, null)
309 | })
310 | it('should notify listeners when we cancel the call', function(done){
311 | cm = CallManager({incomingPhoneNumbers: {list: function () {}}})
312 | var msg = {'CallStatus': 'canceled', sid: 111}
313 | cm.active_call = {sid: 111}
314 | cm.on('canceled', function () {
315 | done()
316 | })
317 |
318 | cm.update_call_status(msg)
319 | assert.equal(cm.active_call, null)
320 | })
321 | })
322 | })
323 |
--------------------------------------------------------------------------------
/test/unit/phonebot.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert')
2 | var mockery = require('mockery')
3 | var async = require('async')
4 |
5 | var log = require('loglevel')
6 | log.disableAll()
7 |
8 | var location
9 | var cbs = {
10 | available: [],
11 | failed: []
12 | }
13 |
14 | var client = {
15 | incomingPhoneNumbers: { list: function () {}}
16 | }
17 |
18 | var ret = {
19 | start: function () {
20 | },
21 | transcript: 'Sample',
22 | on: function (id, cb) {
23 | cbs[id].push(cb)
24 | }
25 | }
26 | var translate = function (_) {
27 | location = _
28 | return ret
29 | }
30 |
31 | describe('PhoneBot', function(){
32 | before(function() {
33 | mockery.enable(); // Enable mockery at the start of your test suite
34 | mockery.warnOnUnregistered(false);
35 | mockery.registerMock('./translate.js', translate);
36 | mockery.registerMock('request', {
37 | post: function () {}
38 | })
39 | PhoneBot = require('../../lib/phonebot.js')
40 | })
41 |
42 | after(function() {
43 | mockery.deregisterMock('request');
44 | mockery.disable(); // Disable Mockery after tests are completed
45 | })
46 |
47 | describe('#constructor', function(){
48 | it('should create slackbots and phone clients for each channel', function(){
49 | var channels = {
50 | 'one': 'hook_one',
51 | 'two': 'hook_two',
52 | 'three': 'hook_three'
53 | }
54 |
55 | var pb = PhoneBot(client, null, channels)
56 | var keys = Object.keys(pb.channels)
57 | assert.deepEqual(Object.keys(channels), keys)
58 | keys.forEach(function (key) {
59 | assert.equal(pb.channels[key].bot.outgoing, channels[key])
60 | assert.equal(pb.channels[key].phone.channel, key)
61 | })
62 | })
63 | it('should not make a call if phone line is busy', function(done){
64 | var channels = {
65 | 'one': 'hook_one'
66 | }
67 |
68 | var pb = PhoneBot(client, null, channels),
69 | bot = pb.channels.one.bot,
70 | phone = pb.channels.one.phone
71 |
72 | phone.active_call = true
73 | bot.post = function () {
74 | done()
75 | }
76 |
77 | bot.emit('call', '123456789')
78 | })
79 | it('should make a call if phone line is free', function(done){
80 | var channels = {
81 | 'one': 'hook_one'
82 | }
83 |
84 | var pb = PhoneBot(client, null, channels, 'http://sample.com'),
85 | bot = pb.channels.one.bot,
86 | phone = pb.channels.one.phone
87 |
88 | phone.active_call = false
89 | phone.call = function (number, location) {
90 | assert.equal(number, '123456789')
91 | assert.equal(location, 'http://sample.com/one')
92 | done()
93 | }
94 |
95 | bot.emit('call', '123456789')
96 | })
97 | it('should sent channel messages to phone line', function(done){
98 | var channels = {
99 | 'one': 'hook_one'
100 | }
101 |
102 | var pb = PhoneBot(client, null, channels),
103 | bot = pb.channels.one.bot,
104 | phone = pb.channels.one.phone
105 |
106 | phone.active_call = true
107 | phone.say = function (text) {
108 | assert.equal(text, '123456789')
109 | done()
110 | }
111 |
112 | bot.emit('say', '123456789')
113 | })
114 | it('should sent allow user to change recording duration', function(done){
115 | var channels = {
116 | 'one': 'hook_one'
117 | }
118 |
119 | var pb = PhoneBot(client, null, channels),
120 | bot = pb.channels.one.bot,
121 | phone = pb.channels.one.phone
122 |
123 | phone.options = function (options) {
124 | assert.equal(options.duration, 100)
125 | done()
126 | }
127 |
128 | bot.emit('duration', 100)
129 | })
130 | it('should sent user to enable verbose mode', function(done){
131 | var channels = {
132 | 'one': 'hook_one'
133 | }
134 |
135 | var pb = PhoneBot(client, null, channels),
136 | bot = pb.channels.one.bot,
137 | phone = pb.channels.one.phone
138 |
139 | phone.options = function (options) {
140 | assert.equal(options.verbose, true)
141 | done()
142 | }
143 |
144 | bot.emit('verbose', true)
145 | })
146 |
147 | it('should not attempt to hang up phone call when line is not active', function(done){
148 | var channels = {
149 | 'one': 'hook_one'
150 | }
151 |
152 | var pb = PhoneBot(client, null, channels),
153 | bot = pb.channels.one.bot,
154 | phone = pb.channels.one.phone
155 |
156 | phone.active_call = false
157 | bot.post = function () {
158 | done()
159 | }
160 | bot.emit('hangup')
161 | })
162 | it('should hang up phone call when line is active', function(done){
163 | var channels = {
164 | 'one': 'hook_one'
165 | }
166 |
167 | var pb = PhoneBot(client, null, channels),
168 | bot = pb.channels.one.bot,
169 | phone = pb.channels.one.phone
170 |
171 | phone.active_call = true
172 | phone.hangup = function () {
173 | done()
174 | }
175 | bot.emit('hangup')
176 | })
177 | it('should notify channel when phone call is queued', function(done){
178 | var channels = {
179 | 'one': 'hook_one'
180 | }
181 |
182 | var pb = PhoneBot(client, null, channels),
183 | bot = pb.channels.one.bot,
184 | phone = pb.channels.one.phone
185 |
186 | phone.active_call = {number: '1234'}
187 | bot.post = function (text) {
188 | assert.equal(text, ':phone: Connecting to 1234')
189 | done()
190 | }
191 | phone.emit('queued')
192 | })
193 | it('should notify channel when phone call is ringing', function(done){
194 | var channels = {
195 | 'one': 'hook_one'
196 | }
197 |
198 | var pb = PhoneBot(client, null, channels),
199 | bot = pb.channels.one.bot,
200 | phone = pb.channels.one.phone
201 |
202 | bot.post = function (text) {
203 | assert.equal(text, ':phone: Still ringing...')
204 | done()
205 | }
206 | phone.emit('ringing')
207 | })
208 | it('should notify channel when phone call is connected', function(done){
209 | var channels = {
210 | 'one': 'hook_one'
211 | }
212 |
213 | var pb = PhoneBot(client, null, channels),
214 | bot = pb.channels.one.bot,
215 | phone = pb.channels.one.phone
216 |
217 | bot.post = function (text) {
218 | assert.equal(text, ':phone: You\'re connected! :+1:')
219 | done()
220 | }
221 | phone.emit('in-progress')
222 | })
223 | it('should notify channel when phone call is canceled', function(done){
224 | var channels = {
225 | 'one': 'hook_one'
226 | }
227 |
228 | var pb = PhoneBot(client, null, channels),
229 | bot = pb.channels.one.bot,
230 | phone = pb.channels.one.phone
231 |
232 | bot.post = function (text) {
233 | assert.equal(text, ':phone: That\'s it, call over!')
234 | done()
235 | }
236 | phone.emit('canceled')
237 | })
238 | it('should notify channel when phone call is busy', function(done){
239 | var channels = {
240 | 'one': 'hook_one'
241 | }
242 |
243 | var pb = PhoneBot(client, null, channels),
244 | bot = pb.channels.one.bot,
245 | phone = pb.channels.one.phone
246 |
247 | bot.post = function (text) {
248 | assert.equal(text, ':phone: They were busy, sorry :unamused:')
249 | done()
250 | }
251 | phone.emit('busy')
252 | })
253 | it('should notify channel when phone call is not answered', function(done){
254 | var channels = {
255 | 'one': 'hook_one'
256 | }
257 |
258 | var pb = PhoneBot(client, null, channels),
259 | bot = pb.channels.one.bot,
260 | phone = pb.channels.one.phone
261 |
262 | bot.post = function (text) {
263 | assert.equal(text, ':phone: Oh no, they didn\'t answer :sleeping:')
264 | done()
265 | }
266 | phone.emit('no-answer')
267 | })
268 | it('should notify channel when phone call fails', function(done){
269 | var channels = {
270 | 'one': 'hook_one'
271 | }
272 |
273 | var pb = PhoneBot(client, null, channels),
274 | bot = pb.channels.one.bot,
275 | phone = pb.channels.one.phone
276 |
277 | bot.post = function (text) {
278 | assert.equal(text, ':phone: Whoops, something failed. My bad. Try again? :see_no_evil:')
279 | done()
280 | }
281 | phone.emit('failed')
282 | })
283 |
284 | it('should post message when phone call ends once queue empties', function(done){
285 | var channels = {
286 | 'one': 'hook_one'
287 | }
288 |
289 | var pb = PhoneBot(client, null, channels),
290 | bot = pb.channels.one.bot,
291 | phone = pb.channels.one.phone
292 |
293 | pb.channels.one.queue = async.queue(function (task, callback) {
294 | setTimeout(function () {
295 | callback()
296 | }, 100)
297 | })
298 |
299 | pb.channels.one.queue.push({})
300 | pb.channels.one.queue.push({})
301 | pb.channels.one.queue.push({})
302 |
303 | bot.post = function (text) {
304 | assert.ok(pb.channels.one.queue.idle())
305 | assert.equal(text, ':phone: That\'s it, call over!')
306 | done()
307 | }
308 |
309 | phone.emit('completed')
310 | })
311 | it('should post message when phone call ends and the queue is empty', function(done){
312 | var channels = {
313 | 'one': 'hook_one'
314 | }
315 |
316 | var pb = PhoneBot(client, null, channels),
317 | bot = pb.channels.one.bot,
318 | phone = pb.channels.one.phone
319 |
320 | bot.post = function (text) {
321 | assert.equal(text, ':phone: That\'s it, call over!')
322 | done()
323 | }
324 | phone.emit('completed')
325 | })
326 | it('should schedule translation when recording is available', function(done){
327 | var channels = {
328 | 'one': 'hook_one'
329 | }
330 |
331 | var pb = PhoneBot(client, null, channels),
332 | bot = pb.channels.one.bot,
333 | phone = pb.channels.one.phone
334 |
335 | var messages = [':speech_balloon: Sample', ':speech_balloon: _waiting for translation_']
336 | bot.post = function (text) {
337 | assert.equal(text, messages[messages.length-1])
338 | messages.pop()
339 | if (messages.length === 0) done()
340 | }
341 | // Need to mock out translate
342 | phone.emit('recording', 'location')
343 | })
344 | it('should post translation to channel when async task finishes', function(done){
345 | var channels = {
346 | 'one': 'hook_one'
347 | }
348 |
349 | var pb = PhoneBot(client, null, channels),
350 | bot = pb.channels.one.bot,
351 | phone = pb.channels.one.phone
352 |
353 | ret.transcript = null
354 | cbs.available = []
355 | cbs.failed = []
356 |
357 | var messages = [':speech_balloon: Sample', ':speech_balloon: _waiting for translation_']
358 | bot.post = function (text) {
359 | assert.equal(text, messages[messages.length-1])
360 | messages.pop()
361 | if (messages.length === 0) done()
362 | }
363 |
364 | // Need to mock out translate
365 | phone.emit('recording', 'location')
366 | setTimeout(function () {
367 | ret.transcript = "Sample"
368 | cbs.available[0]()
369 | }, 10)
370 | })
371 | it('should handle multiple translation tasks being queued', function(done){
372 | var channels = {
373 | 'one': 'hook_one'
374 | }
375 |
376 | var pb = PhoneBot(client, null, channels),
377 | bot = pb.channels.one.bot,
378 | phone = pb.channels.one.phone
379 |
380 | ret.transcript = null
381 | cbs.available = []
382 | cbs.failed = []
383 |
384 | var count = 0
385 |
386 | bot.post = function (text) {
387 | if (text === ':speech_balloon: _waiting for translation_') return
388 | assert.equal(text, ':speech_balloon: transcript')
389 | if (++count === 2) done()
390 | }
391 | // Need to mock out translate
392 | phone.emit('recording', 'location')
393 | phone.emit('recording', 'location')
394 | phone.emit('recording', 'location')
395 |
396 | setTimeout(function () {
397 | ret.transcript = "transcript"
398 | cbs.available[0]()
399 | }, 50)
400 | })
401 | it('should handle failed translation tasks', function(done){
402 | var channels = {
403 | 'one': 'hook_one'
404 | }
405 |
406 | var pb = PhoneBot(client, null, channels),
407 | bot = pb.channels.one.bot,
408 | phone = pb.channels.one.phone
409 |
410 | ret.transcript = null
411 | cbs.available = []
412 | cbs.failed = []
413 |
414 | bot.post = function (text) {
415 | if (text === ':speech_balloon: _waiting for translation_') return
416 | assert.equal(text, ':speech_balloon: _unable to recognise speech_')
417 | done()
418 | }
419 | // Need to mock out translate
420 | phone.emit('recording', 'location')
421 | phone.emit('recording', 'location')
422 |
423 | setTimeout(function () {
424 | ret.transcript = "transcript"
425 | cbs.failed[0]()
426 | }, 50)
427 | })
428 | })
429 | describe('#phone_message', function(){
430 | it('should ignore messages for channels not registered', function(){
431 | var channels = {
432 | 'one': 'hook_one'
433 | }
434 |
435 | var pb = PhoneBot(client, null, channels),
436 | phone = pb.channels.one.phone
437 |
438 | assert.equal(null, pb.phone_message('two', null))
439 | })
440 | it('should process messages for registered channels', function(){
441 | var channels = {
442 | 'one': 'hook_one'
443 | }
444 |
445 | var pb = PhoneBot(client, null, channels),
446 | phone = pb.channels.one.phone
447 |
448 | phone.process = function () {
449 | return "testing"
450 | }
451 | assert.equal('testing', pb.phone_message('one', null))
452 | })
453 | })
454 | describe('#slack_message', function(){
455 | it('should ignore messages for channels not registered', function(){
456 | var channels = {
457 | 'one': 'hook_one'
458 | }
459 |
460 | var pb = PhoneBot(client, null, channels),
461 | phone = pb.channels.one.phone
462 |
463 | assert.equal(null, pb.slack_message('two', null))
464 | })
465 | it('should process messages for registered channels', function(){
466 | var channels = {
467 | 'one': 'hook_one'
468 | }
469 |
470 | var pb = PhoneBot(client, null, channels),
471 | bot = pb.channels.one.bot
472 |
473 | var called = false
474 | bot.channel_message = function () {
475 | called = true
476 | }
477 | pb.slack_message('one', null)
478 | assert.equal(true, called)
479 | })
480 | })
481 | })
482 |
--------------------------------------------------------------------------------