├── .env.example ├── .eslintignore ├── .eslintrc.yml ├── .github ├── dependabot.yml └── workflows │ └── node.js.yml ├── .gitignore ├── LICENSE ├── README.md ├── app.js ├── name_generator.js ├── package-lock.json ├── package.json ├── public ├── chat │ ├── index.css │ ├── index.html │ └── index.js ├── config-check.js ├── index.css ├── index.html ├── notify │ ├── facebook_messenger.html │ ├── index.html │ ├── notify.css │ └── notify.js └── sync │ ├── index.css │ ├── index.html │ └── index.js ├── src ├── config.js ├── notification_handler.js ├── router.js ├── sync_service_details.js └── token_generator.js └── test ├── config.test.js └── token_generator.test.js /.env.example: -------------------------------------------------------------------------------- 1 | # Required for all uses 2 | TWILIO_ACCOUNT_SID=ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 3 | TWILIO_API_KEY=SKXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 4 | TWILIO_API_SECRET=YYXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 5 | 6 | # Required for Chat 7 | TWILIO_CHAT_SERVICE_SID=ISXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 8 | 9 | # Required for Notify 10 | TWILIO_NOTIFICATION_SERVICE_SID=ISXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 11 | 12 | # Optional for Sync. By default, the app uses the 'default' instance on your account. 13 | TWILIO_SYNC_SERVICE_SID=ISXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 14 | 15 | NODE_ENV=production 16 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | public/**/*.js 2 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | extends: google 2 | parserOptions: 3 | ecmaVersion: 6 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | ignore: 9 | - dependency-name: y18n 10 | versions: 11 | - 4.0.2 12 | - dependency-name: twilio 13 | versions: 14 | - 3.51.0 15 | - dependency-name: lodash 16 | versions: 17 | - 4.17.20 18 | -------------------------------------------------------------------------------- /.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: ${{ matrix.os }} 19 | strategy: 20 | matrix: 21 | os: [ubuntu-latest, macos-latest] 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: Create env file 33 | run: cp .env.example .env 34 | 35 | - name: Install Dependencies 36 | run: npm install 37 | - run: npm run build --if-present 38 | - run: npm test 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .DS_Store 3 | .idea 4 | node_modules 5 | npm-debug.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Twilio 3 | 4 | 5 | # Twilio SDK Starter Application for Node.js 6 | [![Node.js CI](https://github.com/TwilioDevEd/sdk-starter-node/actions/workflows/node.js.yml/badge.svg)](https://github.com/TwilioDevEd/sdk-starter-node/actions/workflows/node.js.yml) 7 | 8 | This sample project demonstrates how to use Twilio APIs in a Node.js web 9 | application. Once the app is up and running, check out [the home page](http://localhost:3000) 10 | to see which demos you can run. You'll find examples for [Chat](https://www.twilio.com/chat), 11 | [Sync](https://www.twilio.com/sync), and [Notify](https://www.twilio.com/notify). 12 | 13 | Let's get started! 14 | 15 | ## Configure the sample application 16 | 17 | To run the application, you'll need to gather your Twilio account credentials and configure them 18 | in a file named `.env`. To create this file from an example template, do the following in your 19 | Terminal. 20 | 21 | ```bash 22 | cp .env.example .env 23 | ``` 24 | 25 | Open `.env` in your favorite text editor and configure the following values. 26 | 27 | ### Configure account information 28 | 29 | Every sample in the demo requires some basic credentials from your Twilio account. Configure these first. 30 | 31 | | Config Value | Description | 32 | | :------------- |:------------- | 33 | `TWILIO_ACCOUNT_SID` | Your primary Twilio account identifier - find this [in the console here](https://www.twilio.com/console). 34 | `TWILIO_API_KEY` | Used to authenticate - [generate one here](https://www.twilio.com/console/dev-tools/api-keys). 35 | `TWILIO_API_SECRET` | Used to authenticate - [just like the above, you'll get one here](https://www.twilio.com/console/dev-tools/api-keys). 36 | 37 | #### A Note on API Keys 38 | 39 | When you generate an API key pair at the URLs above, your API Secret will only be shown once - 40 | make sure to save this information in a secure location, or possibly your `~/.bash_profile`. 41 | 42 | ### Configure product-specific settings 43 | 44 | Depending on which demos you'd like to run, you may need to configure a few more values in your `.env` file. 45 | 46 | ### Configuring Twilio Sync 47 | 48 | Twilio Sync works out of the box, using default settings per account. Once you have your API keys configured, execute `npm install; npm run start` and [open a browser](http://localhost:3000/sync)! 49 | 50 | ### Configuring Twilio Chat 51 | 52 | In addition to the above, you'll need to [generate a Chat Service](https://www.twilio.com/console/chat/services) in the Twilio Console. Put the result in your `.env` file. 53 | 54 | | Config Value | Where to get one. | 55 | | :------------- |:------------- | 56 | `TWILIO_CHAT_SERVICE_SID` | Chat | [Generate one in the Twilio Chat console](https://www.twilio.com/console/chat/services) 57 | 58 | ### Configuring Twilio Notify 59 | 60 | You will need to create a Notify Service and add at least one credential on the [Mobile Push Credential screen](https://www.twilio.com/console/notify/credentials) (such as Apple Push Notification Service or Firebase Cloud Messaging for Android) to send notifications using Notify. 61 | 62 | | Config Value | Where to get one. | 63 | | :------------- |:------------- | 64 | `TWILIO_NOTIFICATION_SERVICE_SID` | Generate one in the [Notify Console](https://www.twilio.com/console/notify/services) and put this in your `.env` file. 65 | A Push Credential | Generate one with Apple or Google and [configure it as a Notify credential](https://www.twilio.com/console/notify/credentials). 66 | 67 | Once you've done that, run the application and [open a browser](localhost:3000/notify)! 68 | 69 | ## Run the sample application 70 | 71 | Now that the application is configured, we need to install our dependencies from npm. 72 | 73 | ```bash 74 | npm install 75 | ``` 76 | 77 | Now we should be all set! Run the application using the `node` command. 78 | 79 | ```bash 80 | npm start 81 | ``` 82 | 83 | Your application should now be running at [http://localhost:3000/](http://localhost:3000/). 84 | 85 | ![Home Screen](https://cloud.githubusercontent.com/assets/809856/26252870/0bfd80ac-3c77-11e7-9252-2b19dff5d784.png) 86 | 87 | Check your config values, and follow the links to the demo applications! 88 | 89 | ## Running the SDK Starter Kit with ngrok 90 | 91 | If you are going to connect to this SDK Starter Kit with a mobile app (and you should try it out!), your phone won't be able to access localhost directly. You'll need to create a publicly accessible URL using a tool like [ngrok](https://ngrok.com/) to send HTTP/HTTPS traffic to a server running on your localhost. Use HTTPS to make web connections that retrieve a Twilio access token. 92 | 93 | ```bash 94 | ngrok http 3000 95 | ``` 96 | 97 | ## Meta 98 | 99 | * No warranty expressed or implied. Software is as is. Diggity. 100 | * [MIT License](http://www.opensource.org/licenses/mit-license.html) 101 | * Lovingly crafted by Twilio Developer Education. 102 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | // Node/Express 4 | const express = require('express'); 5 | const http = require('http'); 6 | const path = require('path'); 7 | const bodyParser = require('body-parser'); 8 | 9 | const router = require('./src/router'); 10 | const syncServiceDetails = require('./src/sync_service_details'); 11 | 12 | // Create Express webapp 13 | const app = express(); 14 | app.use(express.static(path.join(__dirname, 'public'))); 15 | 16 | // Add body parser for Notify device registration 17 | app.use(bodyParser.urlencoded({extended: true})); 18 | app.use(bodyParser.json()); 19 | 20 | app.use(router); 21 | 22 | // production error handler 23 | // no stacktraces leaked to user 24 | app.use(function(err, req, res, next) { 25 | console.trace(err); 26 | res.status(err.status || 500); 27 | res.send({ 28 | message: err.message, 29 | error: {}, 30 | }); 31 | }); 32 | 33 | // Get Sync Service Details for lazy creation of default service if needed 34 | syncServiceDetails(); 35 | 36 | // Create http server and run it 37 | const server = http.createServer(app); 38 | const port = process.env.PORT || 3000; 39 | server.listen(port, function() { 40 | console.log('Express server running on *:' + port); 41 | }); 42 | 43 | module.exports = app; 44 | -------------------------------------------------------------------------------- /name_generator.js: -------------------------------------------------------------------------------- 1 | const ADJECTIVES = [ 2 | 'Awesome', 'Bold', 'Creative', 'Dapper', 'Eccentric', 'Fiesty', 'Golden', 3 | 'Holy', 'Ignominious', 'Jolly', 'Kindly', 'Lucky', 'Mushy', 'Natural', 4 | 'Oaken', 'Precise', 'Quiet', 'Rowdy', 'Sunny', 'Tall', 5 | 'Unique', 'Vivid', 'Wonderful', 'Xtra', 'Yawning', 'Zesty', 6 | ]; 7 | 8 | const FIRST_NAMES = [ 9 | 'Anna', 'Bobby', 'Cameron', 'Danny', 'Emmett', 'Frida', 'Gracie', 'Hannah', 10 | 'Isaac', 'Jenova', 'Kendra', 'Lando', 'Mufasa', 'Nate', 'Owen', 'Penny', 11 | 'Quincy', 'Roddy', 'Samantha', 'Tammy', 'Ulysses', 'Victoria', 'Wendy', 12 | 'Xander', 'Yolanda', 'Zelda', 13 | ]; 14 | 15 | const LAST_NAMES = [ 16 | 'Anchorage', 'Berlin', 'Cucamonga', 'Davenport', 'Essex', 'Fresno', 17 | 'Gunsight', 'Hanover', 'Indianapolis', 'Jamestown', 'Kane', 'Liberty', 18 | 'Minneapolis', 'Nevis', 'Oakland', 'Portland', 'Quantico', 'Raleigh', 19 | 'SaintPaul', 'Tulsa', 'Utica', 'Vail', 'Warsaw', 'XiaoJin', 'Yale', 20 | 'Zimmerman', 21 | ]; 22 | 23 | const rand = (arr) => arr[Math.floor(Math.random() * arr.length)]; 24 | 25 | module.exports = () => rand(ADJECTIVES) + rand(FIRST_NAMES) + rand(LAST_NAMES); 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sdk-starter-node", 3 | "version": "1.0.0", 4 | "description": "Starter Server Application for Node.js", 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "node app.js", 8 | "test": "NODE_ENV=test && jest" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/TwilioDevEd/sdk-starter-node.git" 13 | }, 14 | "author": "Twilio Developer Education", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/TwilioDevEd/sdk-starter-node/issues" 18 | }, 19 | "homepage": "https://github.com/TwilioDevEd/sdk-starter-node#readme", 20 | "dependencies": { 21 | "body-parser": "^1.19.0", 22 | "camelcase": "^6.2.0", 23 | "dotenv": "^10.0.0", 24 | "express": "^4.17.1", 25 | "extend": "^3.0.2", 26 | "pug": "^3.0.2", 27 | "twilio": "^3.66.0" 28 | }, 29 | "devDependencies": { 30 | "jest": "^27.0.6", 31 | "jest-extended": "^0.11.5", 32 | "jsonwebtoken": "^8.5.1" 33 | }, 34 | "jest": { 35 | "setupFilesAfterEnv": [ 36 | "jest-extended" 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /public/chat/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing:border-box; 3 | } 4 | 5 | html, body { 6 | padding:0; 7 | margin:0; 8 | height:100%; 9 | width:100%; 10 | color:#dedede; 11 | background-color: #849091; 12 | font-family: 'Helvetica Neue', Helvetica, sans-serif; 13 | } 14 | 15 | header { 16 | width:100%; 17 | position:absolute; 18 | text-align:center; 19 | bottom:20px; 20 | } 21 | 22 | header a, header a:visited { 23 | font-size:18px; 24 | color:#dedede; 25 | text-decoration:none; 26 | } 27 | 28 | header a:hover { 29 | text-decoration:underline; 30 | } 31 | 32 | section { 33 | height:70%; 34 | background-color:#2B2B2A; 35 | } 36 | 37 | section input { 38 | display:block; 39 | height:52px; 40 | width:800px; 41 | margin:10px auto; 42 | outline:none; 43 | background-color:transparent; 44 | border:none; 45 | border-bottom:1px solid #2B2B2A; 46 | padding:0; 47 | font-size:42px; 48 | color:#eee; 49 | } 50 | 51 | #messages { 52 | background-color:#232323; 53 | padding:10px; 54 | height:100%; 55 | width:800px; 56 | margin:0 auto; 57 | overflow-y:auto; 58 | } 59 | 60 | #messages p { 61 | margin:5px 0; 62 | padding:0; 63 | } 64 | 65 | .info { 66 | margin:5px 0; 67 | font-style:italic; 68 | } 69 | 70 | .message-container { 71 | margin:5px 0; 72 | color:#fff; 73 | } 74 | 75 | .message-container .username { 76 | display:inline-block; 77 | margin-right:5px; 78 | font-weight:bold; 79 | color:#849091; 80 | } 81 | 82 | .me, .username.me { 83 | font-weight:bold; 84 | color:cyan; 85 | } 86 | 87 | .message-container .username.me { 88 | display:inline-block; 89 | margin-right:5px; 90 | } -------------------------------------------------------------------------------- /public/chat/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Twilio Chat Quickstart 5 | 6 | 7 | 8 | 9 | 10 |
11 | Read the getting started guide 13 | 14 | 15 |
16 | 17 |
18 |
19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /public/chat/index.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | // Get handle to the chat div 3 | var $chatWindow = $('#messages'); 4 | 5 | // Our interface to the Chat service 6 | var chatClient; 7 | 8 | // A handle to the "general" chat channel - the one and only channel we 9 | // will have in this sample app 10 | var generalChannel; 11 | 12 | // The server will assign the client a random username - store that value 13 | // here 14 | var username; 15 | 16 | // Helper function to print info messages to the chat window 17 | function print(infoMessage, asHtml) { 18 | var $msg = $('
'); 19 | if (asHtml) { 20 | $msg.html(infoMessage); 21 | } else { 22 | $msg.text(infoMessage); 23 | } 24 | $chatWindow.append($msg); 25 | } 26 | 27 | // Helper function to print chat message to the chat window 28 | function printMessage(fromUser, message) { 29 | var $user = $('').text(fromUser + ':'); 30 | if (fromUser === username) { 31 | $user.addClass('me'); 32 | } 33 | var $message = $('').text(message); 34 | var $container = $('
'); 35 | $container.append($user).append($message); 36 | $chatWindow.append($container); 37 | $chatWindow.scrollTop($chatWindow[0].scrollHeight); 38 | } 39 | 40 | // Alert the user they have been assigned a random username 41 | print('Logging in...'); 42 | 43 | // Get an access token for the current user, passing a username (identity) 44 | $.getJSON('/token', function(data) { 45 | 46 | 47 | // Initialize the Chat client 48 | Twilio.Chat.Client.create(data.token).then(client => { 49 | console.log('Created chat client'); 50 | chatClient = client; 51 | chatClient.getSubscribedChannels().then(createOrJoinGeneralChannel); 52 | 53 | // when the access token is about to expire, refresh it 54 | chatClient.on('tokenAboutToExpire', function() { 55 | refreshToken(username); 56 | }); 57 | 58 | // if the access token already expired, refresh it 59 | chatClient.on('tokenExpired', function() { 60 | refreshToken(username); 61 | }); 62 | 63 | // Alert the user they have been assigned a random username 64 | username = data.identity; 65 | print('You have been assigned a random username of: ' 66 | + '' + username + '', true); 67 | 68 | }).catch(error => { 69 | console.error(error); 70 | print('There was an error creating the chat client:
' + error, true); 71 | print('Please check your .env file.', false); 72 | }); 73 | }); 74 | 75 | function refreshToken(identity) { 76 | console.log('Token about to expire'); 77 | // Make a secure request to your backend to retrieve a refreshed access token. 78 | // Use an authentication mechanism to prevent token exposure to 3rd parties. 79 | $.getJSON('/token/' + identity, function(data) { 80 | console.log('updated token for chat client'); 81 | chatClient.updateToken(data.token); 82 | }); 83 | } 84 | 85 | function createOrJoinGeneralChannel() { 86 | // Get the general chat channel, which is where all the messages are 87 | // sent in this simple application 88 | print('Attempting to join "general" chat channel...'); 89 | chatClient.getChannelByUniqueName('general') 90 | .then(function(channel) { 91 | generalChannel = channel; 92 | console.log('Found general channel:'); 93 | console.log(generalChannel); 94 | setupChannel(); 95 | }).catch(function() { 96 | // If it doesn't exist, let's create it 97 | console.log('Creating general channel'); 98 | chatClient.createChannel({ 99 | uniqueName: 'general', 100 | friendlyName: 'General Chat Channel' 101 | }).then(function(channel) { 102 | console.log('Created general channel:'); 103 | console.log(channel); 104 | generalChannel = channel; 105 | setupChannel(); 106 | }).catch(function(channel) { 107 | console.log('Channel could not be created:'); 108 | console.log(channel); 109 | }); 110 | }); 111 | } 112 | 113 | // Set up channel after it has been found 114 | function setupChannel() { 115 | // Join the general channel 116 | generalChannel.join().then(function(channel) { 117 | print('Joined channel as ' 118 | + '' + username + '.', true); 119 | }); 120 | 121 | // Listen for new messages sent to the channel 122 | generalChannel.on('messageAdded', function(message) { 123 | printMessage(message.author, message.body); 124 | }); 125 | } 126 | 127 | // Send a new message to the general channel 128 | var $input = $('#chat-input'); 129 | $input.on('keydown', function(e) { 130 | 131 | if (e.keyCode == 13) { 132 | if (generalChannel === undefined) { 133 | print('The Chat Service is not configured. Please check your .env file.', false); 134 | return; 135 | } 136 | generalChannel.sendMessage($input.val()) 137 | $input.val(''); 138 | } 139 | }); 140 | }); 141 | -------------------------------------------------------------------------------- /public/config-check.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | $.get('/config', function(response) { 3 | Object.keys(fields).forEach(configureField(fields, response)); 4 | Object.keys(buttons).forEach(configureButton(buttons, response)); 5 | }); 6 | 7 | // Button Ids' and Config Keys 8 | var buttons = { 9 | chatDemoButton: 'TwilioChatServiceSid', 10 | syncDemoButton: 'TwilioSyncServiceSid', 11 | notifyDemoButton: 'TwilioNotificationServiceSid' 12 | }; 13 | 14 | // Field Ids' and Masked Flag 15 | var fields = { 16 | twilioAccountSid: false, 17 | twilioApiKey: false, 18 | twilioApiSecret: true, 19 | twilioNotificationServiceSid: false, 20 | twilioChatServiceSid: false, 21 | twilioSyncServiceSid: false 22 | }; 23 | 24 | var configureField = function(fields, response) { 25 | var htmlContent = 'Not configured in .env'; 26 | var cssClass = 'unset'; 27 | return function(fieldId) { 28 | var configKey = strToConfig(fieldId); 29 | var isMasked = fields[fieldId]; 30 | if (!!response[configKey]) { 31 | htmlContent = isMasked ? 'Configured properly' : response[configKey]; 32 | cssClass = 'set'; 33 | } 34 | $('#' + fieldId).html(htmlContent).addClass(cssClass); 35 | }; 36 | }; 37 | 38 | var configureButton = function(buttons, response) { 39 | var hasBasicConfig = !!response.TWILIO_ACCOUNT_SID && 40 | !!response.TWILIO_API_KEY && 41 | !!response.TWILIO_API_SECRET; 42 | return function(buttonId) { 43 | var configKey = strToConfig(buttons[buttonId]); 44 | var cssClass = hasBasicConfig && !!response[configKey] 45 | ? 'btn-success' 46 | : 'btn-danger'; 47 | $('#' + buttonId).addClass(cssClass); 48 | }; 49 | }; 50 | 51 | var strToConfig = function(string) { 52 | return string 53 | .split(/(?=[A-Z])/) 54 | .map(function(e) { return e.toUpperCase(); }) 55 | .join('_'); 56 | } 57 | }); 58 | -------------------------------------------------------------------------------- /public/index.css: -------------------------------------------------------------------------------- 1 | .config-value.set { 2 | color:seagreen; 3 | } 4 | 5 | .config-value.unset { 6 | color:darkred; 7 | } -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Twilio Server Starter Kit 5 | 6 | 9 | 10 | 11 | 12 | 13 |
14 |

