├── .gitignore ├── .nvmrc ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── forward-message-as-email ├── README.md ├── __mocks__ │ └── got.js ├── sendgrid.js ├── sendgrid.test.js ├── sparkpost.js └── sparkpost.test.js ├── forward-message ├── README.md ├── forward-message-multiple.js ├── forward-message-multiple.test.js ├── forward-message.js └── forward-message.test.js ├── hello-world ├── README.md ├── hello-world.js └── hello-world.test.js ├── hunt ├── README.md ├── hunt.js └── hunt.test.js ├── package-lock.json ├── package.json └── test └── test-helper.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 6.10.2 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at philnash@twilio.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Phil Nash 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Useful Twilio Functions - Deprecated ⚠️ 2 | 3 | _This repo is now deprecated in favour of [@twilio-labs/function-templates](https://github.com/twilio-labs/function-templates). There are more templates, better tests and integration with [`twilio-run`](https://github.com/twilio-labs/twilio-run) and [`create-twilio-function`](https://github.com/twilio-labs/create-twilio-function). Please head to [@twilio-labs/function-templates](https://github.com/twilio-labs/function-templates) and start using it for your useful Twilio Functions._ 4 | 5 | --- 6 | 7 | [![Build Status](https://travis-ci.org/philnash/useful-twilio-functions.svg?branch=master)](https://travis-ci.org/philnash/useful-twilio-functions) 8 | 9 | [Twilio Functions](https://www.twilio.com/functions) are a serverless environment to build and run Twilio applications so you can get to production faster. You provide the Node.js code to perform the task you need and Twilio runs it. You can read [more about Twilio Functions and how to use them in the introductory blog post](https://www.twilio.com/blog/2017/05/introducing-twilio-functions.html). 10 | 11 | ## This repo 12 | 13 | This repo is intended to be a collection of useful Twilio Functions that are tested and documented. The intention is that you can take any of these Functions and drop them into a project, confident they will work. Whether you are a developer looking for a particular building block or a builder who justs needs a particular Function. 14 | 15 | ## Usage 16 | 17 | Each Function lives in its own directory within this repo. To use the code, copy the contents of the main Function file to your Twilio Function. Make sure you read the Function README so that you know which environment variables to set and the parameters the Function takes when you make HTTP requests to it. 18 | 19 | ## Available Functions 20 | 21 | This is the list of Functions available in this repo: 22 | 23 | - [Hello world](hello-world) - A very basic Function to get this repo started 24 | - [Forward message](forward-message) - Forward incoming SMS messages to another number 25 | - [Forward message to email using SendGrid](forward-message-as-email) 26 | - [Hunt/Find me](hunt) - Call a list of numbers in order until one answers 27 | 28 | ### Todo 29 | 30 | - [ ] Forward message to email with other API providers 31 | - [ ] Generate Video access token 32 | - [ ] Generate Sync access token 33 | - [ ] Generate Chat access token 34 | - [ ] Generate Twilio Client access token 35 | - [ ] Voicemail 36 | - [ ] Conference line 37 | - [ ] Inbound calls for SIP registration 38 | - [ ] Outbound calls for SIP registration 39 | - [x] Hunt/FindMe caller (take a list of numbers and calls each in order until one answers) 40 | - [ ] Translate webhook from `application/x-www-form-urlencoded` into `application/json` and forward on to another service 41 | 42 | Please add ideas if you have them. 43 | 44 | ## Contribute 45 | 46 | Pull requests and new Functions are accepted. To make a contribution, follow these steps: 47 | 48 | 1. Fork this repository ( https://github.com/philnash/useful-twilio-functions/fork ) 49 | 2. Create your feature branch (git checkout -b my-new-feature) 50 | 3. Commit your changes (git commit -am 'Add some feature') 51 | 4. Push to the branch (git push origin my-new-feature) 52 | 5. Create a new Pull Request 53 | 54 | Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT). By participating in this project you agree to abide by its terms. 55 | 56 | ### Install 57 | 58 | This project and Twilio Functions use Node.js version 6.10.2. I recommend using [nvm](https://github.com/creationix/nvm) if you need to install multiple versions of Node.js. Clone or download the project, use the correct version of Node.js and install the dependencies: 59 | 60 | ```bash 61 | nvm use 62 | npm install 63 | ``` 64 | 65 | ### Tests 66 | 67 | Tests are written with [Jest](https://facebook.github.io/jest/). Run them with: 68 | 69 | ```bash 70 | npm test 71 | ``` 72 | 73 | ## License 74 | 75 | MIT © Phil Nash 76 | -------------------------------------------------------------------------------- /forward-message-as-email/README.md: -------------------------------------------------------------------------------- 1 | # Forward SMS message as an email 2 | 3 | This set of Functions will receive an incoming SMS message and forward the message to an email address. 4 | 5 | There are different Functions for different email service providers. The Function is named after the provider. 6 | 7 | ## SendGrid 8 | 9 | The SendGrid Function will forward incoming SMS messages to an email address using the [SendGrid API](http://sendgrid.com/). 10 | 11 | You will need a SendGrid account and an [API key](https://app.sendgrid.com/settings/api_keys) to use this Function. 12 | 13 | ### Environment Variables 14 | 15 | This Function expects three environment variables to be set. 16 | 17 | | Variable | Meaning | 18 | | :------------------- | :--------------------------------------------------------- | 19 | | `SENDGRID_API_KEY` | Your SendGrid API key | 20 | | `TO_EMAIL_ADDRESS` | The email address to forward the message to | 21 | | `FROM_EMAIL_ADDRESS` | The email address that SendGrid should send the email from | 22 | 23 | ### Dependencies 24 | 25 | This Function depends on one npm module. You should add the following dependencies in your [Functions configuration page](https://www.twilio.com/console/runtime/functions/configure). 26 | 27 | | Dependency | Version | 28 | | :--------- | :------ | 29 | | `got` | ^6.7.1 | 30 | 31 | ### Parameters 32 | 33 | This Function expects the incoming request to be a messaging webhook. The parameters that will be used are `From` and `Body`. 34 | 35 | ## SparkPost 36 | 37 | The SparkPost Function will forward incoming SMS messages to an email address using the [SparkPost API](https://www.sparkpost.com/). 38 | 39 | You will need a SparkPost account and an [API key](https://app.sparkpost.com/account/credentials) to use this Function. 40 | 41 | ### The SparkPost Sandbox 42 | 43 | If you are testing out with SparkPost you can [use their sandbox domain to send test emails](https://developers.sparkpost.com/api/transmissions/#header-the-sandbox-domain). To do so you should set your `FROM_EMAIL_ADDRESS` to "anything@sparkpostbox.com" and set `options.sandbox` to `true`. 44 | 45 | This code example includes the `sandbox` option. Ensure you set it to `false` or remove it when you change to use your own domain. 46 | 47 | ### Environment Variables 48 | 49 | This Function expects three environment variables to be set. 50 | 51 | | Variable | Meaning | 52 | | :------------------- | :---------------------------------------------------------- | 53 | | `SPARKPOST_API_KEY` | Your SparkPost API key | 54 | | `TO_EMAIL_ADDRESS` | The email address to forward the message to | 55 | | `FROM_EMAIL_ADDRESS` | The email address that SparkPost should send the email from | 56 | 57 | ### Dependencies 58 | 59 | This Function depends on one npm module. You should add the following dependencies in your [Functions configuration page](https://www.twilio.com/console/runtime/functions/configure). 60 | 61 | | Dependency | Version | 62 | | :--------- | :------ | 63 | | `got` | ^6.7.1 | 64 | -------------------------------------------------------------------------------- /forward-message-as-email/__mocks__/got.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | shouldSucceed: true, 3 | post: function(url, options) { 4 | return new Promise((resolve, reject) => { 5 | if (this.shouldSucceed) { 6 | process.nextTick(resolve()); 7 | } else { 8 | process.nextTick(reject(new Error())); 9 | } 10 | }); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /forward-message-as-email/sendgrid.js: -------------------------------------------------------------------------------- 1 | const got = require('got'); 2 | 3 | exports.handler = function(context, event, callback) { 4 | const requestBody = { 5 | personalizations: [{ to: [{ email: context.TO_EMAIL_ADDRESS }] }], 6 | from: { email: context.FROM_EMAIL_ADDRESS }, 7 | subject: `New SMS message from: ${event.From}`, 8 | content: [ 9 | { 10 | type: 'text/plain', 11 | value: event.Body 12 | } 13 | ] 14 | }; 15 | 16 | got 17 | .post('https://api.sendgrid.com/v3/mail/send', { 18 | headers: { 19 | Authorization: `Bearer ${context.SENDGRID_API_KEY}`, 20 | 'Content-Type': 'application/json' 21 | }, 22 | body: JSON.stringify(requestBody) 23 | }) 24 | .then(response => { 25 | let twiml = new Twilio.twiml.MessagingResponse(); 26 | callback(null, twiml); 27 | }) 28 | .catch(err => { 29 | callback(err); 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /forward-message-as-email/sendgrid.test.js: -------------------------------------------------------------------------------- 1 | const helpers = require('../test/test-helper'); 2 | const got = jest.mock('got'); 3 | const sendGrid = require('./sendgrid').handler; 4 | const Twilio = require('twilio'); 5 | 6 | const context = { 7 | SENDGRID_API_KEY: 'APIKEY', 8 | TO_EMAIL_ADDRESS: 'test_to@example.com', 9 | FROM_EMAIL_ADDRESS: 'test_from@example.com' 10 | }; 11 | const event = { 12 | Body: 'Hello', 13 | From: 'ExternalNumber' 14 | }; 15 | 16 | beforeAll(() => { 17 | helpers.setup(context); 18 | }); 19 | 20 | afterAll(() => { 21 | helpers.teardown(); 22 | }); 23 | 24 | test('returns an TwiML MessagingResponse', done => { 25 | const callback = (err, result) => { 26 | expect(result).toBeInstanceOf(Twilio.twiml.MessagingResponse); 27 | done(); 28 | }; 29 | 30 | sendGrid(context, event, callback); 31 | }); 32 | 33 | test('returns an error when the external request fails', done => { 34 | const callback = (err, result) => { 35 | expect(err).toBeInstanceOf(Error); 36 | done(); 37 | }; 38 | 39 | got.shouldSucceed = false; 40 | 41 | sendGrid(context, event, callback); 42 | }); 43 | -------------------------------------------------------------------------------- /forward-message-as-email/sparkpost.js: -------------------------------------------------------------------------------- 1 | const got = require('got'); 2 | 3 | exports.handler = function(context, event, callback) { 4 | const requestBody = { 5 | content: { 6 | from: context.FROM_EMAIL_ADDRESS, 7 | subject: `New SMS message from: ${event.From}`, 8 | text: event.Body 9 | }, 10 | recipients: [{ address: { email: context.TO_EMAIL_ADDRESS } }], 11 | options: { 12 | sandbox: true 13 | } 14 | }; 15 | 16 | got 17 | .post('https://api.sparkpost.com/api/v1/transmissions', { 18 | headers: { 19 | Authorization: `${context.SPARKPOST_API_KEY}`, 20 | 'Content-Type': 'application/json' 21 | }, 22 | body: JSON.stringify(requestBody) 23 | }) 24 | .then(response => { 25 | const twiml = new Twilio.twiml.MessagingResponse(); 26 | callback(null, twiml); 27 | }) 28 | .catch(err => { 29 | callback(err); 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /forward-message-as-email/sparkpost.test.js: -------------------------------------------------------------------------------- 1 | const helpers = require('../test/test-helper'); 2 | const got = jest.mock('got'); 3 | const sparkPost = require('./sparkpost').handler; 4 | const Twilio = require('twilio'); 5 | 6 | const context = { 7 | SPARKPOST_API_KEY: 'APIKEY', 8 | TO_EMAIL_ADDRESS: 'test_to@example.com', 9 | FROM_EMAIL_ADDRESS: 'test_from@example.com' 10 | }; 11 | const event = { 12 | Body: 'Hello', 13 | From: 'ExternalNumber' 14 | }; 15 | 16 | beforeAll(() => { 17 | helpers.setup(context); 18 | }); 19 | 20 | afterAll(() => { 21 | helpers.teardown(); 22 | }); 23 | 24 | test('returns an TwiML MessagingResponse', done => { 25 | const callback = (err, result) => { 26 | expect(result).toBeInstanceOf(Twilio.twiml.MessagingResponse); 27 | done(); 28 | }; 29 | 30 | sparkPost(context, event, callback); 31 | }); 32 | 33 | test('returns an error when the external request fails', done => { 34 | const callback = (err, result) => { 35 | expect(err).toBeInstanceOf(Error); 36 | done(); 37 | }; 38 | 39 | got.shouldSucceed = false; 40 | 41 | sparkPost(context, event, callback); 42 | }); 43 | -------------------------------------------------------------------------------- /forward-message/README.md: -------------------------------------------------------------------------------- 1 | # Forward Message 2 | 3 | This set of Functions will receive an incoming SMS message and forward it on to other numbers. There is a Function for forwarding to a single number and another for multiple numbers. 4 | 5 | ## Forward to one number 6 | 7 | This Function in `forward-message.js` will return the TwiML required to forward an incoming SMS message to a number that is set in the environment variables. 8 | 9 | ### Environment variables 10 | 11 | This Function expects one environment variable to be set. 12 | 13 | | Variable | Meaning | 14 | | :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 15 | | `MY_PHONE_NUMBER` | The number you want to forward incoming messages to [in E.164 format](https://support.twilio.com/hc/en-us/articles/223183008-Formatting-International-Phone-Numbers) | 16 | 17 | ### Parameters 18 | 19 | This Function expects the incoming request to be a messaging webhook. The parameters that will be used are `From` and `Body`. 20 | 21 | ## Forward to multiple numbers 22 | 23 | This Function in `forward-message-multiple.js` will return the TwiML required to forward an incoming SMS message to each number in a comma separated list of numbers set in the environment variables. 24 | 25 | ### Environment variables 26 | 27 | This Function expects one environment variable to be set. 28 | 29 | | Variable | Meaning | 30 | | :------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 31 | | `FORWARDING_NUMBERS` | A list of numbers [in E.164 format](https://support.twilio.com/hc/en-us/articles/223183008-Formatting-International-Phone-Numbers) you want to forward incoming messages to, separated by commas | 32 | 33 | ### Parameters 34 | 35 | This Function expects the incoming request to be a messaging webhook. The parameters that will be used are `From` and `Body`. 36 | -------------------------------------------------------------------------------- /forward-message/forward-message-multiple.js: -------------------------------------------------------------------------------- 1 | exports.handler = function(context, event, callback) { 2 | let twiml = new Twilio.twiml.MessagingResponse(); 3 | context.FORWARDING_NUMBERS.split(/,\s?/).forEach(number => { 4 | console.log(number); 5 | twiml.message(`From: ${event.From}. Body: ${event.Body}`, { 6 | to: number 7 | }); 8 | }); 9 | callback(null, twiml); 10 | }; 11 | -------------------------------------------------------------------------------- /forward-message/forward-message-multiple.test.js: -------------------------------------------------------------------------------- 1 | const helpers = require('../test/test-helper'); 2 | const forwardMessageMultiple = require('./forward-message-multiple').handler; 3 | const Twilio = require('twilio'); 4 | 5 | const context = { 6 | FORWARDING_NUMBERS: 'TwilioNumber1, TwilioNumber2' 7 | }; 8 | const event = { 9 | Body: 'Hello', 10 | From: 'ExternalNumber' 11 | }; 12 | 13 | beforeAll(() => { 14 | helpers.setup(context); 15 | }); 16 | 17 | afterAll(() => { 18 | helpers.teardown(); 19 | }); 20 | 21 | test('returns a MessagingResponse', done => { 22 | const callback = (err, result) => { 23 | expect(result).toBeInstanceOf(Twilio.twiml.MessagingResponse); 24 | done(); 25 | }; 26 | 27 | forwardMessageMultiple(context, event, callback); 28 | }); 29 | 30 | test('forwards the message to both numbers from the context', done => { 31 | const callback = (err, result) => { 32 | const twiml = result.toString(); 33 | expect(twiml).toMatch('to="TwilioNumber1"'); 34 | expect(twiml).toMatch('to="TwilioNumber2"'); 35 | expect(twiml.match(/ { 43 | const callback = (err, result) => { 44 | expect(result.toString()).toMatch( 45 | '>From: ' + event.From + '. Body: ' + event.Body + '<' 46 | ); 47 | done(); 48 | }; 49 | 50 | forwardMessageMultiple(context, event, callback); 51 | }); 52 | -------------------------------------------------------------------------------- /forward-message/forward-message.js: -------------------------------------------------------------------------------- 1 | exports.handler = function(context, event, callback) { 2 | let twiml = new Twilio.twiml.MessagingResponse(); 3 | twiml.message(`From: ${event.From}. Body: ${event.Body}`, { 4 | to: context.MY_PHONE_NUMBER 5 | }); 6 | callback(null, twiml); 7 | }; 8 | -------------------------------------------------------------------------------- /forward-message/forward-message.test.js: -------------------------------------------------------------------------------- 1 | const helpers = require('../test/test-helper'); 2 | const forwardMessage = require('./forward-message').handler; 3 | const Twilio = require('twilio'); 4 | 5 | const context = { 6 | MY_PHONE_NUMBER: 'TwilioNumber' 7 | }; 8 | const event = { 9 | Body: 'Hello', 10 | From: 'ExternalNumber' 11 | }; 12 | 13 | beforeAll(() => { 14 | helpers.setup(context); 15 | }); 16 | 17 | afterAll(() => { 18 | helpers.teardown(); 19 | }); 20 | 21 | test('returns a MessagingResponse', done => { 22 | const callback = (err, result) => { 23 | expect(result).toBeInstanceOf(Twilio.twiml.MessagingResponse); 24 | done(); 25 | }; 26 | 27 | forwardMessage(context, event, callback); 28 | }); 29 | 30 | test('forwards the message to the number from the context', done => { 31 | const callback = (err, result) => { 32 | expect(result.toString()).toMatch('to="' + context.MY_PHONE_NUMBER + '"'); 33 | done(); 34 | }; 35 | 36 | forwardMessage(context, event, callback); 37 | }); 38 | 39 | test('includes the original From number and Body', done => { 40 | const callback = (err, result) => { 41 | expect(result.toString()).toMatch( 42 | '>From: ' + event.From + '. Body: ' + event.Body + '<' 43 | ); 44 | done(); 45 | }; 46 | 47 | forwardMessage(context, event, callback); 48 | }); 49 | -------------------------------------------------------------------------------- /hello-world/README.md: -------------------------------------------------------------------------------- 1 | # Hello world! 2 | 3 | This Function will return the string 'Hello world'. 4 | 5 | ## Environment variables 6 | 7 | This Function requires no environment variables to run successfully. 8 | 9 | ## Parameters 10 | 11 | This Function requires no URL or POST parameters to run successfully. 12 | -------------------------------------------------------------------------------- /hello-world/hello-world.js: -------------------------------------------------------------------------------- 1 | exports.handler = function(context, event, callback) { 2 | callback(null, 'Hello world!'); 3 | }; 4 | -------------------------------------------------------------------------------- /hello-world/hello-world.test.js: -------------------------------------------------------------------------------- 1 | const helloWorld = require('./hello-world').handler; 2 | 3 | test('returns the string "Hello world!"', done => { 4 | const callback = (err, result) => { 5 | expect(result).toBe('Hello world!'); 6 | done(); 7 | }; 8 | helloWorld({}, {}, callback); 9 | }); 10 | -------------------------------------------------------------------------------- /hunt/README.md: -------------------------------------------------------------------------------- 1 | # Hunt / Find Me 2 | 3 | This Function takes an array of numbers and will return the TwiML required to dial each number in order until one answers. This is an initial implementation of the ["Find Me" Twimlet](https://www.twilio.com/labs/twimlets/findme). 4 | 5 | ## Environment variables 6 | 7 | This Function expects one environment variable to be set. 8 | 9 | | Variable | Meaning | Required | 10 | | :---------------- | :------ | :------- | 11 | | `PHONE_NUMBERS` | A comma separated list of numbers [in E.164 format](https://support.twilio.com/hc/en-us/articles/223183008-Formatting-International-Phone-Numbers) that you want to dial in order | Yes | 12 | | `FINAL_URL` | A URL to redirect the call to if none of the numbers answer. If this is not supplied then the call will just hang up once it has exhausted all the options | No | 13 | 14 | ## Parameters 15 | 16 | This Function expects the incoming request to be a voice webhook. The parameters that will be used are `DialCallStatus` and a custom parameter, `nextNumber` that the function itself provides. 17 | -------------------------------------------------------------------------------- /hunt/hunt.js: -------------------------------------------------------------------------------- 1 | exports.handler = function(context, event, callback) { 2 | const numbers = context.PHONE_NUMBERS.split(',').map(number => number.trim()); 3 | const response = new Twilio.twiml.VoiceResponse(); 4 | if (event.DialCallStatus === 'complete') { 5 | // Call was answered and completed 6 | response.hangup(); 7 | } else if (event.finished === 'true') { 8 | if (context.FINAL_URL) { 9 | response.redirect(context.FINAL_URL); 10 | } else { 11 | response.hangup(); 12 | } 13 | } else { 14 | const numberToDial = event.nextNumber ? event.nextNumber : numbers[0]; 15 | const currentNumberIndex = numbers.indexOf(numberToDial); 16 | let url; 17 | if (currentNumberIndex + 1 === numbers.length) { 18 | // No more numbers to call after this. 19 | url = '/hunt?finished=true'; 20 | } else { 21 | const nextNumber = numbers[currentNumberIndex + 1]; 22 | url = '/hunt?nextNumber=' + encodeURIComponent(nextNumber); 23 | } 24 | const dial = response.dial({ action: url }); 25 | dial.number(numberToDial); 26 | } 27 | callback(null, response); 28 | }; 29 | -------------------------------------------------------------------------------- /hunt/hunt.test.js: -------------------------------------------------------------------------------- 1 | const helpers = require('../test/test-helper'); 2 | const hunt = require('./hunt').handler; 3 | const Twilio = require('twilio'); 4 | 5 | const context = { 6 | PHONE_NUMBERS: '+1234567890,+10987654321' 7 | }; 8 | 9 | const setupLifeCycle = context => { 10 | beforeAll(() => { 11 | helpers.setup(context); 12 | }); 13 | afterAll(() => { 14 | helpers.teardown(); 15 | }); 16 | }; 17 | 18 | describe('a completed call', () => { 19 | const event = { 20 | DialCallStatus: 'complete' 21 | }; 22 | 23 | setupLifeCycle(context); 24 | 25 | test('returns a VoiceResponse', done => { 26 | const callback = (err, result) => { 27 | expect(result).toBeInstanceOf(Twilio.twiml.VoiceResponse); 28 | done(); 29 | }; 30 | 31 | hunt(context, event, callback); 32 | }); 33 | 34 | test('returns a hangup', done => { 35 | const callback = (err, result) => { 36 | expect(result.toString()).toMatch(''); 37 | done(); 38 | }; 39 | 40 | hunt(context, event, callback); 41 | }); 42 | }); 43 | 44 | describe('the first call', () => { 45 | const event = {}; 46 | 47 | setupLifeCycle(context); 48 | 49 | test('returns a VoiceResponse', done => { 50 | const callback = (err, result) => { 51 | expect(result).toBeInstanceOf(Twilio.twiml.VoiceResponse); 52 | done(); 53 | }; 54 | 55 | hunt(context, event, callback); 56 | }); 57 | 58 | test('returns a dial to the first number', done => { 59 | const callback = (err, result) => { 60 | const xml = result.toString(); 61 | expect(xml).toMatch('+1234567890<'); 64 | done(); 65 | }; 66 | 67 | hunt(context, event, callback); 68 | }); 69 | 70 | test('sets the next number to dial in the url', done => { 71 | const callback = (err, result) => { 72 | const xml = result.toString(); 73 | expect(xml).toMatch('/hunt?nextNumber=%2B10987654321'); 74 | done(); 75 | }; 76 | 77 | hunt(context, event, callback); 78 | }); 79 | }); 80 | 81 | describe('a subsequent call', () => { 82 | const context = { 83 | PHONE_NUMBERS: '+1234567890, +10987654321 ' 84 | }; 85 | const event = { 86 | nextNumber: '+10987654321' 87 | }; 88 | 89 | setupLifeCycle(context); 90 | 91 | test('returns a VoiceResponse', done => { 92 | const callback = (err, result) => { 93 | expect(result).toBeInstanceOf(Twilio.twiml.VoiceResponse); 94 | done(); 95 | }; 96 | 97 | hunt(context, event, callback); 98 | }); 99 | 100 | test('returns a dial to the next number', done => { 101 | const callback = (err, result) => { 102 | const xml = result.toString(); 103 | expect(xml).toMatch('+10987654321<'); 106 | done(); 107 | }; 108 | 109 | hunt(context, event, callback); 110 | }); 111 | 112 | test('sets the url to finished', done => { 113 | const callback = (err, result) => { 114 | const xml = result.toString(); 115 | expect(xml).toMatch('/hunt?finished=true'); 116 | done(); 117 | }; 118 | 119 | hunt(context, event, callback); 120 | }); 121 | }); 122 | 123 | describe('the last call', () => { 124 | const event = { 125 | finished: 'true' 126 | }; 127 | 128 | describe('with no final url set', () => { 129 | setupLifeCycle(context); 130 | 131 | test('returns a VoiceResponse', done => { 132 | const callback = (err, result) => { 133 | expect(result).toBeInstanceOf(Twilio.twiml.VoiceResponse); 134 | done(); 135 | }; 136 | 137 | hunt(context, event, callback); 138 | }); 139 | 140 | test('returns a hangup', done => { 141 | const callback = (err, result) => { 142 | expect(result.toString()).toMatch(''); 143 | done(); 144 | }; 145 | 146 | hunt(context, event, callback); 147 | }); 148 | }); 149 | 150 | describe('with a final url set', () => { 151 | const context = { 152 | PHONE_NUMBERS: '+1234567890,+10987654321', 153 | FINAL_URL: '/no-answer' 154 | }; 155 | 156 | setupLifeCycle(context); 157 | 158 | test('returns a VoiceResponse', done => { 159 | const callback = (err, result) => { 160 | expect(result).toBeInstanceOf(Twilio.twiml.VoiceResponse); 161 | done(); 162 | }; 163 | 164 | hunt(context, event, callback); 165 | }); 166 | 167 | test('returns a redirect', done => { 168 | const callback = (err, result) => { 169 | const xml = result.toString(); 170 | expect(xml).toMatch(''); 171 | expect(xml).toMatch(context.FINAL_URL); 172 | done(); 173 | }; 174 | 175 | hunt(context, event, callback); 176 | }); 177 | }); 178 | }); 179 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "useful-twilio-functions", 3 | "version": "0.0.1", 4 | "private": true, 5 | "description": "A set of useful Twilio Functions.", 6 | "scripts": { 7 | "test": "jest" 8 | }, 9 | "author": "Phil Nash ", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "eslint": "^5.4.0", 13 | "jest-cli": "^23.5.0" 14 | }, 15 | "jest": { 16 | "testEnvironment": "node", 17 | "collectCoverage": true, 18 | "coveragePathIgnorePatterns": [ 19 | "/node_modules/", 20 | "/test/" 21 | ] 22 | }, 23 | "dependencies": { 24 | "got": "^6.7.1", 25 | "twilio": "3.6.3" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/test-helper.js: -------------------------------------------------------------------------------- 1 | const Twilio = require('twilio'); 2 | 3 | const setup = (context = {}) => { 4 | global.Twilio = Twilio; 5 | if (context.ACCOUNT_SID && context.AUTH_TOKEN) { 6 | global.twilioClient = new Twilio(context.ACCOUNT_SID, context.AUTH_TOKEN); 7 | } 8 | }; 9 | 10 | const teardown = () => { 11 | delete global.Twilio; 12 | if (global.twilioClient) delete global.twilioClient; 13 | }; 14 | 15 | module.exports = { 16 | setup: setup, 17 | teardown: teardown 18 | }; 19 | --------------------------------------------------------------------------------