diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8be7812 --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +TWILIO_ACCOUNT_SID=ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +TWILIO_AUTH_TOKEN=your_auth_token +TWILIO_TWIML_APP_SID=APXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +TWILIO_CALLER_ID=+1XXXYYYZZZZ diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..9f9b48d --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +public/**/*.js diff --git a/.eslintrc.yml b/.eslintrc.yml new file mode 100644 index 0000000..380428b --- /dev/null +++ b/.eslintrc.yml @@ -0,0 +1,3 @@ +extends: google +parserOptions: + ecmaVersion: 6 diff --git a/.gitignore b/.gitignore index 0beed58..db872f6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ .DS_Store node_modules npm-debug.log -config.js +.env diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..60a96a9 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +language: node_js +node_js: + - 'stable' +env: + global: + - TWILIO_ACCOUNT_SID=ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + - TWILIO_AUTH_TOKEN=your_auth_token + - TWILIO_TWIML_APP_SID=APXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + - TWILIO_CALLER_ID=+1XXXYYYZZZZ diff --git a/LICENSE b/LICENSE index 4bcc4c2..54b85ad 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,6 @@ -Copyright (c) 2015 Twilio Inc. +MIT License + +Copyright (c) 2017 Twilio Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -7,13 +9,13 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. \ No newline at end of file +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index f1beddb..fc8bd94 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,10 @@ + + Twilio + + # Twilio Client Quickstart for Node.js +[![Build Status](https://travis-ci.org/TwilioDevEd/client-quickstart-node.svg?branch=master)](https://travis-ci.org/TwilioDevEd/client-quickstart-node) + This application should give you a ready-made starting point for writing your own voice apps with Twilio Client. Before we begin, we need to collect @@ -15,50 +21,60 @@ Twilio Phone # | A Twilio phone number in [E.164 format](https://en.wi 1. Create a configuration file for your application: - ```bash - cp config.sample.js config.js - ``` + ```bash + cp .env.example .env + ``` -2. Edit `config.js` with the four configuration parameters we gathered from above. +1. Edit `config.js` with the four configuration parameters we gathered from above. -3. Next, we need to install our dependencies from npm: +1. Next, we need to install our dependencies from npm: - ```bash - npm install - ``` + ```bash + npm install + ``` -4. Now we should be all set! Run the application using `npm`. +1. Now we should be all set! Run the application using `npm`. - ```bash - npm start - ``` + ```bash + npm start + ``` - Your application should now be running at http://localhost:3000. - Leave the server running and continue on in another command window. + Your application should now be running at http://localhost:3000. + Leave the server running and continue on in another command window. -5. [Download and install ngrok](https://ngrok.com/download) +1. [Download and install ngrok](https://ngrok.com/download) -6. Run ngrok: +1. Run ngrok: - ```bash - ngrok http 3000 - ``` + ```bash + ngrok http 3000 + ``` -7. When ngrok starts up, it will assign a unique URL to your tunnel. -It might be something like `https://asdf456.ngrok.io`. Take note of this. Note you **must** use the https URL, otherwise some browsers will block microphone access. +1. When ngrok starts up, it will assign a unique URL to your tunnel. + It might be something like `https://asdf456.ngrok.io`. Take note of this. + Note you **must** use the https URL, otherwise some browsers will block + microphone access. -8. [Configure your TwiML app](https://www.twilio.com/console/phone-numbers/dev-tools/twiml-apps)'s +1. [Configure your TwiML app](https://www.twilio.com/console/phone-numbers/dev-tools/twiml-apps)'s Voice "REQUEST URL" to be your ngrok URL plus `/voice`. For example: - ![screenshot of twiml app](https://s3.amazonaws.com/com.twilio.prod.twilio-docs/images/TwilioClientRequestUrl.original.png) + ![screenshot of twiml app](https://s3.amazonaws.com/com.twilio.prod.twilio-docs/images/TwilioClientRequestUrl.original.png) + + You should now be ready to rock! Make some phone calls. + Open it on another device and call yourself. Note that Twilio Client requires + WebRTC enabled browsers, so Edge and Internet Explorer will not work for + testing. We'd recommend Google Chrome or Mozilla Firefox instead. + + ![screenshot of chat app](https://s3.amazonaws.com/com.twilio.prod.twilio-docs/images/TwilioClientQuickstart.original.png) -You should now be ready to rock! Make some phone calls. -Open it on another device and call yourself. Note that Twilio Client requires -WebRTC enabled browsers, so Edge and Internet Explorer will not work for testing. -We'd recommend Google Chrome or Mozilla Firefox instead. +### Run tests -![screenshot of chat app](https://s3.amazonaws.com/com.twilio.prod.twilio-docs/images/TwilioClientQuickstart.original.png) +```bash +npm test +``` -## License +## Meta -MIT +* No warranty expressed or implied. Software is as is. Diggity. +* [MIT License](http://www.opensource.org/licenses/mit-license.html) +* Lovingly crafted by Twilio Developer Education. diff --git a/config.js b/config.js new file mode 100644 index 0000000..a38d135 --- /dev/null +++ b/config.js @@ -0,0 +1,26 @@ +const dotenv = require('dotenv'); +const cfg = {}; + +if (process.env.NODE_ENV !== 'test') { + dotenv.config({path: '.env'}); +} else { + dotenv.config({path: '.env.example', silent: true}); +} + +// HTTP Port to run our web application +cfg.port = process.env.PORT || 3000; + +// Your Twilio account SID and auth token, both found at: +// https://www.twilio.com/user/account +// +// A good practice is to store these string values as system environment +// variables, and load them from there as we are doing below. Alternately, +// you could hard code these values here as strings. +cfg.accountSid = process.env.TWILIO_ACCOUNT_SID; +cfg.authToken = process.env.TWILIO_AUTH_TOKEN; + +cfg.twimlAppSid = process.env.TWILIO_TWIML_APP_SID; +cfg.callerId = process.env.TWILIO_CALLER_ID; + +// Export configuration object +module.exports = cfg; diff --git a/config.sample.js b/config.sample.js deleted file mode 100644 index 8d7d5ee..0000000 --- a/config.sample.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - TWILIO_ACCOUNT_SID: 'ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', - TWILIO_AUTH_TOKEN: 'your_auth_token', - TWILIO_TWIML_APP_SID: 'APXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', - TWILIO_CALLER_ID: '+1XXXYYYZZZZ' -}; diff --git a/index.js b/index.js index e477a78..ee83d69 100644 --- a/index.js +++ b/index.js @@ -1,66 +1,21 @@ -var http = require('http'); -var path = require('path'); -var ClientCapability = require('twilio').jwt.ClientCapability; -var VoiceResponse = require('twilio').twiml.VoiceResponse; -var express = require('express'); -var bodyParser = require('body-parser'); -var randomUsername = require('./randos'); -var config = require('./config'); +const http = require('http'); +const path = require('path'); +const express = require('express'); +const bodyParser = require('body-parser'); + +const router = require('./src/router'); // Create Express webapp -var app = express(); +const app = express(); app.use(express.static(path.join(__dirname, 'public'))); -app.use(bodyParser.urlencoded({ extended: false })); - -/* -Generate a Capability Token for a Twilio Client user - it generates a random -username for the client requesting a token. -*/ -app.get('/token', function(request, response) { - var identity = randomUsername(); - var capability = new ClientCapability({ - accountSid: config.TWILIO_ACCOUNT_SID, - authToken: config.TWILIO_AUTH_TOKEN - }); - - capability.addScope(new ClientCapability.IncomingClientScope(identity)); - capability.addScope(new ClientCapability.OutgoingClientScope({ - applicationSid: config.TWILIO_TWIML_APP_SID, - clientName: identity - })); - - // Include identity and token in a JSON response - response.send({ - identity: identity, - token: capability.toJwt() - }); -}); +app.use(bodyParser.urlencoded({extended: false})); -app.post('/voice', function (req, res) { - // Create TwiML response - var twiml = new VoiceResponse(); - - if(req.body.To) { - twiml.dial({ callerId: config.TWILIO_CALLER_ID}, function() { - // wrap the phone number or client name in the appropriate TwiML verb - // by checking if the number given has only digits and format symbols - if (/^[\d\+\-\(\) ]+$/.test(req.body.To)) { - this.number(req.body.To); - } else { - this.client(req.body.To); - } - }); - } else { - twiml.say("Thanks for calling!"); - } - - res.set('Content-Type', 'text/xml'); - res.send(twiml.toString()); -}); +app.use(router); // Create http server and run it -var server = http.createServer(app); -var port = process.env.PORT || 3000; +const server = http.createServer(app); +const port = process.env.PORT || 3000; + server.listen(port, function() { - console.log('Express server running on *:' + port); + console.log('Express server running on *:' + port); }); diff --git a/name_generator.js b/name_generator.js new file mode 100644 index 0000000..c811c32 --- /dev/null +++ b/name_generator.js @@ -0,0 +1,25 @@ +const ADJECTIVES = [ + 'Abrasive', 'Brash', 'Callous', 'Daft', 'Eccentric', 'Fiesty', 'Golden', + 'Holy', 'Ignominious', 'Joltin', 'Killer', 'Luscious', 'Mushy', 'Nasty', + 'OldSchool', 'Pompous', 'Quiet', 'Rowdy', 'Sneaky', 'Tawdry', + 'Unique', 'Vivacious', 'Wicked', 'Xenophobic', 'Yawning', 'Zesty', +]; + +const FIRST_NAMES = [ + 'Anna', 'Bobby', 'Cameron', 'Danny', 'Emmett', 'Frida', 'Gracie', 'Hannah', + 'Isaac', 'Jenova', 'Kendra', 'Lando', 'Mufasa', 'Nate', 'Owen', 'Penny', + 'Quincy', 'Roddy', 'Samantha', 'Tammy', 'Ulysses', 'Victoria', 'Wendy', + 'Xander', 'Yolanda', 'Zelda', +]; + +const LAST_NAMES = [ + 'Anchorage', 'Berlin', 'Cucamonga', 'Davenport', 'Essex', 'Fresno', + 'Gunsight', 'Hanover', 'Indianapolis', 'Jamestown', 'Kane', 'Liberty', + 'Minneapolis', 'Nevis', 'Oakland', 'Portland', 'Quantico', 'Raleigh', + 'SaintPaul', 'Tulsa', 'Utica', 'Vail', 'Warsaw', 'XiaoJin', 'Yale', + 'Zimmerman', +]; + +const rand = (arr) => arr[Math.floor(Math.random() * arr.length)]; + +module.exports = () => rand(ADJECTIVES) + rand(FIRST_NAMES) + rand(LAST_NAMES); diff --git a/package.json b/package.json index cac738d..9189cda 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "main": "index.js", "scripts": { "start": "node index.js", - "test": "echo \"Error: no test specified\" && exit 1" + "test": "NODE_ENV=test ./node_modules/.bin/eslint . && ./node_modules/.bin/jest" }, "repository": { "type": "git", @@ -24,12 +24,17 @@ "author": "Twilio Developer Education", "license": "MIT", "engines": { - "node": ">=4.1.0 <5.5.0" + "node": ">=6.x" }, "dependencies": { "body-parser": "^1.15.2", "dotenv": "^1.2.0", "express": "^4.13.3", "twilio": "~3.0.0-rc.16" + }, + "devDependencies": { + "eslint": "^3.19.0", + "eslint-config-google": "^0.7.1", + "jest": "^19.0.2" } } diff --git a/randos.js b/randos.js deleted file mode 100644 index 544c424..0000000 --- a/randos.js +++ /dev/null @@ -1,30 +0,0 @@ -var ADJECTIVES = [ - 'Abrasive', 'Brash', 'Callous', 'Daft', 'Eccentric', 'Fiesty', 'Golden', - 'Holy', 'Ignominious', 'Joltin', 'Killer', 'Luscious', 'Mushy', 'Nasty', - 'OldSchool', 'Pompous', 'Quiet', 'Rowdy', 'Sneaky', 'Tawdry', - 'Unique', 'Vivacious', 'Wicked', 'Xenophobic', 'Yawning', 'Zesty' -]; - -var FIRST_NAMES = [ - 'Anna', 'Bobby', 'Cameron', 'Danny', 'Emmett', 'Frida', 'Gracie', 'Hannah', - 'Isaac', 'Jenova', 'Kendra', 'Lando', 'Mufasa', 'Nate', 'Owen', 'Penny', - 'Quincy', 'Roddy', 'Samantha', 'Tammy', 'Ulysses', 'Victoria', 'Wendy', - 'Xander', 'Yolanda', 'Zelda' -]; - -var LAST_NAMES = [ - 'Anchorage', 'Berlin', 'Cucamonga', 'Davenport', 'Essex', 'Fresno', - 'Gunsight', 'Hanover', 'Indianapolis', 'Jamestown', 'Kane', 'Liberty', - 'Minneapolis', 'Nevis', 'Oakland', 'Portland', 'Quantico', 'Raleigh', - 'SaintPaul', 'Tulsa', 'Utica', 'Vail', 'Warsaw', 'XiaoJin', 'Yale', - 'Zimmerman' -]; - -function randomUsername() { - function rando(arr) { - return arr[Math.floor(Math.random()*arr.length)]; - } - return rando(ADJECTIVES) + rando(FIRST_NAMES) + rando(LAST_NAMES); -} - -module.exports = randomUsername; \ No newline at end of file diff --git a/src/handler.js b/src/handler.js new file mode 100644 index 0000000..73eb54f --- /dev/null +++ b/src/handler.js @@ -0,0 +1,54 @@ +const ClientCapability = require('twilio').jwt.ClientCapability; +const VoiceResponse = require('twilio').twiml.VoiceResponse; + +const nameGenerator = require('../name_generator'); +const config = require('../config'); + +exports.tokenGenerator = function tokenGenerator() { + const identity = nameGenerator(); + const capability = new ClientCapability({ + accountSid: config.accountSid, + authToken: config.authToken, + }); + + capability.addScope(new ClientCapability.IncomingClientScope(identity)); + capability.addScope(new ClientCapability.OutgoingClientScope({ + applicationSid: config.twimlAppSid, + clientName: identity, + })); + + // Include identity and token in a JSON response + return { + identity: identity, + token: capability.toJwt(), + }; +}; + +exports.voiceResponse = function voiceResponse(toNumber) { + // Create a TwiML voice response + const twiml = new VoiceResponse(); + + if(toNumber) { + // Wrap the phone number or client name in the appropriate TwiML verb + // if is a valid phone number + const attr = isAValidPhoneNumber(toNumber) ? 'number' : 'client'; + + twiml.dial({ + [attr]: toNumber, + callerId: config.callerId, + }); + } else { + twiml.say('Thanks for calling!'); + } + + return twiml.toString(); +}; + +/** +* Checks if the given value is valid as phone number +* @param {Number|String} number +* @return {Boolean} +*/ +function isAValidPhoneNumber(number) { + return /^[\d\+\-\(\) ]+$/.test(number); +} diff --git a/src/router.js b/src/router.js new file mode 100644 index 0000000..154955a --- /dev/null +++ b/src/router.js @@ -0,0 +1,20 @@ +const Router = require('express').Router; + +const {tokenGenerator, voiceResponse} = require('./handler'); + +const router = new Router(); + +/** + * Generate a Capability Token for a Twilio Client user - it generates a random + * username for the client requesting a token. + */ +router.get('/token', (req, res) => { + res.send(tokenGenerator()); +}); + +router.post('/voice', (req, res) => { + res.set('Content-Type', 'text/xml'); + res.send(voiceResponse(req.body.To)); +}); + +module.exports = router; diff --git a/tests/handler.test.js b/tests/handler.test.js new file mode 100644 index 0000000..f4669d3 --- /dev/null +++ b/tests/handler.test.js @@ -0,0 +1,70 @@ +const jwt = require('jsonwebtoken'); +const {tokenGenerator, voiceResponse} = require('../src/handler'); + +const when = describe; + +describe('#tokenGenerator', () => { + it('generates a new token', () => { + const token = tokenGenerator(); + const decoded = jwt.decode(token.token, {complete: true}); + + expect(decoded.payload.scope).toContain('incoming'); + expect(decoded.payload.scope).toContain('outgoing'); + expect(decoded.payload.scope).toContain(`clientName=${token.identity}`); + }); +}); + +describe('#voiceResponse', () => { + when('receives an empty or no value value', () => { + it('returns a goodbye message', () => { + const twiml = voiceResponse(); + const count = countWord(twiml); + + // TwiML Verbs + expect(count('Say')).toBe(2); + + // TwiML content + expect(twiml).toContain('Thanks for calling!'); + }); + }); + + when('receives a value as string', () => { + it('returns a dial verb with the client attribute', () => { + const toNumber = 'BigBoss'; + const twiml = voiceResponse(toNumber); + const count = countWord(twiml); + + // TwiML Verbs + expect(count('Dial')).toBe(1); + + // TwiML options + expect(twiml).toContain(`client="${toNumber}"`); + }); + }); + + when('receives a valid phone number', () => { + it('returns a dial verb with the number attribute', () => { + const toNumber = '+1235555555'; + const twiml = voiceResponse(toNumber); + const count = countWord(twiml); + + // TwiML Verbs + expect(count('Dial')).toBe(1); + + // TwiML options + expect(twiml).toContain(`number="${toNumber}"`); + }); + }); +}); + +/** + * Counts how many times a word is repeated + * @param {String} paragraph + * @return {String[]} + */ +function countWord(paragraph) { + return (word) => { + const regex = new RegExp(`\<${word}[ | \/?\>]|\<\/${word}?\>`); + return (paragraph.split(regex).length - 1); + }; +}