Twilio Server Starter Kit Environment Setup

15 | 16 |

Account Information

17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
TWILIO_ACCOUNT_SID
TWILIO_API_KEY
TWILIO_API_SECRET
31 | 32 |

Products

33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 |
NotifyTWILIO_NOTIFICATION_SERVICE_SID
ChatTWILIO_CHAT_SERVICE_SID
SyncTWILIO_SYNC_SERVICE_SID
50 | 51 |

Demos

52 | Sync 53 | Notify 54 | Chat 55 |
56 | 57 | 58 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /public/notify/facebook_messenger.html: -------------------------------------------------------------------------------- 1 | 2 | 27 |
32 |
33 | 34 | -------------------------------------------------------------------------------- /public/notify/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Hello App! - Notify Quickstart 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | Read the Twilio Notify guide 13 | 14 |
15 | 16 |
17 |

Send Notification to Identity

18 | 19 | 20 |

21 | 22 | 23 |

Welcome to Notify!
24 | 25 |

After you set up a notification binding, go ahead and send a notification to that identity!

26 |
27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /public/notify/notify.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing:border-box; 3 | } 4 | 5 | html, body { 6 | padding:0; 7 | margin:0; 8 | height:100%; 9 | width:100%; 10 | color:#dedede; 11 | background-color: #849091; 12 | font-family: 'Helvetica Neue', Helvetica, sans-serif; 13 | } 14 | 15 | header { 16 | width:100%; 17 | position:absolute; 18 | text-align:center; 19 | bottom:20px; 20 | } 21 | 22 | header a, header a:visited { 23 | font-size:18px; 24 | color:#dedede; 25 | text-decoration:none; 26 | } 27 | 28 | header a:hover { 29 | text-decoration:underline; 30 | } 31 | 32 | section { 33 | background-color:#2B2B2A; 34 | text-align:center; 35 | padding:16px; 36 | } 37 | 38 | #message { 39 | padding:6px; 40 | } 41 | -------------------------------------------------------------------------------- /public/notify/notify.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | $('#sendNotificationButton').on('click', function() { 3 | $.post('/send-notification', { 4 | identity: $('#identityInput').val(), 5 | body: "Hello, world!" 6 | }, function(response) { 7 | $('#identityInput').val(''); 8 | $('#message').html(response.message); 9 | console.log(response); 10 | }); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /public/sync/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing:border-box; 3 | } 4 | 5 | html, body { 6 | padding:0; 7 | margin:0; 8 | height:100%; 9 | width:100%; 10 | color:#dedede; 11 | background-color: #849091; 12 | font-family: 'Helvetica Neue', Helvetica, sans-serif; 13 | } 14 | 15 | header { 16 | width:100%; 17 | position:absolute; 18 | text-align:center; 19 | bottom:20px; 20 | } 21 | 22 | header a, header a:visited { 23 | font-size:18px; 24 | color:#dedede; 25 | text-decoration:none; 26 | } 27 | 28 | header a:hover { 29 | text-decoration:underline; 30 | } 31 | 32 | section { 33 | background-color:#2B2B2A; 34 | text-align:center; 35 | padding:16px; 36 | } 37 | 38 | button:hover { 39 | cursor:pointer; 40 | background-color:#000; 41 | color:#fff; 42 | } 43 | 44 | #message { 45 | padding:6px; 46 | } 47 | 48 | #board { 49 | width: 33%; 50 | margin-left: auto; 51 | margin-right: auto; 52 | } 53 | 54 | #board .board-row { 55 | width: 100%; 56 | padding-bottom: 3px; 57 | } 58 | 59 | #board .board-row button { 60 | width: 30%; 61 | height: 100px; 62 | font-size: 50px; 63 | } -------------------------------------------------------------------------------- /public/sync/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Tic-Tac-Twilio - Sync Quickstart 5 | 6 | 7 | 8 | 9 | 10 |
11 | 13 | Read the getting started guide 14 | 15 | 16 |
17 | 18 |
19 |

Tic-Tac-Twilio

20 | 21 |
22 |
23 | 24 | 25 | 26 |
27 |
28 | 29 | 30 | 31 |
32 |
33 | 34 | 35 | 36 |
37 |
38 | 39 |
40 | Welcome! Initializing Sync... 41 |
42 | 43 |

Open this page in a few tabs to test!

44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /public/sync/index.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | //We'll use message to tell the user what's happening 3 | var $message = $('#message'); 4 | 5 | //Get handle to the game board buttons 6 | var $buttons = $('#board .board-row button'); 7 | 8 | //Our interface to the Sync service 9 | var syncClient; 10 | 11 | // Every browser Sync client relies on FPA tokens to authenticate and authorize it 12 | // for access to your Sync data. 13 | // 14 | // In this quickstart, we're using our own token generator. You can generate a token 15 | // in any backend language that Twilio supports, or generate a Twilio Function so 16 | // Twilio can host it for you. See the docs for more details. 17 | // 18 | $.getJSON('/token', function (tokenResponse) { 19 | 20 | // Once we have the token, we can initialize the Sync client and start subscribing 21 | // to data. The client will initialize asynchronously while we load the rest of 22 | // the user interface. 23 | // 24 | syncClient = new Twilio.Sync.Client(tokenResponse.token, { logLevel: 'info' }); 25 | syncClient.on('connectionStateChanged', function(state) { 26 | if (state != 'connected') { 27 | $message.html('Sync is not live (websocket connection ' + state + ')…'); 28 | } else { 29 | // Now that we're connected, lets light up our board and play! 30 | $buttons.attr('disabled', false); 31 | $message.html('Sync is live!'); 32 | } 33 | }); 34 | 35 | // Let's pop a message on the screen to show that Sync is working 36 | $message.html('Loading board data…'); 37 | 38 | // Our game state is stored in a Sync document. Here, we'll attach to that document 39 | // (or create it, if it doesn't exist) and connect the necessary event handlers. 40 | // 41 | syncClient.document('SyncGame').then(function(syncDoc) { 42 | var data = syncDoc.data; 43 | if (data.board) { 44 | updateUserInterface(data); 45 | } 46 | 47 | // Any time the board changes, we want to show the new state. The 'updated' 48 | // event is for this. 49 | syncDoc.on('updated', function(event) { 50 | console.debug("Board was updated", event.isLocal? "locally." : "by the other guy."); 51 | updateUserInterface(event.data); 52 | }); 53 | 54 | // Let's make our buttons control the game state in Sync… 55 | $buttons.on('click', function (e) { 56 | // Toggle the value: X, O, or empty 57 | toggleCellValue($(e.target)); 58 | 59 | // Send updated document to Sync. This will trigger "updated" events for all players. 60 | var data = readGameBoardFromUserInterface(); 61 | syncDoc.set(data); 62 | }); 63 | }); 64 | 65 | }); 66 | 67 | //Toggle the value: X, O, or empty (  for UI) 68 | function toggleCellValue($cell) { 69 | var cellValue = $cell.html(); 70 | 71 | if (cellValue === 'X') { 72 | $cell.html('O'); 73 | } else if (cellValue === 'O') { 74 | $cell.html(' '); 75 | } else { 76 | $cell.html('X'); 77 | } 78 | } 79 | 80 | //Read the state of the UI and create a new document 81 | function readGameBoardFromUserInterface() { 82 | var board = [ 83 | ['', '', ''], 84 | ['', '', ''], 85 | ['', '', ''] 86 | ]; 87 | 88 | for (var row = 0; row < 3; row++) { 89 | for (var col = 0; col < 3; col++) { 90 | var selector = '[data-row="' + row + '"]' + 91 | '[data-col="' + col + '"]'; 92 | board[row][col] = $(selector).html().replace(' ', ''); 93 | } 94 | } 95 | 96 | return {board: board}; 97 | } 98 | 99 | //Update the buttons on the board to match our document 100 | function updateUserInterface(data) { 101 | for (var row = 0; row < 3; row++) { 102 | for (var col = 0; col < 3; col++) { 103 | var this_cell = '[data-row="' + row + '"]' + '[data-col="' + col + '"]'; 104 | var cellValue = data.board[row][col]; 105 | $(this_cell).html(cellValue === '' ? ' ' : cellValue); 106 | } 107 | } 108 | } 109 | }); 110 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | module.exports = { 4 | TWILIO_ACCOUNT_SID: process.env.TWILIO_ACCOUNT_SID, 5 | TWILIO_API_KEY: process.env.TWILIO_API_KEY, 6 | TWILIO_API_SECRET: process.env.TWILIO_API_SECRET, 7 | TWILIO_CHAT_SERVICE_SID: process.env.TWILIO_CHAT_SERVICE_SID, 8 | TWILIO_NOTIFICATION_SERVICE_SID: process.env.TWILIO_NOTIFICATION_SERVICE_SID, 9 | TWILIO_SYNC_SERVICE_SID: process.env.TWILIO_SYNC_SERVICE_SID || 'default' 10 | } 11 | -------------------------------------------------------------------------------- /src/notification_handler.js: -------------------------------------------------------------------------------- 1 | const Twilio = require('twilio'); 2 | 3 | const config = require('./config'); 4 | 5 | /** 6 | * Create a device binding from a POST HTTP request 7 | * 8 | * @param {Object} binding 9 | * 10 | * @return {Promise} 11 | * {Object.status} 12 | * {Object.data} 13 | * {Object.message} 14 | */ 15 | exports.registerBind = function registerBind(binding) { 16 | const service = getTwilioClient(); 17 | 18 | return service.bindings.create(binding).then((binding) => { 19 | console.log(binding); 20 | // Send a JSON response indicating success 21 | return { 22 | status: 200, 23 | data: {message: 'Binding created!'}, 24 | }; 25 | }).catch((error) => { 26 | console.log(error); 27 | return { 28 | status: 500, 29 | data: { 30 | error: error, 31 | message: 'Failed to create binding: ' + error, 32 | }, 33 | }; 34 | }); 35 | }; 36 | 37 | // Notify - send a notification from a POST HTTP request 38 | exports.sendNotification = function sendNotification(notification) { 39 | // Create a reference to the user notification service 40 | const service = getTwilioClient(); 41 | 42 | // Send a notification 43 | return service.notifications.create(notification).then((message) => { 44 | console.log(message); 45 | return { 46 | status: 200, 47 | data: {message: 'Successful sending notification'}, 48 | }; 49 | }).catch((error) => { 50 | console.log(error); 51 | return { 52 | status: 500, 53 | data: {error: error}, 54 | }; 55 | }); 56 | }; 57 | 58 | function getTwilioClient() { 59 | // Twilio Library 60 | const client = new Twilio( 61 | config.TWILIO_API_KEY, 62 | config.TWILIO_API_SECRET, 63 | {accountSid: config.TWILIO_ACCOUNT_SID} 64 | ); 65 | 66 | // Get a reference to the user notification service instance 67 | const service = client.notify.services( 68 | config.TWILIO_NOTIFICATION_SERVICE_SID 69 | ); 70 | 71 | return service; 72 | } 73 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | const Router = require('express').Router; 2 | 3 | const {registerBind, sendNotification} = require('./notification_handler'); 4 | const tokenGenerator = require('./token_generator'); 5 | const config = require('./config'); 6 | 7 | const router = new Router(); 8 | 9 | // Convert keys to camelCase to conform with the twilio-node api definition contract 10 | const camelCase = require('camelcase'); 11 | function camelCaseKeys(hashmap) { 12 | var newhashmap = {}; 13 | Object.keys(hashmap).forEach(function(key) { 14 | var newkey = camelCase(key); 15 | newhashmap[newkey] = hashmap[key]; 16 | }); 17 | return newhashmap; 18 | }; 19 | 20 | router.get('/token/:id?', (req, res) => { 21 | const id = req.params.id; 22 | res.send(tokenGenerator(id)); 23 | }); 24 | 25 | router.post('/token', (req, res) => { 26 | const id = req.body.id; 27 | res.send(tokenGenerator(id)); 28 | }); 29 | 30 | router.post('/register', (req, res) => { 31 | var content = camelCaseKeys(req.body); 32 | registerBind(content).then((data) => { 33 | res.header('Access-Control-Allow-Origin', '*'); 34 | res.status(data.status); 35 | res.send(data.data); 36 | }); 37 | }); 38 | 39 | router.post('/send-notification', (req, res) => { 40 | var content = camelCaseKeys(req.body); 41 | sendNotification(content).then((data) => { 42 | res.status(data.status); 43 | res.send(data.data); 44 | }); 45 | }); 46 | 47 | router.get('/config', (req, res) => { 48 | res.json(config); 49 | }); 50 | 51 | 52 | //Create a facebook-messenger binding based on the authentication webhook from Facebook 53 | router.post('/messenger_auth', function(req, res) { 54 | //Extract the request received from Facebook 55 | const message = req.body.entry[0].messaging[0]; 56 | console.log(message); 57 | // Set user identity using their fb messenger user id 58 | const identity = message.sender.id; 59 | //Let's create a new facebook-messenger Binding for our user 60 | const binding = { 61 | "identity":identity, 62 | "BindingType":'facebook-messenger', 63 | "Address":message.sender.id 64 | }; 65 | registerBind(camelCaseKeys(binding)).then((data) => { 66 | res.status(data.status); 67 | res.send(data.data); 68 | }); 69 | }); 70 | 71 | //Verification endpoint for Facebook needed to register a webhook. 72 | router.get('/messenger_auth', function(req, res) { 73 | console.log(req.query["hub.challenge"]); 74 | res.send(req.query["hub.challenge"]); 75 | }); 76 | 77 | 78 | 79 | module.exports = router; 80 | -------------------------------------------------------------------------------- /src/sync_service_details.js: -------------------------------------------------------------------------------- 1 | const Twilio = require('twilio'); 2 | require('dotenv').config(); 3 | 4 | function syncServiceDetails() { 5 | const syncServiceSid = process.env.TWILIO_SYNC_SERVICE_SID || 'default'; 6 | 7 | const client = new Twilio( 8 | process.env.TWILIO_API_KEY, 9 | process.env.TWILIO_API_SECRET, 10 | {accountSid: process.env.TWILIO_ACCOUNT_SID} 11 | ); 12 | client.sync 13 | .services(syncServiceSid) 14 | .fetch() 15 | .then(response => { 16 | console.log(response); 17 | }) 18 | .catch(error => { 19 | console.log(error); 20 | }); 21 | 22 | } 23 | 24 | module.exports = syncServiceDetails; -------------------------------------------------------------------------------- /src/token_generator.js: -------------------------------------------------------------------------------- 1 | const Twilio = require('twilio'); 2 | 3 | const config = require('./config'); 4 | const nameGenerator = require('../name_generator'); 5 | 6 | // Access Token used for Chat and Sync 7 | const AccessToken = Twilio.jwt.AccessToken; 8 | const ChatGrant = AccessToken.ChatGrant; 9 | const SyncGrant = AccessToken.SyncGrant; 10 | 11 | /** 12 | * Generate an Access Token for an application user - it generates a random 13 | * username for the client requesting a token or generates a token with an 14 | * identity if one is provided. 15 | * 16 | * @return {Object} 17 | * {Object.identity} String random indentity 18 | * {Object.token} String token generated 19 | */ 20 | function tokenGenerator(identity = 0) { 21 | // Create an access token which we will sign and return to the client 22 | const token = new AccessToken( 23 | config.TWILIO_ACCOUNT_SID, 24 | config.TWILIO_API_KEY, 25 | config.TWILIO_API_SECRET 26 | ); 27 | 28 | // Assign the provided identity or generate a new one 29 | token.identity = identity || nameGenerator(); 30 | 31 | if (config.TWILIO_CHAT_SERVICE_SID) { 32 | // Create a "grant" which enables a client to use IPM as a given user, 33 | // on a given device 34 | const chatGrant = new ChatGrant({ 35 | serviceSid: config.TWILIO_CHAT_SERVICE_SID 36 | }); 37 | token.addGrant(chatGrant); 38 | } 39 | 40 | if (config.TWILIO_SYNC_SERVICE_SID) { 41 | // Point to a particular Sync service, or use the account default to 42 | // interact directly with Functions. 43 | const syncGrant = new SyncGrant({ 44 | serviceSid: config.TWILIO_SYNC_SERVICE_SID || 'default' 45 | }); 46 | token.addGrant(syncGrant); 47 | } 48 | 49 | // Serialize the token to a JWT string and include it in a JSON response 50 | return { 51 | identity: token.identity, 52 | token: token.toJwt() 53 | }; 54 | } 55 | 56 | module.exports = tokenGenerator; 57 | -------------------------------------------------------------------------------- /test/config.test.js: -------------------------------------------------------------------------------- 1 | const config = require('../src/config'); 2 | 3 | describe('#config', () => { 4 | it('contains the required keys', () => { 5 | expect(config).toHaveProperty('TWILIO_ACCOUNT_SID'); 6 | expect(config).toHaveProperty('TWILIO_API_KEY'); 7 | expect(config).toHaveProperty('TWILIO_API_SECRET'); 8 | expect(config).toHaveProperty('TWILIO_CHAT_SERVICE_SID'); 9 | expect(config).toHaveProperty('TWILIO_NOTIFICATION_SERVICE_SID'); 10 | expect(config).toHaveProperty('TWILIO_SYNC_SERVICE_SID'); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /test/token_generator.test.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | const tokenGenerator = require('../src/token_generator'); 3 | 4 | describe('#tokenGenerator', () => { 5 | it('generates a new token', () => { 6 | const token = tokenGenerator(); 7 | const decoded = jwt.decode(token.token, {complete: true}); 8 | 9 | expect(decoded).toHaveProperty('payload.grants.identity', token.identity); 10 | expect(decoded).toHaveProperty('payload.grants.chat.service_sid'); 11 | expect(decoded.payload.grants.chat.service_sid).toStartWith('IS') 12 | expect(decoded).toHaveProperty('payload.grants.data_sync.service_sid'); 13 | expect(decoded.payload.grants.data_sync.service_sid).toStartWith('IS') 14 | }); 15 | 16 | it('generates a new token using the specified identity', () => { 17 | const identity = "Alice"; 18 | const token = tokenGenerator(identity); 19 | const decoded = jwt.decode(token.token, {complete: true}); 20 | 21 | expect(token.identity).toEqual(identity); 22 | expect(decoded).toHaveProperty('payload.grants.identity', token.identity); 23 | }); 24 | 25 | 26 | }); 27 | --------------------------------------------------------------------------------