├── .gitignore ├── lib ├── index.js ├── AvailablePhoneNumber.js ├── Conference.js ├── SMSMessage.js ├── ListIterator.js ├── Tag.js ├── Account.js ├── util.js ├── Client.js ├── Call.js └── Application.js ├── test ├── sample-credentials.js ├── accounts.js ├── sms_server.js ├── server.js └── basics.js ├── package.json ├── LICENSE.txt └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | test/credentials.js 2 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | exports.Client = require('./Client'); -------------------------------------------------------------------------------- /lib/AvailablePhoneNumber.js: -------------------------------------------------------------------------------- 1 | function AvailablePhoneNumber(account) { 2 | this._account = account; 3 | this._client = account._client; 4 | } 5 | module.exports = AvailablePhoneNumber; 6 | /* This will eventually have methods like: 7 | - provisionNumber() 8 | or... whatever 9 | */ -------------------------------------------------------------------------------- /lib/Conference.js: -------------------------------------------------------------------------------- 1 | var utilFuncs = require('./util'); 2 | 3 | function Conference(app, Sid) { 4 | this._app = app; 5 | this._client = app._client; 6 | this._parent = app._account; 7 | } 8 | module.exports = Conference; 9 | 10 | Conference.prototype._getResourceURI = function(type) { 11 | return this._parent._getResourceURI(type) + '/Conferences/' + this.Sid; 12 | } 13 | Conference.prototype.load = utilFuncs.globalLoad; 14 | Conference.prototype.save = utilFuncs.globalSave; 15 | -------------------------------------------------------------------------------- /lib/SMSMessage.js: -------------------------------------------------------------------------------- 1 | var util = require('util'), EventEmitter = require('events').EventEmitter, utilFuncs = require('./util'); function SMSMessage(app, sid) { this._app = app; this._parent = app._account; this._client = app._client; this._cb = {}; //Indexed by callback ID this._cbID = 0; EventEmitter.call(this); //Make `this` a new EventEmitter } util.inherits(SMSMessage, EventEmitter); //Inherit all EventEmitter prototype methods module.exports = SMSMessage; SMSMessage.prototype._getResourceURI = function(type) { return this._parent._getResourceURI(type) + '/SMS/Messages/' + this.Sid; } SMSMessage.prototype.load = utilFuncs.globalLoad; SMSMessage.prototype.reply = function(body, cb) { if(this.Direction != "incoming") throw new Error("You cannot reply to an outbound SMS Message."); this._app.sendSMS(this.To, this.From, body, cb); } -------------------------------------------------------------------------------- /test/sample-credentials.js: -------------------------------------------------------------------------------- 1 | /* The test suite can optionally use your credentials to perform tests. 2 | 1.) Copy this file to test/credentials.js 3 | 2.) Enter your credentials and save the file 4 | 5 | The .gitignore file will ignore test/credentials.js, so you can safely commit without 6 | having to worry about accidentally publishing your Twilio account ID and auth token. 7 | 8 | CAUTION: You may be billed for running the test suite. 9 | */ 10 | exports.AccountSid = "AC................................"; //Twilio AccountSid 11 | exports.AuthToken = "................................"; //Twilio AuthToken 12 | exports.ApplicationSid = "AP................................"; 13 | exports.FromNumber = "+1.........."; //Your sandbox number can go here 14 | exports.ToNumber = "+1.........."; //A test number to call when using the test suite. 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Blake Miner (http://www.blakeminer.com/)", 3 | "contributors": [ "Jacob Williams (http://iakob.com/)" ], 4 | "name": "twilio-api", 5 | "description": "Add voice and SMS messaging capabilities to your Node.JS applications with node-twilio-api - a high-level Twilio helper library to make Twilio API requests, handle incoming requests, and generate TwiML", 6 | "version": "0.3.3", 7 | "homepage": "https://github.com/bminer/node-twilio-api/", 8 | "repository": { 9 | "type": "git", 10 | "url": "git://github.com/bminer/node-twilio-api.git" 11 | }, 12 | "main": "./lib", 13 | "scripts": { 14 | "test": "NODE_ENV=development nodeunit ./test" 15 | }, 16 | "engines": { 17 | "node": ">=0.4" 18 | }, 19 | "dependencies": {}, 20 | "devDependencies": { 21 | "nodeunit": ">=0.6.4" 22 | }, 23 | "optionalDependencies": { 24 | "express": ">=3.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2012 Blake Miner (http://www.blakeminer.com) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /lib/ListIterator.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | function ListIterator(parent, command, _class, filters) { 3 | this._parent = parent; 4 | this._client = parent._client; 5 | this.command = command; 6 | this._class = _class; 7 | this.filters = filters; 8 | this.Page = 0; 9 | this.NumPages = 1; 10 | this.PageSize = 20; 11 | } 12 | module.exports = ListIterator; 13 | ListIterator.prototype.nextPage = function(cb) { 14 | if(this.Page < this.NumPages - 1) 15 | { 16 | this.Page++; 17 | this.load(cb); 18 | } 19 | else 20 | cb(new Error('No next page.')); 21 | } 22 | ListIterator.prototype.prevPage = function(cb) { 23 | if(this.Page > 0) 24 | { 25 | this.Page--; 26 | this.load(cb); 27 | } 28 | else 29 | cb(new Error('No previous page.')); 30 | } 31 | ListIterator.prototype.load = function(cb) { 32 | var li = this; 33 | var data = { 34 | 'Page': li.Page, 35 | 'PageSize': li.PageSize 36 | }; 37 | for(var i in li.filters) 38 | data[i] = li.filters[i]; 39 | li._client._restAPI('GET', li.command, data, function(err, res) { 40 | if(err) return cb(err); 41 | for(var i in res.body) 42 | li[i] = res.body[i]; 43 | //There's some pretty abstract shiz goin' on up in here! WTF does it mean? 44 | var list = li.command.split("/"); 45 | list = list[list.length - 1]; 46 | li.Results = li[list]; 47 | list = li[list]; 48 | for(var i in list) 49 | { 50 | //It means take the Objects and cast them into their class! 51 | var tmp = list[i]; 52 | li._class.call(tmp, li._parent, tmp.Sid); 53 | tmp.__proto__ = li._class.prototype; 54 | tmp.constructor = li._class; 55 | list[i] = tmp; 56 | } 57 | cb(null, li); 58 | }); 59 | }; 60 | -------------------------------------------------------------------------------- /test/accounts.js: -------------------------------------------------------------------------------- 1 | var twilio = require('../lib'), 2 | client, 3 | Account = require('../lib/Account') 4 | basicTest = require('./basics'); 5 | 6 | exports.getTwilioCredentials = basicTest.getTwilioCredentials; 7 | exports.constructClient = function() { 8 | client = basicTest.constructClient.apply(this, arguments); 9 | } 10 | 11 | exports.getMainAccountDetails = function(t) { 12 | t.expect(6); 13 | client.account.load(function(err, account) { 14 | t.ifError(err); 15 | t.ok(account != null, "Account object is " + account); 16 | if(account) 17 | { 18 | t.ok(account == client.account, "Account Objects do not match"); 19 | t.ok(account.AuthToken == client.AuthToken, "Account token does not match"); 20 | t.ok(client.AccountSid == account.Sid, "Account Sid does not match"); 21 | t.ok(account.Status == 'active', "Account is not active?"); 22 | } 23 | t.done(); 24 | }); 25 | } 26 | 27 | var subAccountName = "Testing 2878373748734872"; 28 | exports.createSubAccount = function(t) { 29 | t.expect(5); 30 | client.createSubAccount(subAccountName, function(err, account) { 31 | t.ifError(err); 32 | t.ok(account != null, "Account object is " + account); 33 | if(account) 34 | { 35 | t.ok(account != client.account, "Account is the same as main account"); 36 | t.ok(account.FriendlyName == subAccountName, "Account friendly name does not match"); 37 | t.ok(account.Status == 'active', "Account is not active?"); 38 | } 39 | t.done(); 40 | }); 41 | } 42 | 43 | var subAccount; 44 | exports.getSubAccountByFriendlyName = function(t) { 45 | t.expect(4); 46 | client.listAccounts({'FriendlyName': subAccountName, 'Status': 'active'}, function(err, li) { 47 | t.ifError(err); 48 | t.ok(li.Accounts.length == 1, "Multiple sub accounts with testing FriendlyName"); 49 | t.ok(li.Accounts[0] instanceof Account, "Not an instance of Account class"); 50 | subAccount = li.Accounts[0]; 51 | t.ok(subAccount.Status == 'active', "Account is not active?"); 52 | t.done(); 53 | }); 54 | } 55 | 56 | exports.closeSubAccount = function(t) { 57 | t.expect(4); 58 | if(subAccount) 59 | subAccount.closeAccount(function(err) { 60 | t.ifError(err); 61 | t.ok(subAccount.Status == 'closed', "Account is not closed"); 62 | subAccount.load(function(err) { 63 | t.ifError(err); 64 | //Double-check... 65 | t.ok(subAccount.Status == 'closed', "Account is not closed"); 66 | t.done(); 67 | }); 68 | }); 69 | } -------------------------------------------------------------------------------- /lib/Tag.js: -------------------------------------------------------------------------------- 1 | /** Constructor for a Tag Object, which represents an XML tag. 2 | @param name - the name of the tag. 3 | */ 4 | var Tag = module.exports = function Tag(name) { 5 | this.name = name; 6 | this.attributes = {}; 7 | }; 8 | /** Set an attribute for this tag. 9 | @param name 10 | @param value 11 | */ 12 | Tag.prototype.setAttribute = function(name, value) { 13 | if(name != null && value != null) 14 | this.attributes[name] = value; 15 | return this; 16 | } 17 | /** Appends a Tag, a string, or an array of these to the content 18 | of the tag. 19 | @param content - a Tag object, a string, or an array containing a mixture 20 | of the two 21 | */ 22 | Tag.prototype.append = function(content) { 23 | if(this.content == undefined) 24 | this.content = []; 25 | this.content.push(content); 26 | return this; 27 | } 28 | /** Generates an XML string with this Tag as the root element. 29 | @param excludeXMLDecl - if false or unspecified, the XML declaration will 30 | be prepended to the XML string; if true, the XML declation will not be 31 | prepended to the XML string, allowing you to render parts of an XML 32 | document. 33 | @return an XML string 34 | */ 35 | Tag.prototype.render = function(excludeXMLDecl) { 36 | var str = ''; 37 | if(excludeXMLDecl !== true) 38 | str = ''; 39 | str += '<' + this.name; 40 | for(var i in this.attributes) 41 | str += " " + i + '="' + escape(this.attributes[i]) + '"'; 42 | if(typeof this.content == "string") 43 | str += ">" + escape(this.content) + ""; 44 | else if(this.content instanceof Array) 45 | { 46 | str += ">"; 47 | for(var i in this.content) 48 | { 49 | if(typeof this.content[i] == "string") 50 | str += escape(this.content[i]); 51 | else if(this.content[i] instanceof Tag) 52 | str += this.content[i].render(true); //Recursive rendering 53 | } 54 | str += ""; 55 | } 56 | else 57 | str += "/>"; 58 | return str; 59 | } 60 | /** toString function is an alias for render. 61 | @see #render(...) 62 | */ 63 | Tag.prototype.toString = Tag.prototype.render; 64 | /** Escapes certain HTML entities. Similar to PHP's htmlspecialchars. 65 | @param html - the string to escape (i.e. " & John") 66 | @return the escaped string (i.e. "<Smith> & John") 67 | */ 68 | function escape(html) { 69 | return String(html) 70 | .replace(/&(?!\w+;)/g, '&') 71 | .replace(//g, '>') 73 | .replace(/"/g, '"'); 74 | }; -------------------------------------------------------------------------------- /test/sms_server.js: -------------------------------------------------------------------------------- 1 | var twilio = require('../lib'), 2 | basicTest = require('./basics'), 3 | client, 4 | express = require('express'), 5 | app = express.createServer(), 6 | http, 7 | tapp; 8 | 9 | exports.getTwilioCredentials = basicTest.getTwilioCredentials; 10 | exports.constructClient = function() { 11 | client = basicTest.constructClient.apply(this, arguments); 12 | } 13 | 14 | exports.setupExpressMiddleware = function(t) { 15 | t.expect(2); 16 | t.equal(typeof client.middleware, "function"); 17 | t.equal(typeof express.errorHandler, "function"); 18 | if(process.env.NODE_ENV == "development") 19 | app.use(function(req, res, next) { 20 | console.log("\033[46m\033[30m" + "Incoming request: " + req.method + " " + req.url + "\033[0m"); 21 | next(); 22 | }); 23 | app.use(client.middleware() ); 24 | app.use(express.errorHandler({ 25 | 'showMessage': true, 26 | 'dumpExceptions': true 27 | }) ); 28 | http = app.listen(8002); 29 | t.done(); 30 | } 31 | 32 | exports.loadApplication = function(t) { 33 | t.expect(2); 34 | client.account.getApplication(client.credentials.ApplicationSid, function(err, app) { 35 | t.ifError(err); 36 | t.notEqual(app, null, "Application is null or undefined"); 37 | tapp = app; 38 | t.done(); 39 | }); 40 | } 41 | 42 | exports.registerApplication = function(t) { 43 | t.expect(6); 44 | t.equal(client._appMiddlewareSids.length, 0); 45 | t.equal(Object.keys(client._appMiddleware).length, 0); 46 | tapp.register(); 47 | t.equal(client._appMiddlewareSids.length, 1); 48 | t.equal(client._appMiddlewareSids[0], tapp.Sid); 49 | t.equal(Object.keys(client._appMiddleware).length, 1); 50 | t.equal(typeof client._appMiddleware[tapp.Sid], "function"); 51 | t.done(); 52 | } 53 | 54 | /* Place a call from caller ID credentials.FromNumber to credentials.ToNumber. 55 | Callee must pick up the phone and press a key for this test to be successful. */ 56 | exports.sendSMS = function(t) { 57 | var credentials = client.credentials; 58 | if(credentials.FromNumber && credentials.ToNumber) 59 | { 60 | t.expect(4); 61 | console.log("Sending SMS to " + credentials.ToNumber); 62 | tapp.sendSMS(credentials.FromNumber, credentials.ToNumber, "This is only a test. Reply with text 'got it'", function(err, sms) { 63 | t.ifError(err); 64 | sms.on('sendStatus', function(success, status) { 65 | console.log("SMS Sent?", success, ":", status); 66 | t.ok(success); 67 | }); 68 | tapp.on('incomingSMSMessage', function(sms) { 69 | t.equal(sms.From, credentials.ToNumber); 70 | t.equal(sms.Body, "got it"); 71 | t.equal(sms.Status, "received"); 72 | t.done(); 73 | }); 74 | }); 75 | } 76 | else t.done(); 77 | } 78 | 79 | exports.stopServer = function(t) { 80 | http.close(); 81 | t.done(); 82 | } 83 | -------------------------------------------------------------------------------- /lib/Account.js: -------------------------------------------------------------------------------- 1 | var utilFuncs = require('./util'), 2 | ListIterator = require('./ListIterator'), 3 | Application = require('./Application'), 4 | AvailablePhoneNumber = require('./AvailablePhoneNumber'); 5 | 6 | function Account(client, Sid) { 7 | this._client = client; 8 | this.Sid = Sid; 9 | this._conferences = {}; 10 | } 11 | module.exports = Account; 12 | Account.mutableProps = ['FriendlyName', 'Status']; 13 | Account.prototype._getResourceURI = function(type) { 14 | return this._client._getResourceURI(type) + '/Accounts/' + this.Sid; 15 | } 16 | Account.prototype.load = utilFuncs.globalLoad; 17 | Account.prototype.save = utilFuncs.globalSave; 18 | Account.prototype.closeAccount = function(cb) { 19 | this.Status = 'closed'; 20 | this.save(cb); 21 | } 22 | Account.prototype.suspendAccount = function(cb) { 23 | this.Status = 'suspended'; 24 | this.save(cb); 25 | } 26 | Account.prototype.activateAccount = function(cb) { 27 | this.Status = 'active'; 28 | this.save(cb); 29 | } 30 | 31 | /** Query Twilio for available local numbers 32 | @param countryCode - Country code in ISO 3166-1 alpha-2 format 33 | @param filters - (optional) An object containing any of these properties: 34 | * AreaCode 35 | * Contains - A pattern to match phone numbers on. Valid characters are '*' 36 | and [0-9a-zA-Z]. The '*' character will match any single digit. 37 | 38 | ### These filters are limited to US and Canadian phone numbers: 39 | * InPostalCode - Limit results to a particular postal code. 40 | * InRegion - Works in US/Canada only. Filters by 2-character province/state code 41 | * NearLatLong - Given a latitude/longitude pair lat,long find geographically 42 | close numbers within Distance miles. 43 | * NearNumber - Given a phone number, find a geographically close number within 44 | Distance miles. 45 | * InLata - Limit results to a specific Local access and transport area (LATA). 46 | * InRateCenter - Limit results to a specific rate center, or given a phone number 47 | search within the same rate center as that number. Requires InLata to be set as well. 48 | * Distance - Specifies the search radius for a Near- query in miles. 49 | If not specified this defaults to 25 miles. 50 | @param callback - `cb(err, listIterator)` 51 | */ 52 | Account.prototype.listAvailableLocalNumbers = function(countryCode, filters, cb) { 53 | var func = utilFuncs.globalList("AvailablePhoneNumbers/" + countryCode + "/Local", 54 | AvailablePhoneNumber); 55 | return func.call(this, filters, cb); 56 | } 57 | 58 | /** Query Twilio for available toll-free numbers 59 | @param countryCode - Country code in ISO 3166-1 alpha-2 format 60 | @param filters - (optional) An object containing any of these properties: 61 | * AreaCode 62 | * Contains - A pattern to match phone numbers on. Valid characters are '*' 63 | and [0-9a-zA-Z]. The '*' character will match any single digit. 64 | @param callback - `cb(err, listIterator)` 65 | */ 66 | Account.prototype.listAvailableTollFreeNumbers = function(countryCode, filters, cb) { 67 | var func = utilFuncs.globalList("AvailablePhoneNumbers/" + countryCode + "/TollFree", 68 | AvailablePhoneNumber); 69 | return func.call(this, filters, cb); 70 | } 71 | 72 | 73 | Account.prototype.getApplication = utilFuncs.globalGet(Application); 74 | Account.prototype.listApplications = utilFuncs.globalList("Applications", Application); 75 | Account.prototype.createApplication = utilFuncs.globalCreate("Applications", Application, 76 | ['VoiceUrl', 'VoiceMethod', 'StatusCallback', 'StatusCallbackMethod', 'SmsUrl', 'SmsMethod', 77 | 'SmsStatusCallback'], ['FriendlyName']); 78 | 79 | Account.prototype.getRandomConferenceRoomName = function() { 80 | return "Rand_" + new Date().getTime() + ":" + Math.random(); //good enough for now 81 | } -------------------------------------------------------------------------------- /lib/util.js: -------------------------------------------------------------------------------- 1 | var ListIterator = require('./ListIterator'); 2 | exports.globalCreate = function(listResourceURI, objType, requiredFields, optionalFields) { 3 | if(requiredFields == null) requiredFields = []; 4 | if(optionalFields == null) optionalFields = []; 5 | return function() { 6 | var data = {}, cb; 7 | //Extract arguments 8 | for(var i = 0; i < arguments.length; i++) 9 | { 10 | if(typeof arguments[i] == "function") 11 | { 12 | cb = arguments[i]; 13 | i++; 14 | break; //Stop! We are done processing arguments now 15 | } 16 | else if(i < requiredFields.length) 17 | data[requiredFields[i]] = arguments[i]; 18 | //At this point, we've taken care of the required arguments. Now then... 19 | else if(typeof arguments[i] == "object") 20 | { 21 | /* If we have an object, just load the contents of the Object into `data`, 22 | assuming this argument is the list of optional fields */ 23 | for(var j in optionalFields) 24 | data[optionalFields[j]] = arguments[i][optionalFields[j]]; 25 | } 26 | else 27 | //Otherwise, just load optional fields one at a time into `data` until we hit `cb` 28 | data[optionalFields[i - requiredFields.length]] = arguments[i]; 29 | } 30 | if(i <= requiredFields.length) 31 | throw new Error("Insufficient number of arguments passed when creating " + objType.name); 32 | if(cb == undefined) throw new Error("No callback was provided when creating " + objType.name); 33 | //API call 34 | var cli = this._client, 35 | theCaller = this; 36 | cli._restAPI('POST', this._getResourceURI("create") + "/" + listResourceURI, data, function(err, res) { 37 | if(err) return cb(err); 38 | cb(null, new objType(theCaller, res.body.Sid).load(res.body) ); 39 | }); 40 | } 41 | } 42 | 43 | /** List the set of objects of type `objType`. 44 | @param filters - (optional) the filter Object that allows you to limit the 45 | list returned. 46 | @param cb - a callback of the form `cb(err, listIterator)` 47 | */ 48 | exports.globalList = function(listResourceURI, objType) { 49 | //`this` refers to this module - do not use 50 | return function(filters, cb) { 51 | //now `this` refers to the proper thing 52 | if(typeof filters == "function") 53 | { 54 | cb = filters; 55 | filters = undefined; 56 | } 57 | var li = new ListIterator(this, this._getResourceURI("list") + "/" + listResourceURI, objType, filters); 58 | li.load(cb); 59 | } 60 | } 61 | /** Gets a resource of type `objType` using a Sid 62 | @param Sid - the Twilio Sid of the resource 63 | @param cb - (optional) if set, the callback will be called when completed 64 | */ 65 | exports.globalGet = function(objType) { 66 | return function(Sid, cb) { 67 | var obj = new objType(this, Sid); 68 | if(cb) obj.load(cb); 69 | return obj; 70 | }; 71 | } 72 | /** Loads data from Twilio and updates this Object. 73 | @param data - (optional) if set, these data will replace this Object; otherwise, 74 | a RESTful call to the Twilio API will be made to update this Object. 75 | @param cb - (optional) if set, the callback will be called when completed 76 | @return - If data is set, the updated Object will be returned; otherwise, 77 | the stale object will be returned. 78 | */ 79 | exports.globalLoad = function(data, cb) { 80 | var obj = this, 81 | command = obj._getResourceURI("load"); 82 | if(typeof data == "function") 83 | { 84 | cb = data; 85 | data = undefined; 86 | } 87 | if(data) 88 | { 89 | for(var i in data) 90 | this[i] = data[i]; 91 | if(cb) cb(null, this); 92 | } 93 | else if(cb) 94 | { 95 | obj._client._restAPI('GET', command, function(err, res) { 96 | if(err) return cb(err); 97 | obj.load(res.body, cb); 98 | }); 99 | } 100 | return this; 101 | } 102 | /** Saves mutable object properties to Twilio 103 | @param cb - (optional) called when save completes `cb(err, obj)` 104 | */ 105 | exports.globalSave = function(cb) { 106 | var obj = this, 107 | command = obj._getResourceURI("save"), 108 | mut = obj.constructor.mutableProps, 109 | data = {}; 110 | for(var i in mut) 111 | if(obj[mut[i]]) 112 | data[mut[i]] = obj[mut[i]]; 113 | //Actually use POST here, even though PUT probably makes more sense for updating existing resources 114 | obj._client._restAPI('POST', command, data, function(err, res) { 115 | if(err) { 116 | if(cb) cb(err); 117 | return; 118 | } 119 | obj.load(res.body, cb); 120 | }); 121 | return this; 122 | } 123 | /** Deletes the Object in Twilio 124 | @param cb - (optional) called when the delete completes `cb(err, success)` 125 | */ 126 | exports.globalDelete = function(cb) { 127 | var obj = this, 128 | command = obj._getResourceURI("delete"); 129 | obj._client._restAPI('DELETE', command, function(err, res) { 130 | if(!err) obj._deleted = true; 131 | if(!cb) return; 132 | cb(err, err == null); 133 | }); 134 | } 135 | /** Recursively convert all properties of the specified `obj` from 136 | 'prop_like_this' to 'PropLikeThis' 137 | 138 | The "more better" JSON format isn't more better IMHO. 139 | http://www.twilio.com/docs/api/2010-04-01/changelog#more-better-json 140 | 141 | @param obj - object with property names separated by underscores 142 | @return obj with property names in CamelCase 143 | */ 144 | exports.underscoresToCamelCase = function underscoresToCamelCase(obj) { 145 | for(var i in obj) 146 | { 147 | if(typeof obj[i] == "object") 148 | underscoresToCamelCase(obj[i]); 149 | var newProp = i.split('_'); 150 | for(var j in newProp) 151 | newProp[j] = newProp[j].charAt(0).toUpperCase() + newProp[j].substr(1); 152 | obj[newProp.join('')] = obj[i]; 153 | if(!(obj instanceof Array) ) 154 | delete obj[i]; 155 | } 156 | } -------------------------------------------------------------------------------- /lib/Client.js: -------------------------------------------------------------------------------- 1 | const API_VERSION = '2010-04-01'; 2 | const API_HOST = 'api.twilio.com'; 3 | var debug = process.env.NODE_ENV == "development"; 4 | if(debug) debug = function() { 5 | process.stdout.write("\033[36m"); 6 | console.log.apply(this, arguments); 7 | process.stdout.write("\033[0m"); 8 | }; 9 | 10 | var https = require('https'), 11 | qs = require('querystring'), 12 | util = require('util'), 13 | EventEmitter = require('events').EventEmitter, 14 | Account = require('./Account'), 15 | utilFuncs = require('./util'); 16 | 17 | /** Constructs a new Twilio client. 18 | Your AccountSid and AuthToken are on the Twilio Account Dashboard page. 19 | Access the main Twilio Account using the `account` property on the Client. 20 | @param accountSID - your Twilio account ID 21 | @param authToken - secret authorization token 22 | */ 23 | function Client(AccountSid, AuthToken) { 24 | this._client = this; //This is a hack to make global utiltiy functions work 25 | this.AccountSid = AccountSid; 26 | this.AuthToken = AuthToken; 27 | this.account = new Account(this, AccountSid); 28 | this._getCache = {}; 29 | this._appMiddlewareSids = []; 30 | this._appMiddleware = {}; //indexed by Sid 31 | }; 32 | module.exports = Client; 33 | Client.prototype._getResourceURI = function(type) { 34 | return '/' + API_VERSION; 35 | } 36 | /** Returns an Account object on which you can perform Twilio API calls. If `cb` 37 | is set, then the details for this Account are also retrieved. 38 | @param sid - the Account Sid 39 | @param cb - (optional) the callback to be called once the subaccount details 40 | have been loaded. `cb` should be of the form: `cb(err, subaccount)` 41 | @return an Account object (before details have been retrieved) 42 | */ 43 | Client.prototype.getAccount = utilFuncs.globalGet(Account); 44 | /** Creates a subaccount and returns the Account object to your callback function. 45 | @param friendlyName - (optional) the name of the subaccount to be created 46 | @param cb - the callback to be called once the subaccount has been created. 47 | `cb` should be of the form: `cb(err, subaccount)` 48 | */ 49 | Client.prototype.createSubAccount = utilFuncs.globalCreate('Accounts', Account, 50 | [], ['FriendlyName']); 51 | 52 | /** Filters may include 'FriendlyName' or 'Status'. 53 | */ 54 | Client.prototype.listAccounts = utilFuncs.globalList('Accounts', Account); 55 | 56 | /** Returns Express middleware to direct incoming Twilio requests to the 57 | appropriate Application instance. 58 | @return an Express-style middleware function of the form: `function(req, res, next)` 59 | */ 60 | Client.prototype.middleware = function() { 61 | var cli = this; 62 | return function(req, res, next) { 63 | var keys = cli._appMiddlewareSids; 64 | var i = 0; 65 | (function nextMiddleware(err) { 66 | if(err) return next(err); //abort! 67 | if(i < keys.length) 68 | cli._appMiddleware[keys[i++]](req, res, nextMiddleware); 69 | else 70 | next(); 71 | })(); 72 | } 73 | }; 74 | /** Main REST API function. 75 | @param method - a string indicating which HTTP verb to use (i.e. GET, POST, etc.) 76 | @param command - the REST command to be sent (part of the URL path) 77 | @param data - (optional) an Object to be sent. In a POST, PUT, or DELETE request, this 78 | string is urlencoded and appended to the HTTP request body. In other 79 | requests (i.e. GET), the string is appended to the URL. 80 | @param cb - (optional) a callback executed when Twilio's server responds to the HTTP request. 81 | cb should be of the form: `cb(err, res)` where `err` is an Error object and `res` is 82 | the 83 | "Weak" HTTP Caching is implemented for GET requests, but its performance is rather poor. 84 | Also Note: This function prefers to parse JSON responses, not XML responses; however, 85 | this function maps the JSON property names (lowercase and separate by underscores) 86 | up with the XML tag names (in CamelCase). Rather silly that Twilio made their JSON 87 | responses that way, but... no worries. :) 88 | */ 89 | Client.prototype._restAPI = function(method, command, data, cb) { 90 | var cli = this; 91 | //optional args 92 | if(typeof data == "function") { 93 | cb = data; 94 | data = undefined; 95 | } 96 | //TODO: Make caching a bit better than this... 97 | if(Math.random() > 0.995) 98 | cli._getCache = {}; //clear cache 99 | //Build command, headers, and method type 100 | command += ".json"; 101 | if(data == null) 102 | data = ''; 103 | else 104 | data = qs.stringify(data); 105 | var headers = {}; 106 | if(method == 'POST' || method == 'PUT' || method == 'DELETE') 107 | { 108 | headers['content-type'] = 'application/x-www-form-urlencoded'; 109 | headers['content-length'] = data.length; 110 | } 111 | else 112 | { 113 | method = 'GET'; 114 | if(data != '') 115 | command += '?' + data; 116 | if(cli._getCache[command] != undefined) 117 | headers['if-modified-since'] = new Date( 118 | cli._getCache[command].headers['last-modified']).toUTCString(); 119 | } 120 | //Make HTTPS request 121 | var req = https.request({ 122 | 'host': API_HOST, 123 | 'port': 443, 124 | 'method': method, 125 | 'path': command, 126 | 'headers': headers, 127 | 'auth': cli.AccountSid + ':' + cli.AuthToken 128 | }, function(res) { 129 | if(!cb) return; 130 | var resBody = ''; 131 | res.on('data', function(chunk) { 132 | resBody += chunk; 133 | }); 134 | res.on('end', function() { 135 | if(debug) 136 | { 137 | var color = getColor(res.statusCode); 138 | debug("REST API Request:", method, req.path, method == 'POST' ? data : '', 139 | "-\033[" + color + "m", res.statusCode + "\033[90m"); 140 | } 141 | //Weak caching 142 | if(res.statusCode == 304) 143 | cb(null, cli._getCache[command]); 144 | //Error handling 145 | else if(res.statusCode >= 400) 146 | { 147 | if(debug) debug("\033[" + color + "m\t\t Response Body:", resBody + "\033[90m"); 148 | cb(new Error("An error occurred for command: " + method + " " + command + 149 | "\n\t" + API_HOST + " responded with status code " + res.statusCode), res); 150 | } 151 | else 152 | { 153 | //Weak caching 154 | if(method == 'GET' && res.headers['last-modified']) 155 | cli._getCache[command] = res; 156 | try { 157 | if(resBody == '') 158 | res.body = {}; 159 | else 160 | res.body = JSON.parse(resBody); //may throw exception 161 | //JSON responses look different than XML responses... stupid 162 | utilFuncs.underscoresToCamelCase(res.body); 163 | } catch(e) {return cb(e);} 164 | cb(null, res); 165 | } 166 | }); 167 | }); 168 | req.on('error', cb); 169 | //Send POST data 170 | if(method == 'POST' || method == 'PUT') 171 | req.end(data); 172 | else 173 | req.end(); 174 | }; 175 | function getColor(statusCode) { 176 | var color = 32; 177 | if (statusCode >= 500) color = 31; 178 | else if (statusCode >= 400) color = 33; 179 | else if (statusCode >= 300) color = 36; 180 | return color; 181 | } 182 | -------------------------------------------------------------------------------- /test/server.js: -------------------------------------------------------------------------------- 1 | var twilio = require('../lib'), 2 | basicTest = require('./basics'), 3 | client, 4 | express = require('express'), 5 | app = express.createServer(), 6 | http, 7 | tapp; 8 | 9 | exports.getTwilioCredentials = basicTest.getTwilioCredentials; 10 | exports.constructClient = function() { 11 | client = basicTest.constructClient.apply(this, arguments); 12 | } 13 | 14 | exports.setupExpressMiddleware = function(t) { 15 | t.expect(2); 16 | t.equal(typeof client.middleware, "function"); 17 | t.equal(typeof express.errorHandler, "function"); 18 | if(process.env.NODE_ENV == "development") 19 | app.use(function(req, res, next) { 20 | console.log("\033[46m\033[30m" + "Incoming request: " + req.method + " " + req.url + "\033[0m"); 21 | next(); 22 | }); 23 | app.use(client.middleware() ); 24 | app.use(express.errorHandler({ 25 | 'showMessage': true, 26 | 'dumpExceptions': true 27 | }) ); 28 | http = app.listen(8002); 29 | t.done(); 30 | } 31 | 32 | exports.loadApplication = function(t) { 33 | t.expect(2); 34 | client.account.getApplication(client.credentials.ApplicationSid, function(err, app) { 35 | t.ifError(err); 36 | t.notEqual(app, null, "Application is null or undefined"); 37 | tapp = app; 38 | t.done(); 39 | }); 40 | } 41 | 42 | exports.registerApplication = function(t) { 43 | t.expect(6); 44 | t.equal(client._appMiddlewareSids.length, 0); 45 | t.equal(Object.keys(client._appMiddleware).length, 0); 46 | tapp.register(); 47 | t.equal(client._appMiddlewareSids.length, 1); 48 | t.equal(client._appMiddlewareSids[0], tapp.Sid); 49 | t.equal(Object.keys(client._appMiddleware).length, 1); 50 | t.equal(typeof client._appMiddleware[tapp.Sid], "function"); 51 | t.done(); 52 | } 53 | 54 | /* Place a call from caller ID credentials.FromNumber to credentials.ToNumber. 55 | Callee must pick up the phone and press a key for this test to be successful. */ 56 | exports.makeCall = function(t) { 57 | var credentials = client.credentials; 58 | if(credentials.FromNumber && credentials.ToNumber) 59 | { 60 | t.expect(4); 61 | console.log("Placing call to " + credentials.ToNumber); 62 | tapp.makeCall(credentials.FromNumber, credentials.ToNumber, { 63 | 'timeout': 12 64 | }, function(err, call) { 65 | t.ifError(err); 66 | if(!err && call) 67 | { 68 | call.on('connected', function(status) { 69 | t.equal(status, 'in-progress', "connected event did not show in-progress status"); 70 | console.log(new Date().toUTCString() + ": Call " + call.Sid + 71 | " has been connected: " + status); 72 | }); 73 | call.say("Hello. This is a test of the Twilio API."); 74 | call.pause(); 75 | var input = call.gather(function(call, digits) { 76 | t.ok(digits != '', "Caller did not press any key"); 77 | call.say("You pressed " + digits + "."); 78 | var str = "Congratulations! You just used Node Twilio API to place an " + 79 | "outgoing call."; 80 | call.say(str, {'voice': 'man', 'language': 'en'}); 81 | call.pause(); 82 | var loop = 0; 83 | (function getInputLoop(call) { 84 | input = call.gather(function(call, digits) { 85 | if(digits.length == 10) 86 | { 87 | call.say("OK. I'm calling " + digits + 88 | ". Please wait. Press * at any time to end this call."); 89 | var roomName = call.joinConference({ 90 | 'leaveOnStar': true, 91 | 'timeLimit': 120, 92 | 'endConferenceOnExit': true 93 | }); 94 | call.say("The call has ended."); 95 | //Now call the other person 96 | tapp.makeCall(credentials.ToNumber, digits, { 97 | 'timeout': 12 98 | }, function(err, call2) 99 | { 100 | function errorFunc(message) { 101 | return function(err, call) { 102 | if(err) t.ifError(err); 103 | else 104 | { 105 | call.say(message); 106 | call.say("Goodbye."); 107 | } 108 | }; 109 | }; 110 | if(err) call.liveCb(errorFunc("There was an error placing the call.") ); 111 | call2.on('connected', function(status) { 112 | console.log("Call 2 connected:", status); 113 | if(status != 'in-progress') 114 | call.liveCb(errorFunc("Something weird happened. Sorry.") ); 115 | else 116 | { 117 | call2.say("Hello. Please wait while I connect you to " + 118 | "your party."); 119 | call2.joinConference(roomName, { 120 | 'endConferenceOnExit': true 121 | }); 122 | call2.say("The call has ended. Thank you for participating " + 123 | "in the test. Goodbye."); 124 | } 125 | }); 126 | call2.on('ended', function(status, duration) { 127 | console.log("Call 2 ended:", status, duration); 128 | switch(status) 129 | { 130 | case 'no-answer': 131 | call.liveCb(errorFunc("The caller did not answer.") ); 132 | break; 133 | case 'busy': 134 | call.liveCb(errorFunc("The caller's line was busy.") ); 135 | break; 136 | case 'failed': 137 | call.liveCb(errorFunc("There was an error placing the call.") ); 138 | break; 139 | case 'completed': 140 | break; 141 | default: 142 | call.liveCb(errorFunc("Something weird happened. Call status was " + status) ); 143 | break; 144 | } 145 | }); 146 | }); 147 | } 148 | else 149 | call.say("You entered " + digits.length + 150 | " digits, so I won't run the next test."); 151 | call.say("Goodbye!"); 152 | }, { 153 | 'timeout': 10, 154 | 'finishOnKey': '#' 155 | }); 156 | input.say("If you want to test calling another person, " + 157 | "please enter their telephone number followed by #. " + 158 | "Otherwise, you may hang up or simply press # to disconnect."); 159 | call.say("Sorry. I didn't hear your response."); 160 | //Only prompt for input 3 times, then just give up and hangup 161 | if(++loop <= 3) 162 | call.cb(getInputLoop); 163 | else 164 | call.say("Goodbye!"); 165 | })(call); 166 | }, { 167 | 'timeout': 10, 168 | 'numDigits': 1 169 | }); 170 | input.say("Please press any key to continue. " + 171 | "You may press 1, 2, 3, 4, 5, 6, 7, 8, 9, or 0."); 172 | call.say("I'm sorry. I did not hear your response. The test will fail. Goodbye!"); 173 | call.on('ended', function(status, duration) { 174 | t.equal(status, 'completed', 'Call status was not completed in ended event'); 175 | console.log(new Date().toUTCString() + ": Call " + call.Sid + " has ended: " 176 | + status + ":" + duration + " seconds"); 177 | t.done(); 178 | }); 179 | } 180 | }); 181 | } 182 | else t.done(); 183 | } 184 | 185 | exports.stopServer = function(t) { 186 | http.close(); 187 | t.done(); 188 | } 189 | -------------------------------------------------------------------------------- /test/basics.js: -------------------------------------------------------------------------------- 1 | var twilio = require('../lib'), 2 | credentials, 3 | client, 4 | Application = require('../lib/Application'), 5 | Call = require('../lib/Call'); 6 | 7 | exports.getTwilioCredentials = function(t) { 8 | t.expect(3); 9 | try { 10 | credentials = require('./credentials'); 11 | t.equal(typeof credentials.AccountSid, "string", "Credentials object missing 'AccountSid' property"); 12 | t.equal(typeof credentials.AuthToken, "string", "Credentials object missing 'AuthToken' property"); 13 | t.equal(typeof credentials.ApplicationSid, "string", "Credentials object missing 'ApplicationSid' property"); 14 | t.done(); 15 | } 16 | catch(e) { 17 | console.log("Twilio Credentials not found"); 18 | console.log("To prevent this prompt, create a 'credentials.js' file that exports\n" + 19 | " your AccountSid and AuthToken."); 20 | var readline = require('readline'); 21 | var input = readline.createInterface(process.stdin, process.stdout, null); 22 | input.question("Please enter your Twilio Account Sid: ", function(accountSid) { 23 | input.question("Please enter your Twilio Auth Token: ", function(authToken) { 24 | console.log("Please be certain that your Twilio Application is pointing to this server. The test suite server will listen on port 8002."); 25 | input.question("Please enter a valid Twilio Application Sid for this account: ", 26 | function(appSid) { 27 | input.pause(); 28 | process.stdin.pause(); 29 | credentials = {'AccountSid': accountSid, 'AuthToken': authToken, 'ApplicationSid': appSid}; 30 | t.equal(typeof credentials.AccountSid, "string", "Credentials object missing 'AccountSid' property"); 31 | t.equal(typeof credentials.AuthToken, "string", "Credentials object missing 'AuthToken' property"); 32 | t.equal(typeof credentials.ApplicationSid, "string", "Credentials object missing 'ApplicationSid' property"); 33 | t.done(); 34 | }); 35 | }); 36 | }); 37 | } 38 | } 39 | exports.constructClient = function(t) { 40 | t.expect(2); 41 | client = new twilio.Client(credentials.AccountSid, credentials.AuthToken); 42 | client.credentials = credentials; //Expose this object for the sake of other tests 43 | t.ok(client.AccountSid == credentials.AccountSid, "Account Sid does not match credentials"); 44 | t.ok(client.account.Sid == credentials.AccountSid, "account.Sid does not match credentials"); 45 | t.done(); 46 | return client; 47 | } 48 | 49 | exports.listAvailableLocalNumbers = function(t) { 50 | t.expect(5); 51 | client.account.listAvailableLocalNumbers('US', { 52 | 'AreaCode': 614 //Woot! C-bus! Represent, yo! 53 | }, function(err, li) { 54 | t.ifError(err); 55 | if(li) 56 | { 57 | t.ok(li.AvailablePhoneNumbers instanceof Array, "Not an array"); 58 | t.ok(li.AvailablePhoneNumbers.length > 0, "Hmm... no numbers in Columbus?"); 59 | if(li.AvailablePhoneNumbers.length > 0) 60 | { 61 | t.ok(li.AvailablePhoneNumbers[0].Region == 'OH', "Not in Ohio?"); 62 | t.ok(li.AvailablePhoneNumbers[0].IsoCountry == 'US', "Not in US? Say what?"); 63 | t.done(); 64 | } 65 | } 66 | }); 67 | } 68 | 69 | exports.listAvailableTollFreeNumbers = function(t) { 70 | t.expect(5); 71 | client.account.listAvailableTollFreeNumbers('US', { 72 | 'AreaCode': 866 73 | }, function(err, li) { 74 | t.ifError(err); 75 | if(li) 76 | { 77 | t.ok(li.AvailablePhoneNumbers instanceof Array, "Not an array"); 78 | t.ok(li.AvailablePhoneNumbers.length > 0, "Hmm... toll free numbers?"); 79 | if(li.AvailablePhoneNumbers.length > 0) 80 | { 81 | t.ok(li.AvailablePhoneNumbers[0].PhoneNumber.substr(0, 5) == '+1866', "Does not match filter"); 82 | t.ok(li.AvailablePhoneNumbers[0].IsoCountry == 'US', "Not in US? Say what?"); 83 | } 84 | } 85 | t.done(); 86 | }); 87 | } 88 | 89 | var appName = "Testing 2789278973974982738478"; 90 | exports.createApplicationFail = function(t) { 91 | t.expect(1); 92 | t.throws(function() { 93 | client.account.createApplication(1, 2, 3, 4, 5, function() {}, 6, 7, 8, 9, 10, function() {}); 94 | }); 95 | t.done(); 96 | } 97 | 98 | var createdApp; 99 | const URL_PREFIX = 'https://www.example.com/twilio/'; 100 | exports.createApplication = function(t) { 101 | t.expect(4); 102 | client.account.createApplication(URL_PREFIX + 'voice', 'POST', URL_PREFIX + 'voiceStatus', 'POST', 103 | URL_PREFIX + 'sms', 'POST', URL_PREFIX + 'smsStatus', appName, function(err, app) { 104 | t.ifError(err); 105 | if(app) 106 | { 107 | t.ok(app instanceof Application, "Not an Application"); 108 | t.ok(app.FriendlyName == appName, "FriendlyName does not match"); 109 | t.ok(app.AccountSid == client.account.Sid, "Account Sid does not match"); 110 | createdApp = app; 111 | } 112 | t.done(); 113 | }); 114 | } 115 | 116 | exports.listApplications = function(t) { 117 | t.expect(5); 118 | client.account.listApplications(function(err, li) { 119 | t.ifError(err); 120 | if(li) 121 | { 122 | t.ok(li.Applications instanceof Array, "Not an array"); 123 | t.ok(li.Applications.length > 0, "Hmm... no applications?"); 124 | t.ok(li.Results instanceof Array, "Should also have property Results"); 125 | t.ok(li.Results === li.Applications, "Should be the same Object"); 126 | } 127 | t.done(); 128 | }); 129 | } 130 | 131 | exports.getApplication = function(t) { 132 | t.expect(4); 133 | client.account.getApplication(createdApp.Sid, function(err, app) { 134 | t.ifError(err); 135 | t.ok(app instanceof Application, "Not instanceof Application"); 136 | t.ok(app != createdApp, "Ensure it's a different instance"); 137 | t.ok(app.FriendlyName == appName, "FriendlyName does not match"); 138 | t.done(); 139 | }); 140 | } 141 | 142 | exports.testApplicationRegistration = function(t) { 143 | t.expect(14); 144 | var app = createdApp; 145 | t.equal(client._appMiddlewareSids.length, 0); 146 | t.equal(Object.keys(client._appMiddleware).length, 0); 147 | app.register(); 148 | t.equal(client._appMiddlewareSids.length, 1); 149 | t.equal(client._appMiddlewareSids[0], app.Sid); 150 | t.equal(Object.keys(client._appMiddleware).length, 1); 151 | t.equal(typeof client._appMiddleware[app.Sid], "function"); 152 | app.register(); 153 | t.equal(client._appMiddlewareSids.length, 1); 154 | t.equal(client._appMiddlewareSids[0], app.Sid); 155 | t.equal(Object.keys(client._appMiddleware).length, 1); 156 | t.equal(typeof client._appMiddleware[app.Sid], "function"); 157 | app.unregister(); 158 | t.equal(client._appMiddlewareSids.length, 0); 159 | t.equal(Object.keys(client._appMiddleware).length, 0); 160 | app.unregister(); 161 | t.equal(client._appMiddlewareSids.length, 0); 162 | t.equal(Object.keys(client._appMiddleware).length, 0); 163 | t.done(); 164 | } 165 | 166 | exports.listCalls = function(t) { 167 | t.expect(4); 168 | createdApp.listCalls(function(err, li) { 169 | t.ifError(err); 170 | if(li) 171 | { 172 | t.ok(li.Calls instanceof Array, "List is not an array?"); 173 | t.ok(li.Calls.length > 0, "List is empty"); 174 | t.ok(li.Calls[0] instanceof Call, "List item is not a Call object"); 175 | } 176 | t.done(); 177 | }); 178 | } 179 | 180 | exports.removeApplication = function(t) { 181 | t.expect(3); 182 | createdApp.remove(function(err, success) { 183 | t.ifError(err); 184 | t.ok(success, "Delete failed"); 185 | client.account.getApplication(createdApp.Sid, function(err, app) { 186 | t.ok(err != null && app == null, "Should be an error"); 187 | t.done(); 188 | }); 189 | }); 190 | } 191 | -------------------------------------------------------------------------------- /lib/Call.js: -------------------------------------------------------------------------------- 1 | var util = require('util'), 2 | EventEmitter = require('events').EventEmitter, 3 | utilFuncs = require('./util'), 4 | Tag = require('./Tag'); 5 | function Call(app, Sid) { 6 | this._app = app; 7 | this._parent = app._account; 8 | this._client = app._client; 9 | this.twiml = new Tag('Response'); 10 | this._cb = {}; //Indexed by callback ID 11 | this._cbID = 0; 12 | EventEmitter.call(this); //Make `this` a new EventEmitter 13 | }; 14 | Call.mutableProps = ['Url', 'Method', 'Status']; 15 | util.inherits(Call, EventEmitter); //Inherit all EventEmitter prototype methods 16 | module.exports = Call; 17 | 18 | Call.prototype._getResourceURI = function(type) { 19 | return this._parent._getResourceURI(type) + '/Calls/' + this.Sid; 20 | } 21 | Call.prototype.load = utilFuncs.globalLoad; 22 | Call.prototype.save = utilFuncs.globalSave; 23 | Call.prototype.liveCancel = function(cb) { 24 | this.Status = 'canceled'; 25 | this.save(cb); 26 | } 27 | Call.prototype.liveHangUp = function(cb) { 28 | this.Status = 'completed'; 29 | this.save(cb); 30 | } 31 | Call.prototype.liveRedirect = function(url, method, cb) { 32 | this.Url = url; 33 | this.Method = method; 34 | delete this.Status; 35 | this.save(cb); 36 | delete this.Url; 37 | delete this.Method; 38 | } 39 | Call.prototype.liveCb = function(cb) { 40 | this._cb[this._cbID] = cb; 41 | this.liveRedirect(this._app.VoiceUrl + "?cbtype=liveCb&cb=" + encodeURIComponent(this._cbID), 42 | this._app.VoiceMethod, function(err, obj) { 43 | if(err) cb(err); 44 | }); 45 | this._cbID++; 46 | } 47 | 48 | function Gather() { 49 | this.twiml = new Tag("Gather"); 50 | }; 51 | 52 | //-- Call prototype 53 | Call.prototype._handle = function(res) { 54 | res.statusCode = 200; 55 | res.setHeader('content-type', 'application/xml'); 56 | var twiml = this.twiml.toString(); 57 | res.setHeader('content-length', twiml.length); 58 | res.end(twiml); 59 | this.twiml = new Tag('Response'); //Reset TwiML for next _handle call 60 | if(this._cbID >= 2147483647) this._cbID = 0; //Just in case... :) 61 | }; 62 | /* Uses the TwiML Verb to redirect call flow to your callback 63 | Useful espeically following a Verb that might fall-through due to no input 64 | */ 65 | Call.prototype.cb = function(cb) { 66 | this._cb[this._cbID] = cb; 67 | this.redirect(this._app.VoiceUrl + "?cbtype=cb&cb=" + 68 | encodeURIComponent(this._cbID), this._app.VoiceMethod); 69 | this._cbID++; 70 | } 71 | /* TwiML Verb */ 72 | function _say(rootTag, text, options) { 73 | if(options == null) options = {}; 74 | if(text.length > 4000) 75 | throw new Error("You cannot say more than 4000 characters of text. This is a Twilio limitation."); 76 | var say = new Tag("Say"); 77 | say.append(text) 78 | .setAttribute('voice', options.voice) 79 | .setAttribute('language', options.language) 80 | .setAttribute('loop', options.loop); 81 | rootTag.append(say); 82 | } 83 | /*TwiML Verb */ 84 | function _play(rootTag, audioUrl, options) { 85 | if(options == null) options = {}; 86 | var play = new Tag("Play"); 87 | play.append(audioUrl) 88 | .setAttribute('loop', options.loop); 89 | rootTag.append(play); 90 | } 91 | /* TwiML Verb */ 92 | function _pause(rootTag, pauseDuration) { 93 | var pause = new Tag("Pause"); 94 | pause.setAttribute('length', pauseDuration); 95 | rootTag.append(pause); 96 | } 97 | Call.prototype.say = function(text, options) { 98 | _say(this.twiml, text, options); 99 | } 100 | Call.prototype.play = function(audioUrl, options) { 101 | _play(this.twiml, audioUrl, options); 102 | } 103 | Call.prototype.pause = function(pauseDuration) { 104 | _pause(this.twiml, pauseDuration); 105 | } 106 | /* TwiML Verb 107 | Gathers input from the telephone user's keypad. 108 | Calls `cbIfInput` if input is provided. 109 | Options include: 110 | -timeout 111 | -finishOnKey 112 | -numDigits 113 | If no input was provided by the user, a couple of things may happen: 114 | -If cbIfNoInput was set, call `cbIfNoInput`; 115 | -Otherwise, proceed to the next TwiML instruction 116 | */ 117 | Call.prototype.gather = function(cbIfInput, options, cbIfNoInput) { 118 | if(typeof options == "function") { 119 | cbIfNoInput = options; 120 | options = null; 121 | } 122 | if(options == null) options = {}; 123 | this._cb[this._cbID] = cbIfInput; 124 | var gather = new Gather(); 125 | gather.twiml 126 | .setAttribute('action', this._app.VoiceUrl + "?cbtype=gather&cb=" + 127 | encodeURIComponent(this._cbID) ) 128 | .setAttribute('method', this._app.VoiceMethod) 129 | .setAttribute('timeout', options.timeout) 130 | .setAttribute('finishOnKey', options.finishOnKey) 131 | .setAttribute('numDigits', options.numDigits); 132 | this.twiml.append(gather.twiml); 133 | this._cbID++; 134 | if(typeof cbIfNoInput == "function") 135 | this.cb(cbIfNoInput); 136 | return gather; 137 | } 138 | /* TODO: TwiML Verb */ 139 | Call.prototype.record = function(cb, options, cbIfEmptyRecording) { 140 | if(typeof options == "function") { 141 | cbIfEmptyRecording = options; 142 | options = null; 143 | } 144 | if(options == null) options = {}; 145 | if(options.transcribe === false) 146 | options.transcribeCallback = null; 147 | else if(typeof options.transcribeCallback == "function") 148 | options.transcribe = true; 149 | this._cb[this._cbID] = cb; 150 | var record = new Tag("Record"); 151 | record 152 | .setAttribute('action', this._app.VoiceUrl + "?cbtype=record&cb=" + 153 | encodeURIComponent(this._cbID) ) 154 | .setAttribute('method', this._app.VoiceMethod) 155 | .setAttribute('timeout', options.timeout) 156 | .setAttribute('maxLength', options.maxLength) 157 | .setAttribute('transcribe', options.transcribe) 158 | .setAttribute('playBeep', options.playBeep); 159 | this._cbID++; 160 | if(typeof options.transcribeCallback == "function") 161 | { 162 | this._cb[this._cbID] = options.transcribeCallback; 163 | record.setAttribute('transcribeCallback', this._app.VoiceUrl + "?cbtype=transcribe&cb=" + 164 | encodeURIComponent(this._cbID) ); 165 | this._cbID++; 166 | } 167 | this.twiml.append(record); 168 | if(typeof cbIfEmptyRecording == "function") 169 | this.cb(cbIfEmptyRecording); 170 | } 171 | /* TwiML Verb */ 172 | //Not implemented 173 | /* TwiML Verb 174 | Dials the specified callees and calls cbAfterDial when the callee hangs up or dial fails. 175 | `callees` can be: 176 | -Phone number - a string in E.164 format 177 | -Phone number - object with properties: 178 | -number - in E.164 format 179 | -sendDigits 180 | -CAUTION: The `url` option is not implemented, and will likely not be implemented. 181 | You can achieve this functionality by briding calls using conferences. 182 | See call.bridge(...) 183 | -Twilio Client ID - an object with properties: 184 | -client - the Twilio Client name 185 | -Conference - an object with properties: 186 | -name 187 | -muted 188 | -beep 189 | -startConferenceOnEnter 190 | -startConferenceOnExit 191 | -waitUrl - waitUrl and waitMethod may point to an audio file to or a TwiML document 192 | that uses or for content 193 | -waitMethod - be sure to use GET if requesting audio files (so caching works) 194 | -maxParticipants 195 | -An array of any of these 196 | `options` include: 197 | -timeout - How long to wait for callee to answer (defaults to 30 seconds) 198 | -hangupOnStar - (defaults to false) 199 | -timeLimit - maximum duration of the call (defaults to 4 hours) 200 | -callerID (a valid phone number or client identifier, if calling a Twilio Client only) 201 | If you specify `cbAfterDial`, it will be called when the dialed user 202 | */ 203 | /*Call.prototype.dial = function(callees, options, cbAfterDial) { 204 | if(typeof options == "function") { 205 | cbAfterDial = options; 206 | options = null; 207 | } 208 | if(options == null) options = {}; 209 | this._cb[this._cbID] = cbAfterDial; 210 | var dial = new Tag("Dial"); 211 | dial 212 | .setAttribute('action', this._app.VoiceUrl + "?cbtype=dial&cb=" + 213 | encodeURIComponent(this._cbID) ) 214 | .setAttribute('method', this._app.VoiceMethod) 215 | .setAttribute('timeout', options.timeout) 216 | .setAttribute('hangupOnStar', options.hangupOnStar) 217 | .setAttribute('timeLimit', options.timeLimit) 218 | .setAttribute('callerId', options.callerId); 219 | if(!(callees instanceof Array) ) 220 | callees = [callees]; 221 | var noun; 222 | for(var i in callees) 223 | { 224 | if(typeof callees[i] == "object") 225 | { 226 | if(callees[i].number) 227 | { 228 | noun = new Tag("Number"); 229 | noun.append(callees[i].number) 230 | .setAttribute("sendDigits", callees[i].sendDigits); 231 | dial.append(noun); 232 | } 233 | else if(callees[i].client) 234 | { 235 | noun = new Tag("Client"); 236 | client.append(callees[i].client); 237 | dial.append(noun); 238 | } 239 | else if(callees[i].name) 240 | { 241 | //Assume this is a conference 242 | noun = new Tag("Conference"); 243 | noun.append(callees[i].name) 244 | .setAttribute("muted", callees[i].muted) 245 | .setAttribute("beep", callees[i].beep) 246 | .setAttribute("startConferenceOnEnter", callees[i].startConferenceOnEnter) 247 | .setAttribute("endConferenceOnExit", callees[i].endConferenceOnExit) 248 | .setAttribute("waitUrl", callees[i].waitUrl) 249 | .setAttribute("waitMethod", callees[i].waitMethod) 250 | .setAttribute("maxParticipants", callees[i].maxParticipants); 251 | dial.append(noun); 252 | } 253 | } 254 | else if(typeof callees[i] == "string") 255 | { 256 | noun = new Tag("Number"); 257 | noun.append(callees[i]); 258 | dial.append(noun); 259 | } 260 | } 261 | this.twiml.append(dial); 262 | this.twiml._done = true; //No more TwiML should be added 263 | this._cbID++; 264 | }*/ 265 | Call.prototype.joinConference = function(roomName, options, cbOnEnd) { 266 | if(typeof roomName != "string") 267 | { 268 | cbOnEnd = options; 269 | options = roomName; 270 | roomName = this._parent.getRandomConferenceRoomName(); 271 | } 272 | if(typeof options == "function") 273 | { 274 | cbOnEnd = options; 275 | options = null; 276 | } 277 | if(options == null) options = {}; 278 | var dial = new Tag("Dial"); 279 | dial 280 | .setAttribute('hangupOnStar', options.leaveOnStar) 281 | .setAttribute('timeLimit', options.timeLimit); 282 | var conf = new Tag("Conference"); 283 | conf 284 | .append(roomName) 285 | .setAttribute('muted', options.muted) 286 | .setAttribute('beep', options.beep) 287 | .setAttribute('startConferenceOnEnter', options.startConferenceOnEnter) 288 | .setAttribute('endConferenceOnExit', options.endConferenceOnExit) 289 | .setAttribute('waitUrl', options.waitUrl) 290 | .setAttribute('waitMethod', options.waitMethod) 291 | .setAttribute('maxParticipants', options.maxParticipants); 292 | dial.append(conf); 293 | this.twiml.append(dial); 294 | if(typeof cbOnEnd == "function") 295 | { 296 | this._cb[this._cbID] = cbOnEnd; 297 | dial.setAttribute('action', this._app.VoiceUrl + "?cbtype=dial&cb=" + 298 | encodeURIComponent(this._cbID) ) 299 | .setAttribute('method', this._app.VoiceMethod); 300 | this._cbID++; 301 | this.twiml._done = true; 302 | } 303 | return roomName; 304 | } 305 | /* Special case of joinConference() */ 306 | Call.prototype.putOnHold = function(options) { 307 | return this.joinConference({ 308 | 'timeLimit': options.timeLimit, 309 | 'beep': options.beep || false, 310 | 'waitUrl': options.waitUrl, 311 | 'waitMethod': options.waitMethod 312 | }); 313 | } 314 | /* Special case of joinConference() */ 315 | Call.prototype.putOnHoldWithoutMusic = function(options) { 316 | return this.joinConference({ 317 | 'timeLimit': options.timeLimit, 318 | 'beep': options.beep || false, 319 | 'waitUrl': '' 320 | }); 321 | } 322 | /* TwiML Verb */ 323 | Call.prototype.hangup = function() { 324 | this.twiml.append(new Tag("Hangup") ); 325 | this.twiml._done = true; //No more TwiML should be added 326 | } 327 | /* TwiML Verb 328 | Useful for redirecting calls to another Twilio application 329 | */ 330 | Call.prototype.redirect = function(url, method) { 331 | 332 | var redirect = new Tag("Redirect"); 333 | redirect.append(url) 334 | .setAttribute('method', method); 335 | this.twiml.append(redirect); 336 | this.twiml._done = true; //No more TwiML should be added 337 | } 338 | /* TwiML Verb 339 | Rejects a call without incurring any fees. 340 | This MUST be the first item in your TwiML and is only valid for 341 | incoming calls. 342 | */ 343 | Call.prototype.reject = function(reason) { 344 | if(this.twiml.content != undefined) 345 | throw new Error("The reject instruction must be the first instruction."); 346 | var reject = new Tag("Reject"); 347 | if(reason == "rejected" || reason == "busy") 348 | reject.setAttribute('reason', reason); 349 | this.twiml.append(reject); 350 | this.twiml._done = true; //No more TwiML should be added 351 | } 352 | 353 | //-- Gather prototype 354 | Gather.prototype.say = function(text, options) { 355 | _say(this.twiml, text, options); 356 | } 357 | Gather.prototype.play = function(audioUrl, options) { 358 | _play(this.twiml, audioUrl, options); 359 | } 360 | Gather.prototype.pause = function(pauseDuration) { 361 | _pause(this.twiml, pauseDuration); 362 | } 363 | -------------------------------------------------------------------------------- /lib/Application.js: -------------------------------------------------------------------------------- 1 | var url = require('url'), 2 | qs = require('querystring'), 3 | crypto = require('crypto'), 4 | tls = require('tls'), 5 | util = require('util'), 6 | utilFuncs = require('./util'), 7 | EventEmitter = require('events').EventEmitter, 8 | Call = require('./Call'), 9 | SMSMessage = require('./SMSMessage'), 10 | Tag = require('./Tag'); 11 | function Application(account, Sid) { 12 | this._account = account; 13 | this._client = account._client; 14 | this.Sid = Sid; 15 | this._pendingCalls = {}; //indexed by sid 16 | this._pendingSMSMessages = {}; //indexed by sid 17 | this._nextConf = 0; 18 | EventEmitter.call(this); //Make `this` a new EventEmitter 19 | }; 20 | util.inherits(Application, EventEmitter); //Inherit all EventEmitter prototype methods 21 | module.exports = Application; 22 | 23 | Application.mutableProps = ['FriendlyName', 'ApiVersion', 'VoiceUrl', 'VoiceMethod', 'VoiceFallbackUrl', 24 | 'VoiceFallbackMethod', 'StatusCallback', 'StatusCallbackMethod', 'VoiceCallerIdLookup', 25 | 'SmsUrl', 'SmsMethod', 'SmsFallbackUrl', 'SmsFallbackMethod', 'SmsStatusCallback']; 26 | Application.prototype._getResourceURI = function(type) { 27 | if(type == "create" || type == "list") 28 | return this._account._getResourceURI(type); 29 | else 30 | return this._account._getResourceURI(type) + '/Applications/' + this.Sid; 31 | } 32 | Application.prototype.load = utilFuncs.globalLoad; 33 | Application.prototype.save = utilFuncs.globalSave; 34 | Application.prototype.remove = function() { 35 | this.unregister(); 36 | utilFuncs.globalDelete.apply(this, arguments); 37 | } 38 | Application.prototype.register = function() { 39 | var valid = Application.validateApplication(this); 40 | if(valid !== true) 41 | throw new Error("This application cannot be registered because required fields " + 42 | "are missing or invalid. Hint: Check the `" + valid + "` field."); 43 | if(this._client._appMiddleware[this.Sid] == undefined) 44 | { 45 | this._client._appMiddleware[this.Sid] = this.middleware(); 46 | this._client._appMiddlewareSids.push(this.Sid); 47 | } 48 | } 49 | Application.prototype.unregister = function() { 50 | if(this._client._appMiddleware[this.Sid] != undefined) 51 | { 52 | var index = this._client._appMiddlewareSids.indexOf(this.Sid); 53 | this._client._appMiddlewareSids.splice(index, 1); 54 | delete this._client._appMiddleware[this.Sid]; 55 | } 56 | } 57 | 58 | /*static*/ Application.validateApplication = function(appInfo) { 59 | var required = ['FriendlyName', 'VoiceUrl', 'StatusCallback', 'SmsUrl', 'SmsStatusCallback']; 60 | var reqMethods = ['VoiceMethod', 'StatusCallbackMethod', 'SmsMethod']; 61 | for(var i in required) 62 | if(typeof appInfo[required[i]] != 'string' || appInfo[required[i]].length <= 0) 63 | return required[i]; 64 | for(var i in reqMethods) 65 | if(appInfo[reqMethods[i]] == null || appInfo[reqMethods[i]].toUpperCase() != 'GET') 66 | appInfo[reqMethods[i]] = 'POST'; 67 | return true; 68 | }; 69 | 70 | Application.prototype.listCalls = utilFuncs.globalList("Calls", Call); 71 | Application.prototype.listSMSMessages = utilFuncs.globalList("SMS/Messages", SMSMessage); 72 | 73 | /** 74 | makeCall(fromNumber, toNumber, [options, cb]) 75 | options include: 76 | -sendDigits 77 | -ifMachine 78 | -timeout (defaults to 40 seconds, or roughly 6 rings) 79 | cb(err, call) is called when the call is queued. 80 | You may operate on the `call` Object using TwiML verbs, which will be executed when 81 | the call is answered. 82 | If `cb` is omitted, the call will be treated like an incoming call 83 | */ 84 | Application.prototype.makeCall = function(fromNumber, toNumber, options, cb) { 85 | if(typeof options == "function") 86 | { 87 | cb = options; 88 | options = undefined; 89 | } 90 | if(options == null) 91 | options = {}; 92 | var app = this, 93 | data = { 94 | 'ApplicationSid': app.Sid, 95 | //The following line is there because of a bug within Twilio. I am working with them to get it resolved. :) 96 | 'StatusCallback': app.StatusCallback, 97 | 'From': fromNumber, 98 | 'To': toNumber, 99 | 'Timeout': 40 //changed from API spec default of 60 seconds 100 | }; 101 | if(options.sendDigits) data.SendDigits = options.sendDigits; 102 | if(options.ifMachine) data.IfMachine = options.ifMachine; 103 | if(options.timeout) data.Timeout = options.timeout; 104 | app._client._restAPI('POST', this._getResourceURI("create") + "/Calls", data, function(err, res) { 105 | if(err) return cb(err); 106 | var call = new Call(app, res.body.Sid).load(res.body); 107 | cb(null, call); 108 | app._pendingCalls[call.Sid] = call; //Add to queue 109 | call._queuedOutboundCall = true; 110 | }); 111 | } 112 | 113 | Application.prototype.sendSMS = function(fromNumber, toNumber, body, cb) { 114 | var app = this, 115 | data = { 116 | 'ApplicationSid': app.Sid, 117 | 'From': fromNumber, 118 | 'To': toNumber, 119 | 'Body': body 120 | }; 121 | app._client._restAPI('POST', this._getResourceURI("create") + "/SMS/Messages", data, function(err, res) { 122 | if(err) return cb(err); 123 | var sms = new SMSMessage(app, res.body.Sid).load(res.body); 124 | cb(null, sms); 125 | app._pendingSMSMessages[sms.Sid] = sms; //Add to queue 126 | }); 127 | } 128 | /*Application.prototype.getRandomConference = function() { 129 | return { 130 | "name": "rand:" + (_nextConf++) + ":" + Math.floor(Math.random() * 1e6); 131 | }; 132 | } 133 | Application.prototype.bridgeCalls = function(c1, c2) { 134 | if(typeof c1 == "string") 135 | c1 = this._pendingCalls[c1]; 136 | if(!(c1 instanceof Call) ) 137 | throw new Error("You must specify two call Objects or call Sids"); 138 | if(typeof c2 == "string") 139 | c2 = this._pendingCalls[c2]; 140 | if(!(c2 instanceof Call) ) 141 | throw new Error("You must specify two call Objects or call Sids"); 142 | 143 | }*/ 144 | Application.prototype.middleware = function() { 145 | var app = this; 146 | return function(req, res, next) { 147 | var protocol = (req.protocol || (req.app instanceof tls.Server ? "https" : "http")) + ':'; 148 | var voiceURL = url.parse(app.VoiceUrl, false, true), 149 | voiceStatus = url.parse(app.StatusCallback, false, true), 150 | smsURL = url.parse(app.SmsUrl, false, true), 151 | smsStatus = url.parse(app.SmsStatusCallback, false, true), 152 | reqURL = url.parse(protocol + "//" + req.headers['host'] + req.url, true, true); 153 | function match(testURL, testMethod) { 154 | return (reqURL.hostname == testURL.hostname && 155 | reqURL.pathname == testURL.pathname && 156 | req.method.toUpperCase() == testMethod.toUpperCase() ); 157 | }; 158 | function parseData(cb) { 159 | /* Parses the body of a POST request, parses the querystring and checks the 160 | signature of the incoming message. */ 161 | var data = {}, 162 | sig = protocol + "//" + req.headers['host'] + req.url; 163 | if(req.method == 'POST' && !req.body) 164 | { 165 | //Manual parsing... just in case... 166 | var buf = ''; 167 | req.on('data', function(chunk) {buf += chunk;}); 168 | req.on('end', function() { 169 | try { 170 | req.body = buf.length > 0 ? qs.parse(buf) : {}; 171 | afterBodyParser(); 172 | } catch(err) {console.log(err); return cb(err);} 173 | }); 174 | } 175 | else 176 | afterBodyParser(); 177 | function afterBodyParser() 178 | { 179 | //Merge req.body into data and continue building signature string 180 | if(req.body) 181 | { 182 | var keys = Object.keys(req.body); 183 | keys.sort(); 184 | for(var i = 0; i < keys.length; i++) 185 | { 186 | data[keys[i]] = req.body[keys[i]]; 187 | sig += keys[i] + req.body[keys[i]]; 188 | } 189 | } 190 | //Merge req.query into data 191 | if(!req.query) 192 | req.query = reqURL.query || {}; 193 | for(var i in req.query) 194 | data[i] = req.query[i]; 195 | //Now check the signature of the message 196 | var hmac = crypto.createHmac("sha1", app._client.AuthToken); 197 | hmac.update(sig); 198 | sig = hmac.digest("base64"); 199 | if(sig !== req.headers['x-twilio-signature']) 200 | cb(new Error("HMAC-SHA1 signatures do not match!") ); 201 | else 202 | cb(null, data); 203 | } 204 | } 205 | 206 | /* ------------- BEGIN TWILIO LOGIC ---------------- */ 207 | if(match(voiceURL, app.VoiceMethod) ) 208 | parseData(function(err, data) { 209 | if(err || data == null) return next(err); 210 | if(data.CallSid == null) return next(new Error("Missing CallSid") ); 211 | //Refactor call object 212 | data.Sid = data.CallSid; 213 | delete data.CallSid; 214 | data.Status = data.CallStatus; 215 | delete data.CallStatus; 216 | 217 | //console.log("/voice has been called for call " + data.Sid); 218 | if(app._pendingCalls[data.Sid] != null) 219 | { 220 | //Matched queued outgoing call 221 | var call = app._pendingCalls[data.Sid]; 222 | //Update call object 223 | for(var i in data) 224 | if(i != "cb" && i != "cbtype") 225 | call[i] = data[i]; 226 | //Emit events about the call 227 | if(data.cb) 228 | { 229 | if(call._cb[data.cb]) 230 | { 231 | switch(data.cbtype) 232 | { 233 | case 'dial': 234 | call._cb[data.cb](call, data.DialCallStatus, 235 | data.DialCallSid, data.DialCallDuration); 236 | break; 237 | case 'gather': 238 | call._cb[data.cb](call, data.Digits); 239 | break; 240 | case 'record': 241 | call._cb[data.cb](call, data.RecordingUrl, 242 | data.RecordingDuration, data.Digits); 243 | break; 244 | case 'transcribe': 245 | call._cb[data.cb](call, data.TranscriptionStatus, 246 | data.TranscriptionText, data.TranscriptionUrl, 247 | data.RecordingUrl); 248 | break; 249 | case 'liveCb': 250 | call._cb[data.cb](null, call); 251 | break; 252 | case 'cb': 253 | default: 254 | call._cb[data.cb](call); 255 | break; 256 | } 257 | if(data.cbtype === "transcribe") 258 | { 259 | res.statusCode = 200; 260 | res.end(); 261 | } 262 | else 263 | call._handle(res); 264 | delete call._cb[data.cb]; //Garbage collection, which probably isn't needed 265 | } 266 | else 267 | next(new Error("Callback " + data.cb + " was not found for call " + 268 | data.Sid) ); 269 | } 270 | //Could be a queued outbound call 271 | else if(call._queuedOutboundCall === true) 272 | { 273 | delete call._queuedOutboundCall; 274 | app.emit('outgoingCall', call); 275 | call.emit('connected', call.Status); 276 | call._handle(res); 277 | } 278 | else 279 | next(new Error("Request for pending call " + data.Sid + 280 | " did not specify a callback") ); 281 | } 282 | else 283 | { 284 | var call = new Call(app, data.Sid); 285 | call.load(data); 286 | app._pendingCalls[data.Sid] = call; 287 | app.emit('incomingCall', call); 288 | call.emit('connected', call.Status); 289 | call._handle(res); 290 | } 291 | }); 292 | else if(match(voiceStatus, app.StatusCallbackMethod) ) 293 | parseData(function(err, data) { 294 | if(err || data == null) return next(err); 295 | if(data.CallSid == null) return next(new Error("Missing CallSid") ); 296 | //Refactor call object 297 | data.Sid = data.CallSid; 298 | delete data.CallSid; 299 | data.Status = data.CallStatus; 300 | delete data.CallStatus; 301 | 302 | //console.log("/voiceStatus has been called for call " + data.Sid); 303 | if(app._pendingCalls[data.Sid] != null) 304 | { 305 | //Matched queued outgoing call 306 | var call = app._pendingCalls[data.Sid]; 307 | //Update call object 308 | for(var i in data) 309 | call[i] = data[i]; 310 | //Delete it from _pendingCalls 311 | delete app._pendingCalls[data.Sid]; 312 | //Emit events 313 | call.emit('ended', call.Status, call.CallDuration); 314 | //Respond 315 | res.statusCode = 200; 316 | res.end(); 317 | } 318 | else 319 | next(new Error("Twilio submitted call status for a call that does not exist.") ); 320 | }); 321 | else if(match(smsURL, app.SmsMethod) ) 322 | { 323 | parseData(function(err, data) { 324 | if(err || data == null) return next(err); 325 | if(data.SmsSid == null) return next(new Error("Missing SmsSid") ); 326 | //Refactor SMS Message object 327 | data.Sid = data.SmsSid; 328 | delete data.SmsSid; 329 | data.Status = data.SmsStatus; 330 | delete data.SmsStatus; 331 | 332 | //console.log("/sms has been called for SMS Message " + data.Sid); 333 | var sms = new SMSMessage(app, data.Sid); 334 | sms.load(data); 335 | app.emit('incomingSMSMessage', sms); 336 | 337 | //Respond with empty TwiML 338 | res.statusCode = 200; 339 | res.setHeader('content-type', 'application/xml'); 340 | var twiml = new Tag('Response').toString(); 341 | res.setHeader('content-length', twiml.length); 342 | res.end(twiml); 343 | }); 344 | } 345 | else if(match(smsStatus, 'POST') ) 346 | { 347 | parseData(function(err, data) { 348 | if(err || data == null) return next(err); 349 | if(data.SmsSid == undefined) return next(new Error("Missing SmsSid") ); 350 | //Refactor SMS Message object 351 | data.Sid = data.SmsSid; 352 | delete data.SmsSid; 353 | data.Status = data.SmsStatus; 354 | delete data.SmsStatus; 355 | 356 | //console.log("/smsStatus has been called for SMS Message " + data.Sid); 357 | if(app._pendingSMSMessages[data.Sid] != undefined) 358 | { 359 | //Matched queued outgoing SMS 360 | var sms = app._pendingSMSMessages[data.Sid]; 361 | //Update SMS object 362 | for(var i in data) 363 | sms[i] = data[i]; 364 | //Delete it from _pendingSMSMessages 365 | delete app._pendingSMSMessages[data.Sid]; 366 | //Emit events 367 | app.emit('outgoingSMSMessage', sms); 368 | sms.emit('sendStatus', sms.Status == "sent", sms.Status); 369 | //Respond 370 | res.statusCode = 200; 371 | res.end(); 372 | } 373 | else 374 | next(new Error("Twilio submitted SMS status for a SMS Message that does not exist.") ); 375 | }); 376 | } 377 | else 378 | next(); 379 | }; 380 | } 381 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Add voice and SMS messaging capabilities to your Node.JS applications with node-twilio-api! 2 | 3 | # node-twilio-api 4 | 5 | A high-level Twilio helper library to make Twilio API requests, handle incoming requests, 6 | and generate TwiML. 7 | 8 | Also ships with Connect/Express middleware to handle incoming Twilio requests. 9 | 10 | **IMPORTANT**: You will need a Twilio account to get started (it's not free). [Click here to sign up for 11 | an account](https://www.twilio.com/try-twilio) 12 | 13 | ## Install 14 | 15 | Project Status: Stable 16 | 17 | `npm install twilio-api` 18 | 19 | This project is rather stable. Both placing and receiving calls works rather well. 20 | You can also send/receive SMS messages. For anything else, see the docs below to see if your 21 | feature is supported. 22 | 23 | ## Features and Library Overview 24 | 25 | - [Create Twilio client](#createClient) 26 | - [Manage accounts and subaccounts](#manageAccts) 27 | - [List available local and toll-free numbers](#listNumbers) 28 | - [Manage Twilio applications](#applications) 29 | - [Place calls](#placingCalls) 30 | - [Receive calls](#incomingCallEvent) 31 | - [List calls and modify live calls](#listAndModifyCalls) 32 | - [Generate TwiML responses](#generatingTwiML) without writing any XML (I don't like XML). 33 | - [Conferences, Briding Calls, etc](#joinConference) 34 | - [Send SMS Messages](#send-sms-messages) 35 | - [Receive SMS Messages](#incomingSMSMessageEvent) 36 | - [List and Manage SMS Messages](#list-and-manage-sms-messages) 37 | - [Built-in pagination with ListIterator Object](#listIterator) 38 | 39 | ## Todo 40 | 41 | - List and manage valid outgoing phone numbers 42 | - List and provision incoming phone numbers 43 | - Support for Twilio Connect Applications 44 | - List and manage conferences, conference details, and participants 45 | - List SMS short codes and details 46 | - Respond to fallback URLs 47 | - Better scalability with multiple Node instances 48 | - An idea for this is to intercept incoming Twilio requests only if the message is for 49 | that specific instance. Perhaps use URL namespacing or cookies for this? 50 | - Access recordings, transcriptions, and notifications *(Support is limited at this time)* 51 | 52 | ## Basic Usage 53 | 54 | 1. Create a Client using your Account SID and Auth Token. 55 | 2. Load or create a Twilio application and point the VoiceUrl, SmsUrl, etc. to your Node.JS server. 56 | 3. Add the `client.middleware()` to your Express/Connect stack. Start your server. 57 | Call `app.register()` to register your application with the middleware. 58 | 4. Use `app.makeCall` to place calls 59 | 5. Use `app.on('incomingCall', function(call) {...} );` to handle inbound calls. 60 | 6. Generate TwiML by calling methods on the Call object directly. 61 | 62 | ```javascript 63 | var express = require('express'), 64 | app = express.createServer(); 65 | var twilioAPI = require('twilio-api'), 66 | cli = new twilioAPI.Client(ACCOUNT_SID, AUTH_TOKEN); 67 | app.use(cli.middleware() ); 68 | app.listen(PORT_NUMBER); 69 | //Get a Twilio application and register it 70 | cli.account.getApplication(APPLICATION_SID, function(err, app) { 71 | if(err) throw err; 72 | app.register(); 73 | app.on('incomingCall', function(call) { 74 | //Use the Call object to generate TwiML 75 | call.say("This is a test. Goodbye!"); 76 | }); 77 | app.makeCall("+12225551234", "+13335551234", function(err, call) { 78 | if(err) throw err; 79 | call.on('connected', function(status) { 80 | //Called when the caller picks up 81 | call.say("This is a test. Goodbye!"); 82 | }); 83 | call.on('ended', function(status, duration) { 84 | //Called when the call ends 85 | }); 86 | }); 87 | }); 88 | /* 89 | ... more sample code coming soon... 90 | For now, check the /tests folder 91 | */ 92 | ``` 93 | 94 | ## API 95 | 96 | The detailed documentation for twilio-api follows. 97 | 98 | #### Create Twilio client 99 | 100 | Easy enough... 101 | 102 | ```javascript 103 | var twilioAPI = require('twilio-api'); 104 | var cli = new twilioAPI.Client(AccountSid, AuthToken); 105 | ``` 106 | 107 | #### Create Express middleware 108 | 109 | - `Client.middleware()` - Returns Connect/Express middleware that handles any request for 110 | *registered applications*. A registered application will then handle the request accordingly if 111 | the method (GET/POST) and URL path of the request matches the application's VoiceUrl, 112 | StatusCallback, SmsUrl, or SmsStatusCallback. 113 | 114 | Could this be much easier? 115 | 116 | ```javascript 117 | var express = require('express'), 118 | app = express.createServer(); 119 | var twilioAPI = require('twilio-api'), 120 | cli = new twilioAPI.Client(AccountSid, AuthToken); 121 | //OK... good so far. Now tell twilio-api to intercept incoming HTTP requests. 122 | app.use(cli.middleware() ); 123 | //OK... now we need to register a Twilio application 124 | cli.account.getApplication(ApplicationSid, function(err, app) { 125 | if(err) throw err; //Maybe do something else with the error instead of throwing? 126 | 127 | /* The following line tells Twilio to look at the URL path of incoming HTTP requests 128 | and pass those requests to the application if it matches the application's VoiceUrl/VoiceMethod, 129 | SmsUrl/SmsMethod, etc. As of right now, you need to create a Twilio application to use the 130 | Express middleware. */ 131 | app.register(); 132 | }); 133 | ``` 134 | 135 | Oh, yes. The middleware also uses your Twilio AuthToken to validate incoming requests, 136 | [as described here](http://www.twilio.com/docs/security#validating-requests). If your server is 137 | running behind an HTTPS proxy, be sure that `req.protocol` contains the appropriate protocol. If 138 | using Express 3.0, set the "trust proxy" option to ensure that `req.protocol` is populated with 139 | the value in the `X-Forwarded-Proto` header. For more information, checkout the 140 | [`req.protocol` property](http://expressjs.com/api.html#req.protocol). 141 | 142 | #### Manage accounts and subaccounts 143 | 144 | - `Client.account` - the main Account Object 145 | - `Client.getAccount(Sid, cb)` - Get an Account by Sid. The Account Object is passed to the callback 146 | `cb(err, account)` 147 | - `Client.createSubAccount([FriendlyName,] cb)` Create a subaccount, where callback is `cb(err, account)` 148 | - `Client.listAccounts([filters,] cb)` - List accounts and subaccounts using the specified `filters`, 149 | where callback is `cb(err, li)` and `li` is a ListIterator Object. 150 | `filters` may include 'FriendlyName' and/or 'Status' properties. 151 | - `Account.load([cb])` - Load the Account details from Twilio, where callback is `cb(err, account)` 152 | - `Account.save([cb])` - Save the Account details to Twilio, where callback is `cb(err, account)` 153 | - `Account.closeAccount([cb])` - Permanently close this account, where callback is `cb(err, account)` 154 | - `Account.suspendAccount([cb])` - Suspend this account, where callback is `cb(err, account)` 155 | - `Account.activateAccount([cb])` - Re-activate a suspended account, where callback is `cb(err, account)` 156 | 157 | #### List available local and toll-free numbers 158 | 159 | - `Account.listAvailableLocalNumbers(countryCode, [filters,] cb)` - List available local telephone 160 | numbers in your `countryCode` available for provisioning using the provided `filters` Object. 161 | See [Twilio's documentation](http://www.twilio.com/docs/api/rest/available-phone-numbers#local) 162 | for what filters you can apply. `cb(err, li)` where `li` is a ListIterator. 163 | - `Account.listAvailableTollFreeNumbers(countryCode, [filters,] cb)` - List available toll-free 164 | numbers in your `countryCode` available for provision using the provided `filters` Object. 165 | See [Twilio's documentation](http://www.twilio.com/docs/api/rest/available-phone-numbers#toll-free) 166 | for what filters you can apply. `cb(err, li)` where `li` is a ListIterator. 167 | 168 | #### Applications 169 | 170 | - `Account.getApplication(Sid, cb)` - Get an Application by Sid. The Application Object is passed to 171 | the callback `cb(err, app)` 172 | - `Account.createApplication(voiceUrl, voiceMethod, statusCb, statusCbMethod, 173 | smsUrl, smsMethod, smsStatusCb, [friendlyName], cb)` - Creates an Application with 174 | `friendlyName`, where callback is `cb(err, app)` 175 | The `VoiceUrl`, `voiceMethod` and other required arguments are used to intercept incoming 176 | requests from Twilio using the provided Connect middleware. These URLs should point to the same 177 | server instance as the one running, and you should ensure that they do not interfere with 178 | the namespace of your web application. 179 | **CAUTION: It is highly recommended that you use 'POST' as the method for all requests; 180 | otherwise, strange behavior may occur.** 181 | - `Account.listApplications([filters,] cb)` - List applications associated with this Account. 182 | `filters` may include a 'FriendlyName' property. Callback is of the form: `cb(err, li)` 183 | - `Application.load([cb])` 184 | - `Application.save([cb])` 185 | - `Application.remove([cb])` - Permanently deletes this Application from Twilio, where callback 186 | is `cb(err, success)` and `success` is a boolean. 187 | - `Application.register()` - Registers this application to intercept the appropriate HTTP requests 188 | using the [Connect/Express middleware](#middleware). The application must provide a VoiceUrl, 189 | VoiceMethod, StatusCallback, StatusCallbackMethod, SmsUrl, SmsMethod, and SmsStatusCallback; 190 | otherwise, an exception will be thrown. 191 | - `Application.unregister()` - Unregisters this application. This happens automatically if the 192 | application is deleted. 193 | 194 | A valid application must have a VoiceUrl, VoiceMethod, StatusCallback, StatusCallbackMethod, 195 | SmsUrl, SmsMethod, and SmsStatusCallback. Fallback URLs are ignored at this time. 196 | 197 | #### Placing Calls 198 | 199 | - `Application.makeCall(from, to, [options, cb])` - Place a call and call the callback once the 200 | call is queued. If your application is registered, but your VoiceUrl is not set to the same 201 | server, the callee will likely receive an error message, and a debug error will be logged on 202 | your account. For example, if your server is running at www.example.com, please ensure that 203 | your VoiceUrl is something like: http://www.example.com/twilio/voice 204 | Also, be sure that your VoiceUrl protocol matches your protocol (HTTP vs. HTTPS). 205 | 206 | `from` is the phone number or client identifier to use as the caller id. If using a phone number, 207 | it must be a Twilio number or a verified outgoing caller id for your account. 208 | 209 | `to` is the phone number or client identifier to call. 210 | 211 | `options` is an object containing any of these additional properties: 212 | 213 | - sendDigits - A string of keys to dial after connecting to the number. Valid digits in the string 214 | include: any digit (0-9), '#', '*' and 'w' (to insert a half second pause). 215 | - ifMachine - Tell Twilio to try and determine if a machine (like voicemail) or a human has answered 216 | the call. Possible values are 'Continue', 'Hangup', and null (the default). 217 | Answering machine detection is an experimental feature, and support is limited. The downside of 218 | trying to detect a machine is that Twilio needs to listen to the first few seconds of audio after 219 | connecting a call. This usually results in a few seconds of delay before Twilio begins processing 220 | TwiML. If your application does not care about the human vs. machine distinction, then omit the 221 | 'ifMachine' option, and Twilio will perform no such analysis. 222 | - timeout - The integer number of seconds that Twilio should allow the phone to ring before assuming 223 | there is no answer. Default is 60 seconds, the maximum is 999 seconds. 224 | 225 | `cb` - Callback of the form `cb(err, call)`. This is called as soon as the call is queued, *not when 226 | the call is connected*. 227 | You can being building your TwiML response in the context of this callback, or you can listen for 228 | the various events on the Call Object. 229 | 230 | Phone numbers should be formatted with a '+' and country code e.g., +16175551212 (E.164 format). 231 | 232 | #### List Calls and Modify Live Calls 233 | 234 | - `Application.listCalls([filters,] cb)` - Lists live and completed calls associated with an Account. 235 | Note: you must call Application.listCalls, not Account.listCalls. This is a side-effect 236 | caused by the Application and Call being very inter-related. 237 | Refer to the [Twilio Documentation](http://www.twilio.com/docs/api/rest/call#list-get) to see 238 | what `filters` you can use. Again, callback is of the form: `cb(err, li)`. 239 | - `Call.load([cb])` 240 | - `Call.save([cb])` 241 | - `Call.liveCancel([cb])` - will attempt to hangup this call if it is queued or ringing, but not 242 | affect the call if it is already in progress. 243 | - `Call.liveHangUp([cb])` - will attempt to hang up this call even if it's already in progress. 244 | - `Call.liveRedirect(url, method)` - Transfers control of this call **immediately** to the TwiML at 245 | the specified URL. Note: this is quite different from `Call.redirect`, which should be used when TwiML 246 | is being served to Twilio. 247 | - `Call.liveCb(cb)` - Will use the `Call.liveRedirect` function to **immediately** re-route control to 248 | the specified callback function, `cb`. The `cb` must be of the form `cb(err, call)`. Please 249 | ensure that the Call is associated with a registered application for this to work properly. 250 | 251 | #### Generating TwiML 252 | 253 | Generating TwiML is as simple as calling methods on the Call Object. To make things simple, you 254 | cannot generate TwiML for SMS requests, only voice requests. 255 | 256 | Let's look at an example of placing and handling an outbound call: 257 | 258 | ```javascript 259 | /* Make sure that you have already setup your Twilio client, loaded and registered a valid application, 260 | and started your server. Twilio must be able to contact your server for this to work. Ensure that your 261 | server is running, proper ports are open on your firewall, etc. 262 | */ 263 | app.makeCall("+16145555555", "+16145558888", function(err, call) { 264 | if(err) throw err; 265 | //Now we can use the call Object to generate TwiML and listen for Call events 266 | call.on('connected', function(status) { 267 | /* This is called as soon as the call is connected to 16145558888 (when they answer) 268 | Note: status is probably 'in-progress' at this point. 269 | Now we generate TwiML for this call... which will served up to Twilio when the 270 | call is connected. 271 | */ 272 | call.say("Hello. This is a test of the Twilio API."); 273 | call.pause(); 274 | var gather = call.gather(function(call, digits) { 275 | call.say("You pressed " + digits + "."); 276 | var str = "Congratulations! You just used Node Twilio API to place an outgoing call."; 277 | call.say(str, {'voice': 'man', 'language': 'en'}); 278 | call.pause(); 279 | call.say(str, {'voice': 'man', 'language': 'en-gb'}); 280 | call.pause(); 281 | call.say(str, {'voice': 'woman', 'language': 'en'}); 282 | call.pause(); 283 | call.say(str, {'voice': 'woman', 'language': 'en-gb'}); 284 | call.pause(); 285 | call.say("Goodbye!"); 286 | }, { 287 | 'timeout': 10, 288 | 'numDigits': 1 289 | }); 290 | gather.say("Please press any key to continue. You may press 1, 2, 3, 4, 5, 6, 7, 8, 9, or 0."); 291 | call.say("I'm sorry. I did not hear your response. Goodbye!"); 292 | }); 293 | call.on('ended', function(status, duration) { 294 | /* This is called when the call ends and the StatusCallback is called. 295 | Note: status is probably 'completed' at this point. */ 296 | }); 297 | }); 298 | ``` 299 | 300 | ### CAUTION: COMMON PITFALL: 301 | 302 | DO NOT DO THE FOLLOWING: 303 | 304 | ```javascript 305 | app.makeCall("+16145555555", "+16145558888", function(err, call) { 306 | if(err) throw err; 307 | //Now we can use the call Object to generate TwiML and listen for Call events 308 | call.on('connected', function(status) { 309 | //Using an asyncronous function call -- INCORRECT: this will not work as expected! 310 | fs.readFile('greeting.txt', function(err, data) { 311 | if(err) throw err; 312 | call.say(data); //At this point, the 'connected' event handler has already been executed 313 | }); 314 | /* At this point, no TwiML has been generated yet, and we must serve something to Twilio. 315 | So, we serve an empty TwiML document. 316 | */ 317 | }); 318 | }); 319 | ``` 320 | 321 | Notice that the 'connected' event completes before the file has been read. This means that Twilio 322 | has already requested and received the TwiML for the call. The call will be answered by Twilio 323 | and then immediately hung up (since no TwiML is provided). 324 | 325 | Note: The above code will work, but your generated TwiML will not be executed until another Call event 326 | is triggered. 327 | 328 | See the [Common Pitfalls](https://github.com/bminer/node-twilio-api/wiki/Common-Pitfalls) 329 | page on the wiki for futher details and solutions. 330 | 331 | #### Here are all of the TwiML-generating functions you can call: 332 | 333 | - `Call.say(text[, options])` - Reads `text` to the caller using text to speech. Options include: 334 | - `voice` - 'man' or 'woman' (default: 'man') 335 | - `language` - allows you pick a voice with a specific language's accent and pronunciations. 336 | Allowed values: 'en', 'en-gb', 'es', 'fs', 'de' (default: 'en') 337 | - `loop` - specifies how many times you'd like the text repeated. The default is once. 338 | Specifying '0' will cause the <Say> verb to loop until the call is hung up. 339 | - `Call.play(audioUrl[, options])` - plays an audio file back to the caller. Twilio retrieves the 340 | file from a URL (`audioUrl`) that you provide. Options include: 341 | - `loop` - specifies how many times the audio file is played. The default behavior is to play the 342 | audio once. Specifying '0' will cause the the <Play> verb to loop until the call is hung up. 343 | - `Call.pause([duration])` - waits silently for a `duration` seconds. If <Pause> is the first 344 | verb in a TwiML document, Twilio will wait the specified number of seconds before picking up the call. 345 | - `Call.gather(cbIfInput[, options, cbIfNoInput])` - Gathers input from the telephone user's keypad. 346 | Calls `cbIfInput` once the user provides input, passing the Call object as the first argument and 347 | the input provided as the second argument. If the user does not provide valid input in a timely 348 | manner, `cbIfNoInput` is called if it was provided; otherwise, the next TwiML instruction will 349 | be executed. Options include: 350 | - `timeout` - The limit in seconds that Twilio will wait for the caller to press another digit 351 | before moving on. Twilio waits until completing the execution of all nested verbs before 352 | beginning the timeout period. (default: 5 seconds) 353 | - `finishOnKey` - When this key is pressed, Twilio assumes that input gathering is complete. For 354 | example, if you set 'finishOnKey' to '#' and the user enters '1234#', Twilio will 355 | immediately stop waiting for more input when the '#' is received and will call 356 | `cbIfInput`, passing the call Object as the first argument and the string "1234" as the second. 357 | The allowed values are the digits 0-9, '#' , '*' and the empty string (set 'finishOnKey' to ''). 358 | If the empty string is used, <Gather> captures all input and no key will end the 359 | <Gather> when pressed. (default: #) 360 | - `numDigits` - the number of digits you are expecting, and calls `cbIfInput` once the caller 361 | enters that number of digits. 362 | The `Call.gather()` function returns a Gather Object with methods: `say()`, `play()`, and `pause()`, 363 | allowing you to nest those verbs within the <Gather> verb. 364 | - `Call.record(cb[, options, cbIfEmptyRecording])` - records the caller's voice and returns to you the 365 | URL of a file containing the audio recording. Callback is called when the recording is complete 366 | and should be of the form: `cb(call, recordingUrl, recordingDuration, input)` where `call` 367 | is the Call object, `recordingUrl` is the URL that can be fetched to retrieve the recording, 368 | `recordingDuration` is the duration of the recording, and `input` is the key (if any) pressed 369 | to end the recording, or the string 'hangup' if the caller hung up. If the user does not speak 370 | during the recording, `cbIfEmptyRecording` is called if it was provided; otherwise, the next 371 | TwiML instruction will be executed. 372 | Options include: 373 | - `timeout` - tells Twilio to end the recording after a number of seconds of silence has 374 | passed. The default is 5 seconds. 375 | - `finishOnKey` - a set of digits that end the recording when entered. The allowed values are the 376 | digits 0-9, '#' and '*'. The default is '1234567890*#' (i.e. any key will end the recording). 377 | - `maxLength` - the maximum length for the recording in seconds. This defaults to 3600 seconds 378 | (one hour) for a normal recording and 120 seconds (two minutes) for a transcribed recording. 379 | - `transcribe` - tells Twilio that you would like a text representation of the audio of the recording. 380 | Twilio will pass this recording to our speech-to-text engine and attempt to convert the audio 381 | to human readable text. The 'transcribe' option is off by default. If you do not wish to 382 | perform transcription, simply do not include the transcribe attribute. 383 | **Transcription is a pay feature.** If you include a 'transcribe' or 'transcribeCallback' 384 | option, your account will be charged. See the pricing page for Twilio transcription prices. 385 | - `transcribeCallback` - a function that will be called when the transcription is complete. 386 | Callback should be of the form: `cb(call, transcriptionStatus, transcriptionText, 387 | transcriptionUrl, recordingUrl)`. TwiML generated on the Call object will not be executed until 388 | later because a transcribeCallback does not affect the call flow. 389 | - `playBeep` - play a sound before the start of a recording. If you set the value 390 | to 'false', no beep sound will be played. Defaults to true. 391 | - `Call.sms` - Not implemented, and probably will never be implemented. You can use 392 | `Application.sendSMS` to send SMS messages. 393 | 394 | - `Call.joinConference([roomName, option, cbOnEnd])` - connects the call to a conference room. If 395 | `roomName` is not specified, the caller will be placed into a uniquely named, empty conference room. 396 | The name of the conference room into which the call is placed is returned by this function. 397 | `cbOnEnd` will be called when the call ends, if it is specified; otherwise, the next TwiML 398 | instruction will be executed when this conference ends. `cbOnEnd` should be of the form: 399 | `cb(call, status)`. Please keep in mind that conferences do not start until at least two 400 | participants join; in the meantime, the caller must wait. In addition, a conference does not end 401 | until all callers drop out. This means that there are only a few ways to end a conference: 402 | 403 | - You press * and `leaveOnStar` is set. 404 | - `timeLimit` expires. 405 | - Someone leaves who had `endConferenceOnExit` set. 406 | - You end the conference manually using one of the `Conference` Object methods. 407 | 408 | Options include: 409 | 410 | - `leaveOnStar` - lets the calling party leave the conference room if '*' is pressed on the caller's 411 | keypad. Defaults to false. 412 | - `timeLimit` - the maximum duration of the conference in seconds. By default, there is a four hour 413 | time limit set on calls. 414 | - `muted` - whether the caller can speak on the conference. If this attribute is set to 'true', 415 | the participant will only be able to listen to people on the conference. Defaults to 'false'. 416 | - `beep` - whether a notification beep is played to the conference when a participant joins or 417 | leaves the conference. Defaults to true. 418 | - `startConferenceOnEnter` - tells a conference to start when this participant joins the conference, 419 | if it is not already started. Defaults to true. If this is false and the participant joins a 420 | conference that has not started, they are muted and hear background music until a participant 421 | joins where `startConferenceOnEnter` is true. 422 | - `endConferenceOnExit` - ends the conference when this caller leaves, causing all other participants 423 | in the conference to drop out. Defaults to false. 424 | - `waitUrl` - a URL for music that plays before the conference has started. The URL may be an MP3, 425 | a WAV or a TwiML document that uses or for content. Defaults to 426 | 'http://twimlets.com/holdmusic?Bucket=com.twilio.music.classical'. For more information, 427 | view the [Twilio Documentation] 428 | (http://www.twilio.com/docs/api/twiml/conference#attributes-waitUrl) 429 | - `waitMethod` - indicates which HTTP method to use when requesting 'waitUrl'. Defaults to 'POST'. 430 | Be sure to use 'GET' if you are directly requesting static audio files such as WAV or MP3 files 431 | so that Twilio properly caches the files. 432 | - `maxParticipants` - the maximum number of participants you want to allow within a named conference 433 | room. The default maximum number of participants is 40. The value must be a positive integer 434 | less than or equal to 40. 435 | - `Call.dial` - Not implemented, and probably will never be implemented. 436 | - You can use `Call.joinConference` to join a conference room. 437 | - You can use `Application.makeCall` to call other participants and tell them all to join a 438 | conference room. This is the recommended way to bridge calls, for example. 439 | - `Call.putOnHold([options])` - A special case of `Call.joinConference`. The call is placed into a 440 | random, empty conference room with the specified hold music. The conference room name is returned. 441 | Options include: 442 | - `timeLimit` - see `Call.joinConference()` above 443 | - `beep` - whether a notification beep is played to the conference when a participant joins or 444 | leaves the conference. Defaults to **false**. Note: this default is intentionally different from 445 | `Call.joinConference` 446 | - `waitUrl` - see `Call.joinConference()` above 447 | - `waitMethod` - see `Call.joinConference()` above 448 | - `Call.putOnHoldWithoutMusic([options])` - A special case of `Call.joinConference`. The call is placed into a 449 | random, empty conference room with no hold music. The conference room name is returned. 450 | Options include: 451 | - `timeLimit` - see `Call.joinConference()` above 452 | - `beep` - whether a notification beep is played to the conference when a participant joins or 453 | leaves the conference. Defaults to **false**. Note: this default is intentionally different from 454 | `Call.joinConference` 455 | - `Call.hangup()` - Ends a call. If used as the first verb in a TwiML response it does not prevent 456 | Twilio from answering the call and billing your account. 457 | - `Call.redirect(url, method)` - Transfers control of a call to the TwiML at a different URL. 458 | All verbs after <Redirect> are unreachable and ignored. Twilio will request a new TwiML document 459 | from `url` using the HTTP `method` provided. 460 | - `Call.reject([reason])` - rejects an incoming call to your Twilio number without billing you. This 461 | is very useful for blocking unwanted calls. **If and only if the first verb in a TwiML document is 462 | <Reject>, Twilio will not pick up the call. This means `Call.reject()` must be called before any 463 | other TwiML-generating function. You cannot `Call.say()` and then `Call.reject()`** 464 | The call ends with a status of 'no-answer' or 'busy', depending on the `reason` provided. 465 | **Any verbs after <Reject> are unreachable and ignored.** 466 | Possible `reason`s include: 'rejected', 'busy' (default: 'rejected') 467 | - `Call.cb(cb)` - When reached, Twilio will use the <Redirect> verb to re-route control to the 468 | specified callback function, `cb`. The `cb` will be passed the Call object. This is useful if you 469 | want to loop like this: 470 | 471 | ```javascript 472 | (function getInput() { 473 | call.gather(function(call, digits) { 474 | //Input received 475 | }).say("Please press 1, 2, or 3."); 476 | call.cb(getInput); //Loop until we get input 477 | })(); 478 | ``` 479 | 480 | #### Call Events 481 | 482 | The following Call events are only emitted if the Call is associated with a registered Application. 483 | See `app.register()` for more details. 484 | 485 | - Event: 'connected' `function(status) {}` - Emitted when the call connects. For outbound calls, 486 | this event is only emitted when the callee answers the call. Use the 'ended' event below to 487 | determine why a call was not answered. For further information, refer to the 488 | [Twilio Documentation] 489 | (http://www.twilio.com/docs/api/twiml/twilio_request#request-parameters-call-status) 490 | for possible call status values. 491 | *EDIT:* I think the only possible call status value is 'in-progress'. See issue #3. 492 | 493 | Note: As described above, one must take care when using asynchronous function calls 494 | within the 'connected' event handler, as the callbacks for these async calls will be 495 | executed *after* the TwiML has already been submitted to Twilio. See [Common Pitfalls] 496 | (https://github.com/bminer/node-twilio-api/wiki/Common-Pitfalls) for details. 497 | - Event: 'ended' `function(status, callDuration) {}` - Emitted when the call ends for whatever 498 | reason. For outbound calls, one can use this event to see why the call never connected 499 | (i.e. the person did not answer, the line was busy, etc.) See the [Twilio Documentation] 500 | (http://www.twilio.com/docs/api/twiml/twilio_request#request-parameters-call-status) 501 | for possible call status values. 502 | *EDIT:* I believe the only possible call status values are: `['completed', 'busy', 503 | 'failed', 'no-answer', 'canceled']`. See issue #3. 504 | 505 | #### Application Events 506 | 507 | - Event: 'outgoingCall' `function(call) {}` - Emitted when Twilio connects an outgoing call 508 | placed with `Application.makeCall()`. It is not common to listen for this event. 509 | - Event: 'incomingCall' `function(call) {}` - Emitted when the 510 | Twilio middleware receives a voice request from Twilio. Once you have the Call Object, you 511 | can [generate a TwiML response](#generatingTwiML) or listen for Call events. 512 | - Event: 'outgoingSMSMessage' `function(smsMessage) {}` - Emitted when Twilio sends an outgoing 513 | SMS message sent with `Application.sendSMS()`. It is not common to listen for this event. 514 | - Event: 'incomingSMSMessage' `function(smsMessage) {}` - 515 | Emitted when the Twilio middleware receives a SMS message request from Twilio. 516 | 517 | #### Send SMS Messages 518 | 519 | - `Application.sendSMS(from, to, body [, cb])` - Send a SMS Message to a SMS-enabled phone. 520 | If your application is registered, but your SmsStatusCallbackUrl is not set to the same server, 521 | the SMSMessage Object will not emit the 'sendStatus' event. For example, if your server 522 | is running at www.example.com, please ensure that your 523 | SmsStatusCallbackUrl is something like: http://www.example.com/twilio/smsStatus 524 | Also, be sure that your SmsStatusCallbackUrl protocol matches your protocol (HTTP vs. HTTPS). 525 | 526 | `from` - A Twilio phone number enabled for SMS. Only phone numbers or short codes purchased from 527 | Twilio work here. 528 | 529 | `to` - the destination phone number. 530 | 531 | `body` - the body of the message you want to send, limited to 160 characters. 532 | 533 | `cb` - Callback of the form `cb(err, smsMessage)`. This is called as soon as the message is 534 | queued to be sent, *not when the message is actually sent/delivered*. To check the message's 535 | send status, you can listen for the 'sendStatus' event on the SMSMessage Object. 536 | 537 | Phone numbers should be formatted with a '+' and country code e.g., +16175551212 (E.164 format). 538 | 539 | #### List and Manage SMS Messages 540 | 541 | - `Application.listSMSMessages([filters,] cb)` - Lists inbound and outbound SMS Messages associated 542 | with an Account. Note: you must call Application.listSMSMessages, not Account.listSMSMessages. 543 | This is a side-effect caused by the Application and SMS Message being very inter-related. 544 | Refer to the [Twilio Documentation](http://www.twilio.com/docs/api/rest/sms#list-get) to see 545 | what `filters` you can use. Callback is of the form: `cb(err, li)`. 546 | - `SMSMessage.load([cb])` 547 | - `SMSMessage.reply(body [, cb])` - Calls `Application.sendSMS(this.To, this.From, body, cb)` 548 | internally. This method will not work from outbound SMS Messages. 549 | - Useful SMS Properties include: 550 | - From - The phone number that sent this message 551 | - To - The phone number of the recipient 552 | - Body - The text body of the SMS message. Up to 160 characters long 553 | - Status - The status of this SMS message. Either queued, sending, sent, or failed. 554 | - Direction - The direction of this SMS message. 'incoming' for incoming messages, 555 | 'outbound-api' for messages initiated via the REST API, 'outbound-call' for messages 556 | initiated during a call or 'outbound-reply' for messages initiated in response to an 557 | incoming SMS. At this time, node-twilio-api does not support 'outbound-call' or 558 | 'outbound-reply'; all outbound SMS messages are marked 'outbound-api'. 559 | - Dates (i.e. DateCreated, DateSent) 560 | - Geographic information (i.e. FromCity, FromState, ..., ToZip, ToCountry) 561 | - Any others: http://www.twilio.com/docs/api/twiml/sms/twilio_request#synchronous 562 | 563 | #### SMS Message Events 564 | 565 | - Event: 'sendStatus' `function(success, status) {}` - Emitted when an outbound SMS message has been 566 | processed. Success is either true or false; status is either "sent" or "failed". 567 | 568 | #### ListIterator 569 | 570 | A ListIterator Object is returned when Twilio reponses may be large. For example, if one were to list 571 | all subaccounts, the list might be relatively lengthy. For these responses, Twilio returns 20 or so 572 | items in the list and allows us to access the rest of the list with another API call. To simplify this 573 | process, any API call that would normally return a list returns a ListIterator Object instead. 574 | 575 | The ListIterator Object has several properties and methods: 576 | 577 | - `Page` - A property of the ListIterator that tells you which page is loaded at this time 578 | - `NumPages` - The number of pages in the resultset 579 | - `PageSize` - The number of results per page (this can be changed and the default is 20) 580 | - `Results` - The array of results. If results are a list of accounts, this will be an array of Account 581 | Objects, if it's a list of applications, this will be an array of Application Objects, etc. 582 | - `nextPage([cb])` - Requests that the next page of results be loaded. Callback is of the form 583 | `cb(err, li)` 584 | - `prevPage([cb])` - Requests that the previous page of results be loaded. 585 | 586 | ## Testing 587 | 588 | twilio-api uses [nodeunit](https://github.com/caolan/nodeunit) right now for testing. To test the package, 589 | run `npm test` in the root directory of the repository. 590 | 591 | **BEWARE:** Running the test suite *may* actually place calls and cause you to incur fees on your Twilio 592 | account. Please look through the test suite before running it. 593 | 594 | ## Disclaimer 595 | 596 | Blake Miner is not affliated with Twilio, Inc. in any way. 597 | Use this software AT YOUR OWN RISK. See LICENSE for more details. 598 | --------------------------------------------------------------------------------