├── .env.example ├── .eslintrc.yml ├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── LICENSE ├── Procfile ├── README.md ├── app.json ├── config.js ├── controllers ├── message.js ├── pages.js └── router.js ├── index.js ├── lib └── messageSender.js ├── models └── subscriber.js ├── package-lock.json ├── package.json ├── public ├── 404.html ├── 500.html ├── css │ └── main.css └── landing.html ├── test ├── connectionHelper.js ├── controllers │ └── message.test.js └── lib │ └── messageSender.test.js ├── views ├── index.jade ├── layout.jade └── twiml.jade └── webapp.js /.env.example: -------------------------------------------------------------------------------- 1 | 2 | # Your account id 3 | TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxx 4 | 5 | # Your authentication token 6 | TWILIO_AUTH_TOKEN=TOKEN 7 | 8 | # Your phone number 9 | TWILIO_NUMBER=+123456789 10 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | extends: google 2 | parserOptions: 3 | ecmaVersion: 6 4 | rules: 5 | require-jsdoc: 0 6 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [master] 9 | paths-ignore: 10 | - "**.md" 11 | pull_request: 12 | branches: [master] 13 | paths-ignore: 14 | - "**.md" 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | 20 | strategy: 21 | matrix: 22 | node-version: [14] 23 | 24 | steps: 25 | - uses: actions/checkout@v2 26 | - name: Use Node.js ${{ matrix.node-version }} 27 | uses: actions/setup-node@v2 28 | with: 29 | node-version: ${{ matrix.node-version }} 30 | cache: "npm" 31 | 32 | - name: Start MongoDB 33 | uses: supercharge/mongodb-github-action@1.6.0 34 | with: 35 | mongodb-version: 5 36 | 37 | - name: Create env file 38 | run: cp .env.example .env 39 | 40 | - name: Install Dependencies 41 | run: npm install 42 | - run: npm run build --if-present 43 | - run: npm test 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .DS_Store 3 | npm-debug.log 4 | node_modules 5 | *.log 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Twilio Inc. 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 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node index.js -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Twilio 3 | 4 | 5 | 6 | # SMS Notifications for Node.js and Express 7 | 8 | [![Node.js CI](https://github.com/TwilioDevEd/marketing-notifications-node/actions/workflows/node.js.yml/badge.svg)](https://github.com/TwilioDevEd/marketing-notifications-node/actions/workflows/node.js.yml) 9 | 10 | This example project demonstrates how to send SMS notifications (for a mobile marketing campaign) using Node.js and Express. 11 | 12 | [Read the full tutorial here](https://www.twilio.com/docs/tutorials/walkthrough/marketing-notifications/node/express)! 13 | 14 | 15 | # Running the Project in production 16 | 17 | ## 1. Deploy your App 18 | 19 | ## 2. Configure your Twilio number 20 | 21 | Go to your dashboard on [Twilio](https://www.twilio.com/console/phone-numbers/incoming). Click on Twilio Numbers and choose a number to setup. 22 | 23 | On the phone number page, enter `https:///message` into the _Messaging_ Request URL field. 24 | 25 | [Learn how to configure a Twilio phone number for Programmable SMS](https://support.twilio.com/hc/en-us/articles/223136047-Configure-a-Twilio-Phone-Number-to-Receive-and-Respond-to-Messages) 26 | 27 | ## 3. Wrap Up! 28 | 29 | Now your subscribers will be able to text your new Twilio number to 'Subscribe' to your Marketing Notifications line. 30 | 31 | Congratulations! 32 | 33 | # Running the Project on Your Machine 34 | 35 | To run this project on your computer, download or clone the source. You will also need to download and install: 36 | * [Node.js](https://nodejs.org) or [io.js](https://iojs.org/en/index.html) 37 | * [npm](https://www.npmjs.com) 38 | 39 | Finally you will also need to [sign up for a Twilio account](https://www.twilio.com/try-twilio) if you don't have one already. 40 | 41 | ## 1. Install Dependencies 42 | 43 | Navigate to the project directory in your terminal 44 | 45 | ```bash 46 | npm install 47 | ``` 48 | 49 | This should install all of our project dependencies from npm into a local `node_modules` folder. 50 | 51 | ## 2. Copy the sample configuration file and edit it to match your configuration 52 | 53 | ```bash 54 | cp .env.example .env 55 | ``` 56 | 57 | ## 3. Configuration 58 | 59 | Next, open `config.js` at the root of the project and update it with values from your environment and your [Twilio account](https://www.twilio.com/console/voice/dashboard). You can either export these values as system environment variables (this is the default setup), or you can replace these values with hard-coded strings (be careful you don't commit them to git!). 60 | 61 | This sample application stores data in a MongoDB database using [Mongoose](http://mongoosejs.com). 62 | 63 | You can download and run MongoDB yourself 64 | * [OS X](https://docs.mongodb.org/manual/tutorial/install-mongodb-on-os-x/) 65 | * [Linux](https://docs.mongodb.org/manual/tutorial/install-mongodb-on-ubuntu/) 66 | * [Windows](https://docs.mongodb.org/manual/tutorial/install-mongodb-on-windows/) 67 | * You can also use a hosted service like [compose.io](https://www.compose.io/) 68 | 69 | Our application will be looking for a fully qualified MongoDB connection string with a username and password embedded in it. 70 | 71 | ## 4. Running the Project 72 | 73 | To launch the application, you can use `node .` in the project's root directory. You might also consider using [nodemon](https://github.com/remy/nodemon) for this. It works just like the node command, but automatically restarts your application when you change any source code files. 74 | 75 | ```bash 76 | npm install -g nodemon \ 77 | nodemon . 78 | ``` 79 | 80 | ## 5. Exposing Webhooks to Twilio 81 | 82 | You will likely need to expose your local Node.js web application on the public Internet to work with Twilio. We recommend using [ngrok](https://ngrok.com/docs) to accomplish this. Use ngrok to expose a local port and get a publicly accessible URL you can use to accept incoming calls or texts to your Twilio numbers. 83 | 84 | The following example would expose your local Node application running on port 3000 at `http://chunky-danger-monkey.ngrok.com` (reserved subdomains are a paid feature of ngrok): 85 | 86 | ```bash 87 | ngrok -subdomain=chunky-danger-monkey 3000 88 | ``` 89 | 90 | ## License 91 | 92 | MIT 93 | 94 | ## Meta 95 | 96 | * No warranty expressed or implied. Software is as is. Diggity. 97 | * [MIT License](http://www.opensource.org/licenses/mit-license.html) 98 | * Lovingly crafted by Twilio Developer Education. 99 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Marketing Notifications", 3 | "description": "An example application demonstrating SMS notifications for mobile marketing", 4 | "keywords": [ 5 | "twilio", 6 | "node.js", 7 | "mongodb", 8 | "mongoose", 9 | "express" 10 | ], 11 | "env": { 12 | "TWILIO_ACCOUNT_SID": { 13 | "description": "API username - find your account SID at https://www.twilio.com/user/account/voice-messaging", 14 | "required": true 15 | }, 16 | "TWILIO_AUTH_TOKEN": { 17 | "description": "API password - find your auth token at https://www.twilio.com/user/account/voice-messaging", 18 | "required": true 19 | }, 20 | "TWILIO_NUMBER": { 21 | "description": "A number in your Twilio account to use with this application in E.164 format (e.g. +16518675309) - view your numbers at https://www.twilio.com/user/account/phone-numbers/incoming", 22 | "required": true 23 | } 24 | }, 25 | "website": "https://github.com/TwilioDevEd/marketing-notifications-node", 26 | "repository": "https://github.com/TwilioDevEd/marketing-notifications-node", 27 | "logo": "https://s3.amazonaws.com/howtodocs/twilio-logo.png", 28 | "success_url": "/landing.html", 29 | "addons": [ 30 | "mongolab" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | require('dotenv-safe').load(); 2 | 3 | const cfg = {}; 4 | 5 | // HTTP Port to run our web application 6 | cfg.port = process.env.PORT || 3000; 7 | 8 | // A random string that will help generate secure one-time passwords and 9 | // HTTP sessions 10 | cfg.secret = process.env.APP_SECRET || 'keyboard cat'; 11 | 12 | // Your Twilio account SID and auth token, both found at: 13 | // https://www.twilio.com/user/account 14 | // 15 | // A good practice is to store these string values as system environment 16 | // variables, and load them from there as we are doing below. Alternately, 17 | // you could hard code these values here as strings. 18 | cfg.accountSid = process.env.TWILIO_ACCOUNT_SID; 19 | cfg.authToken = process.env.TWILIO_AUTH_TOKEN; 20 | 21 | // A Twilio number you control - choose one from: 22 | // https://www.twilio.com/user/account/phone-numbers/incoming 23 | // Specify in E.164 format, e.g. "+16519998877" 24 | cfg.twilioNumber = process.env.TWILIO_NUMBER; 25 | 26 | // MongoDB connection string - MONGO_URL is for local dev, 27 | // MONGOLAB_URI is for the MongoLab add-on for Heroku deployment 28 | cfg.mongoUrl = process.env.MONGOLAB_URI || process.env.MONGO_URL || 'mongodb://localhost:27017'; // default 29 | 30 | // MongoDB connection string for test purposes 31 | cfg.mongoUrlTest = 'mongodb://localhost:8000'; 32 | 33 | // Export configuration object 34 | module.exports = cfg; 35 | -------------------------------------------------------------------------------- /controllers/message.js: -------------------------------------------------------------------------------- 1 | const Subscriber = require('../models/subscriber'); 2 | const messageSender = require('../lib/messageSender'); 3 | 4 | // Create a function to handle Twilio SMS / MMS webhook requests 5 | exports.webhook = function(request, response) { 6 | // Get the user's phone number 7 | const phone = request.body.From; 8 | 9 | // Try to find a subscriber with the given phone number 10 | Subscriber.findOne({ 11 | phone: phone, 12 | }, function(err, sub) { 13 | if (err) return respond('Derp! Please text back again later.'); 14 | 15 | if (!sub) { 16 | // If there's no subscriber associated with this phone number, 17 | // create one 18 | const newSubscriber = new Subscriber({ 19 | phone: phone, 20 | }); 21 | 22 | newSubscriber.save(function(err, newSub) { 23 | if (err || !newSub) 24 | return respond('We couldn\'t sign you up - try again.'); 25 | 26 | // We're signed up but not subscribed - prompt to subscribe 27 | respond('Thanks for contacting us! Text "subscribe" to ' + 28 | 'receive updates via text message.'); 29 | }); 30 | } else { 31 | // For an existing user, process any input message they sent and 32 | // send back an appropriate message 33 | processMessage(sub); 34 | } 35 | }); 36 | 37 | // Process any message the user sent to us 38 | function processMessage(subscriber) { 39 | // get the text message command sent by the user 40 | let msg = request.body.Body || ''; 41 | msg = msg.toLowerCase().trim(); 42 | 43 | // Conditional logic to do different things based on the command from 44 | // the user 45 | if (msg === 'subscribe' || msg === 'unsubscribe') { 46 | // If the user has elected to subscribe for messages, flip the bit 47 | // and indicate that they have done so. 48 | subscriber.subscribed = msg === 'subscribe'; 49 | subscriber.save(function(err) { 50 | if (err) 51 | return respond('We could not subscribe you - please try ' 52 | + 'again.'); 53 | 54 | // Otherwise, our subscription has been updated 55 | let responseMessage = 'You are now subscribed for updates.'; 56 | if (!subscriber.subscribed) 57 | responseMessage = 'You have unsubscribed. Text "subscribe"' 58 | + ' to start receiving updates again.'; 59 | 60 | respond(responseMessage); 61 | }); 62 | } else { 63 | // If we don't recognize the command, text back with the list of 64 | // available commands 65 | const responseMessage = 'Sorry, we didn\'t understand that. ' 66 | + 'available commands are: subscribe or unsubscribe'; 67 | 68 | respond(responseMessage); 69 | } 70 | } 71 | 72 | // Set Content-Type response header and render XML (TwiML) response in a 73 | // Jade template - sends a text message back to user 74 | function respond(message) { 75 | response.type('text/xml'); 76 | response.render('twiml', { 77 | message: message, 78 | }); 79 | } 80 | }; 81 | 82 | // Handle form submission 83 | exports.sendMessages = function(request, response) { 84 | // Get message info from form submission 85 | const message = request.body.message; 86 | const imageUrl = request.body.imageUrl; 87 | 88 | // Send messages to all subscribers 89 | Subscriber.find({ 90 | subscribed: true, 91 | }).then((subscribers) => { 92 | messageSender.sendMessageToSubscribers(subscribers, message, imageUrl); 93 | }).then(() => { 94 | request.flash('successes', 'Messages on their way!'); 95 | response.redirect('/'); 96 | }).catch((err) => { 97 | console.log('err ' + err.message); 98 | request.flash('errors', err.message); 99 | response.redirect('/'); 100 | }); 101 | }; 102 | -------------------------------------------------------------------------------- /controllers/pages.js: -------------------------------------------------------------------------------- 1 | // Render a form to send an MMS message 2 | exports.showForm = function(request, response) { 3 | // Render form, with any success or error flash messages 4 | response.render('index', { 5 | errors: request.flash('errors'), 6 | successes: request.flash('successes'), 7 | }); 8 | }; 9 | -------------------------------------------------------------------------------- /controllers/router.js: -------------------------------------------------------------------------------- 1 | const pages = require('./pages'); 2 | const message = require('./message'); 3 | 4 | // Map routes to controller functions 5 | module.exports = function(app) { 6 | // Twilio SMS webhook route 7 | app.post('/message', message.webhook); 8 | 9 | // Render a page that will allow an administrator to send out a message 10 | // to all subscribers 11 | app.get('/', pages.showForm); 12 | 13 | // Handle form submission and send messages to subscribers 14 | app.post('/message/send', message.sendMessages); 15 | }; 16 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const mongoose = require('mongoose'); 3 | const config = require('./config'); 4 | 5 | // Initialize database connection - throws if database connection can't be 6 | // established 7 | mongoose.connect(config.mongoUrl); 8 | mongoose.Promise = Promise; 9 | 10 | // Create Express web app 11 | const app = require('./webapp'); 12 | 13 | // Create an HTTP server and listen on the configured port 14 | const server = http.createServer(app); 15 | server.listen(config.port, function() { 16 | console.log('Express server listening on *:' + config.port); 17 | }); 18 | -------------------------------------------------------------------------------- /lib/messageSender.js: -------------------------------------------------------------------------------- 1 | const twilio = require('twilio'); 2 | const config = require('../config'); 3 | 4 | // create an authenticated Twilio REST API client 5 | const client = twilio(config.accountSid, config.authToken); 6 | 7 | const sendSingleTwilioMessage = function(subscriber, message, url) { 8 | // Create options to send the message 9 | const options = { 10 | to: subscriber.phone, 11 | from: config.twilioNumber, 12 | body: message, 13 | }; 14 | 15 | // Include media URL if one was given for MMS 16 | if (url) options.mediaUrl = url; 17 | 18 | return new Promise((resolve, reject) => { 19 | // Send the message! 20 | client.messages.create(options) 21 | .then((message) => { 22 | console.log(message); 23 | resolve(message); 24 | }) 25 | .catch((error) => { 26 | console.log(error); 27 | reject(error); 28 | }); 29 | }); 30 | }; 31 | 32 | // Function to send a message to all current subscribers 33 | const sendMessageToSubscribers = function(subscribers, message, url) { 34 | // Find all subscribed users 35 | return new Promise((resolve, reject) => { 36 | if (subscribers.length == 0) { 37 | reject({message: 'Could not find any subscribers!'}); 38 | } else { 39 | // Send messages to all subscribers via Twilio 40 | subscribers.map((subscriber) => { 41 | return sendSingleTwilioMessage(subscriber, message, url); 42 | }).reduce((all, currentPromise) => { 43 | return Promise.all([all, currentPromise]); 44 | }, Promise.resolve()).then(() => { 45 | resolve(); 46 | }); 47 | } 48 | }); 49 | }; 50 | 51 | module.exports.sendMessageToSubscribers = sendMessageToSubscribers; 52 | -------------------------------------------------------------------------------- /models/subscriber.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const SubscriberSchema = new mongoose.Schema({ 4 | phone: String, 5 | subscribed: { 6 | type: Boolean, 7 | default: false, 8 | }, 9 | }); 10 | 11 | const Subscriber = mongoose.model('Subscriber', SubscriberSchema); 12 | module.exports = Subscriber; 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sms-notifications-node", 3 | "version": "1.0.0", 4 | "description": "An example application demonstrating SMS notifications for mobile marketing", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "test": "NODE_ENV=test && ./node_modules/.bin/eslint . && node_modules/mocha/bin/mocha --recursive" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/TwilioDevEd/sms-notifications-node" 13 | }, 14 | "keywords": [ 15 | "twilio", 16 | "express", 17 | "mongodb", 18 | "twiml", 19 | "webhooks", 20 | "voip" 21 | ], 22 | "author": "Kevin Whinnery", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/TwilioDevEd/sms-notifications-node/issues" 26 | }, 27 | "homepage": "https://github.com/TwilioDevEd/sms-notifications-node", 28 | "engines": { 29 | "node": ">=6.x" 30 | }, 31 | "dependencies": { 32 | "body-parser": "^1.17.1", 33 | "connect-flash": "^0.1.1", 34 | "dotenv-safe": "^4.0.4", 35 | "express": "^4.15.2", 36 | "express-session": "^1.15.2", 37 | "http-auth": "^3.1.3", 38 | "jade": "^1.11.0", 39 | "mongoose": "^4.9.7", 40 | "morgan": "^1.8.1", 41 | "twilio": "~3.0.0-rc.16" 42 | }, 43 | "devDependencies": { 44 | "chai": "^3.5.0", 45 | "eslint": "^3.19.0", 46 | "eslint-config-google": "^0.7.1", 47 | "mocha": "^3.1.2", 48 | "proxyquire": "^1.7.11", 49 | "sinon": "^2.2.0", 50 | "supertest": "^3.0.0" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | HTTP Error 404: Page Not Found 5 | 6 | 7 |

