├── .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 |
3 |
4 |
5 |
6 | # SMS Notifications for Node.js and Express
7 |
8 | [](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 |
--------------------------------------------------------------------------------