HTTP Error 404: Page Not Found

8 | 9 | -------------------------------------------------------------------------------- /public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | HTTP Error 500: Internal Server Error 5 | 6 | 7 |

HTTP Error 500: Internal Server Error

8 | 9 | -------------------------------------------------------------------------------- /public/css/main.css: -------------------------------------------------------------------------------- 1 | #main { 2 | padding-top:70px; 3 | } 4 | 5 | .demo { 6 | background-color:#eee; 7 | border:1px solid #ccc; 8 | border-radius:5px; 9 | padding:10px; 10 | } 11 | 12 | footer { 13 | border-top:1px solid #eee; 14 | padding:20px; 15 | margin:20px; 16 | text-align:center; 17 | } 18 | 19 | footer i { 20 | color:red; 21 | } 22 | 23 | .box { 24 | border-radius: 5px; 25 | } 26 | 27 | .box p { 28 | padding:10px; 29 | } -------------------------------------------------------------------------------- /public/landing.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | SMS Marketing Notifications Tutorial 10 | 19 | 20 | 87 | 88 | 89 |
90 | 119 | 120 | 121 | 125 | 126 | 127 | 139 | 140 | 141 | 142 | 143 | 162 | 163 | -------------------------------------------------------------------------------- /test/connectionHelper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const mongoose = require('mongoose'); 4 | const cfg = require('../config'); 5 | 6 | 7 | if (!cfg.mongoUrl) { 8 | throw new Error('MONGO_URL env variable not set.'); 9 | } 10 | 11 | const connect = () => { 12 | return mongoose.connect(cfg.mongoUrl); 13 | }; 14 | 15 | const disconnect = () => { 16 | return mongoose.connection.close(); 17 | }; 18 | 19 | exports.connect = connect; 20 | exports.disconnect = disconnect; 21 | mongoose.Promise = Promise; 22 | -------------------------------------------------------------------------------- /test/controllers/message.test.js: -------------------------------------------------------------------------------- 1 | const connectionHelper = require('../connectionHelper'); 2 | const expect = require('chai').expect; 3 | const supertest = require('supertest'); 4 | const proxyquire = require('proxyquire'); 5 | const sinon = require('sinon'); 6 | const Subscriber = require('../../models/subscriber'); 7 | 8 | describe.only('messages', () => { 9 | const sendMessage = sinon.stub(); 10 | const agent = getAgent(sendMessage); 11 | let subscriber = {}; 12 | 13 | before(() => { 14 | return connectionHelper.connect(); 15 | }); 16 | 17 | beforeEach(() => { 18 | return Subscriber.remove({}); 19 | }); 20 | 21 | after(() => { 22 | return connectionHelper.disconnect(); 23 | }); 24 | 25 | describe('/message/send', () => { 26 | it('sends a message', () => { 27 | sendMessage.returns(Promise.resolve()); 28 | return agent 29 | .post('/message/send') 30 | .send({message: 'Hey!', imageUrl: 'myimage'}) 31 | .expect((response) => { 32 | expect(response.statusCode).to.be.equal(302); 33 | expect(response.header.location).to.be.equal('/'); 34 | expect(sendMessage.calledOnce).to.be.true; 35 | }); 36 | }); 37 | }); 38 | 39 | describe('/message', () => { 40 | it('it handles subscribe message for existing customer', () => { 41 | subscriber = new Subscriber({ 42 | phone: 'phone', 43 | subscribed: false, 44 | }); 45 | return subscriber.save().then(() => { 46 | return agent 47 | .post('/message') 48 | .type('form') 49 | .send('From=phone') 50 | .send('Body=subscribe') 51 | .then((response) => { 52 | expect(response.text).to.contain( 53 | 'You are now subscribed for updates.'); 54 | expect(response.header['content-type']).to.be.equal( 55 | 'text/xml; charset=utf-8'); 56 | expect(response.statusCode).to.be.equal(200); 57 | return Subscriber.findOne({phone: 'phone'}); 58 | }).then((subscriber) => { 59 | expect(subscriber.subscribed).to.be.true; 60 | }); 61 | }); 62 | }); 63 | 64 | it('it handles unsubscribe message for existing customer', () => { 65 | subscriber = new Subscriber({ 66 | phone: 'phone', 67 | subscribed: true, 68 | }); 69 | return subscriber.save().then(() => { 70 | return agent 71 | .post('/message') 72 | .type('form') 73 | .send('From=phone') 74 | .send('Body=unsubscribe') 75 | .then((response) => { 76 | expect(response.text).to.contain( 77 | 'You have unsubscribed. Text "subscribe" to start ' + 78 | 'receiving updates again.'); 79 | expect(response.header['content-type']).to.be.equal( 80 | 'text/xml; charset=utf-8'); 81 | expect(response.statusCode).to.be.equal(200); 82 | return Subscriber.findOne({phone: 'phone'}); 83 | }).then((subscriber) => { 84 | expect(subscriber.subscribed).to.be.false; 85 | }); 86 | }); 87 | }); 88 | 89 | it('it handles message for new customer', () => { 90 | return subscriber.save().then(() => { 91 | return agent 92 | .post('/message') 93 | .type('form') 94 | .send('From=phone') 95 | .then((response) => { 96 | expect(response.text).to.contain( 97 | 'Thanks for contacting us! Text "subscribe" to ' + 98 | 'receive updates via text message.'); 99 | expect(response.header['content-type']).to.be.equal( 100 | 'text/xml; charset=utf-8'); 101 | expect(response.statusCode).to.be.equal(200); 102 | return Subscriber.findOne({phone: 'phone'}); 103 | }).then((subscriber) => { 104 | expect(subscriber.subscribed).to.be.false; 105 | }); 106 | }); 107 | }); 108 | }); 109 | }); 110 | 111 | function getAgent(sendMessage) { 112 | const messageController = proxyquire('../../controllers/message', { 113 | '../lib/messageSender': { 114 | sendMessageToSubscribers: sendMessage, 115 | }, 116 | }); 117 | 118 | const router = proxyquire('../../controllers/router', { 119 | './message': messageController, 120 | }); 121 | 122 | const webapp = proxyquire('../../webapp', { 123 | 'controllers/router': router, 124 | }); 125 | 126 | return supertest(webapp); 127 | } 128 | -------------------------------------------------------------------------------- /test/lib/messageSender.test.js: -------------------------------------------------------------------------------- 1 | const connectionHelper = require('../connectionHelper'); 2 | const expect = require('chai').expect; 3 | const sinon = require('sinon'); 4 | const proxyquire = require('proxyquire'); 5 | const Subscriber = require('../../models/subscriber'); 6 | 7 | describe('subscriber', function() { 8 | const createStub = sinon.stub(); 9 | const twilioClient = { 10 | messages: {create: createStub}, 11 | }; 12 | let messageSender = {}; 13 | 14 | before(() => { 15 | return connectionHelper.connect().then(() => { 16 | messageSender = proxyquire('../../lib/messageSender', { 17 | 'twilio': () => { 18 | return twilioClient; 19 | }, 20 | }); 21 | }); 22 | }); 23 | 24 | after(() => { 25 | return connectionHelper.disconnect(); 26 | }); 27 | 28 | it('sends a message to subscribers', function() { 29 | // given 30 | const subscribers = [ 31 | new Subscriber({phone: 'phone1', subscribed: true}), 32 | new Subscriber({phone: 'phone2', subscribed: true})]; 33 | 34 | createStub.returns(Promise.resolve('message')); 35 | 36 | // when 37 | return messageSender.sendMessageToSubscribers(subscribers, 'message', 'url') 38 | .then(() => { 39 | // then 40 | expect(createStub.calledTwice).to.be.true; 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /views/index.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1 SMS Notifications 5 | 6 | p. 7 | Use this form to send MMS notifications to any subscribers. 8 | 9 | form(action='/message/send', method='POST') 10 | 11 | // The text of the message to send 12 | .form-group 13 | label(for='message') Enter a message 14 | input.form-control(type='text', name='message', 15 | placeholder='Hi there, gorgeous ;)') 16 | 17 | // An optional image URL 18 | .form-group 19 | label(for='imageUrl') (Optional) Image URL to send in an MMS 20 | input.form-control(type='text', name='imageUrl', 21 | placeholder='http://fake.twilio.com/some_image.png') 22 | 23 | button.btn.btn-primary(type='submit') Send Yourself a Message! -------------------------------------------------------------------------------- /views/layout.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang='en') 3 | head 4 | title= title || 'SMS Notifications for Node.js and Express' 5 | 6 | // Twilio shortcut icons 7 | link(rel='shortcut icon', 8 | href='//twilio.com/bundles/marketing/img/favicons/favicon.ico') 9 | link(rel='apple-touch-icon', 10 | href='//twilio.com/bundles/marketing/img/favicons/favicon.ico') 11 | link(rel='apple-touch-icon', sizes='72x72', 12 | href='//twilio.com/bundles/marketing/img/favicons/favicon.ico') 13 | link(rel='apple-touch-icon' sizes='114x114' 14 | href='//twilio.com/bundles/marketing/img/favicons/favicon.ico') 15 | 16 | // Include Font Awesome Icons 17 | link(rel='stylesheet', 18 | href='//maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css') 19 | 20 | // Twitter Bootstrap included for some basic styling 21 | link(rel='stylesheet', 22 | href='//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css') 23 | link(rel='stylesheet', 24 | href='//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap-theme.min.css') 25 | 26 | // HTML 5 shims for older IE 27 | 31 | 32 | link(rel='stylesheet', href='/css/main.css') 33 | 34 | //- Include any page-specific styles 35 | block styles 36 | 37 | body 38 | // Bootstrap-powered nav bar 39 | nav.navbar.navbar-default.navbar-fixed-top 40 | .container 41 | .navbar-header 42 | 43 | //- Nav bar toggle for mobile 44 | button.navbar-toggle.collapsed(type='button', data-toggle='collapse', 45 | data-target='#navbar', aria-expanded='false', aria-controls='navbar') 46 | i.fa.fa-bars 47 | 48 | a.navbar-brand(href='/') SMS Notifications 49 | 50 | 51 | //- Include page content 52 | #main.container 53 | // Display any error or informational messages 54 | mixin messages(list) 55 | each message in list 56 | p= message 57 | 58 | .message-list 59 | if (errors && errors.length > 0) 60 | .box.bg-danger 61 | +messages(errors) 62 | 63 | if (successes && successes.length > 0) 64 | .box.bg-success 65 | +messages(successes) 66 | 67 | block content 68 | 69 | footer.container. 70 | Made with by your pals 71 | @twilio. 72 | 73 | // Include jQuery and Bootstrap scripts 74 | script(src='//ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js') 75 | script(src='//maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js') 76 | 77 | //- Include any page-specific scripts 78 | block scripts 79 | -------------------------------------------------------------------------------- /views/twiml.jade: -------------------------------------------------------------------------------- 1 | doctype xml 2 | Response 3 | Message= message -------------------------------------------------------------------------------- /webapp.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const express = require('express'); 3 | const bodyParser = require('body-parser'); 4 | const session = require('express-session'); 5 | const flash = require('connect-flash'); 6 | const morgan = require('morgan'); 7 | const config = require('./config'); 8 | 9 | // Create Express web app 10 | const app = express(); 11 | app.set('view engine', 'jade'); 12 | 13 | // Use morgan for HTTP request logging 14 | app.use(morgan('combined')); 15 | 16 | // Serve static assets 17 | app.use(express.static(path.join(__dirname, 'public'))); 18 | 19 | // Parse incoming form-encoded HTTP bodies 20 | app.use(bodyParser.urlencoded({ 21 | extended: true, 22 | })); 23 | 24 | // Create and manage HTTP sessions for all requests 25 | app.use(session({ 26 | secret: config.secret, 27 | resave: true, 28 | saveUninitialized: true, 29 | })); 30 | 31 | // Use connect-flash to persist informational messages across redirects 32 | app.use(flash()); 33 | 34 | // Configure application routes 35 | require('./controllers/router')(app); 36 | 37 | // Handle 404 38 | app.use(function(request, response, next) { 39 | response.status(404); 40 | response.sendFile(path.join(__dirname, 'public', '404.html')); 41 | }); 42 | 43 | // Unhandled errors (500) 44 | app.use(function(err, request, response, next) { 45 | console.error('An application error has occurred:'); 46 | console.error(err); 47 | console.error(err.stack); 48 | response.status(500); 49 | response.sendFile(path.join(__dirname, 'public', '500.html')); 50 | }); 51 | 52 | // Export Express app 53 | module.exports = app; 54 | --------------------------------------------------------------------------------