├── .env.example ├── index.js ├── .gitignore ├── webapp.js ├── controllers └── router.js ├── js ├── twilio.js └── trello.js ├── LICENSE ├── package.json ├── config.js ├── .travis.yml ├── README.md └── .jscsrc /.env.example: -------------------------------------------------------------------------------- 1 | TWILIO_ACCOUNT_SID=your_account_sid 2 | TWILIO_AUTH_TOKEN=your_account_secret 3 | TWILIO_NUMBER=the_twilio_number_you_purchased 4 | TRELLO_KEY=your_trello_api_key 5 | TRELLO_AUTH_TOKEN=the_trello_token_you_generated 6 | TRELLO_BOARD=the_name_of_your_trello_board 7 | SERVER_URL=the_ngrok_url -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var config = require('./config'); 3 | 4 | // Create Express web app 5 | var app = require('./webapp'); 6 | 7 | // Create an HTTP server and listen on the configured port 8 | var server = http.createServer(app); 9 | server.listen(config.port, function() { 10 | console.log('Express server listening on *:' + config.port); 11 | }); 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | 4 | # Logs 5 | logs 6 | *.log 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # node-waf configuration 23 | .lock-wscript 24 | 25 | # Compiled binary addons (http://nodejs.org/api/addons.html) 26 | build/Release 27 | 28 | # Dependency directory 29 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 30 | node_modules 31 | 32 | # Development/test local configuration 33 | .env 34 | .env.test -------------------------------------------------------------------------------- /webapp.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var express = require('express'); 3 | var bodyParser = require('body-parser'); 4 | var morgan = require('morgan'); 5 | 6 | // Create Express web app 7 | var app = express(); 8 | 9 | // Use morgan for HTTP request logging in dev and prod 10 | if (process.env.NODE_ENV !== 'test') { 11 | app.use(morgan('combined')); 12 | } 13 | 14 | // Serve static assets 15 | app.use(express.static(path.join(__dirname, 'public'))); 16 | 17 | // Parse incoming form-encoded HTTP bodies 18 | app.use(bodyParser.urlencoded({ 19 | extended: true 20 | })); 21 | 22 | // For Trello webhook 23 | app.use(bodyParser.json()); 24 | 25 | // Configure application routes 26 | var routes = require('./controllers/router'); 27 | var router = express.Router(); 28 | 29 | routes(router); 30 | app.use(router); 31 | 32 | // Export Express app 33 | module.exports = app; -------------------------------------------------------------------------------- /controllers/router.js: -------------------------------------------------------------------------------- 1 | import { findCardId, createCard, attachWebHook, addComment } from '../js/trello'; 2 | import { sendTextMessage } from '../js/twilio'; 3 | 4 | module.exports = function(router) { 5 | router.post('/sms', async (req, res) => { 6 | const sms = req.body.Body; 7 | const number = req.body.From.replace(/\+/g, "") 8 | 9 | let cardId = await findCardId(number); 10 | if (!cardId) { 11 | cardId = await createCard(number); 12 | await attachWebHook(cardId); 13 | } 14 | 15 | await addComment(cardId, sms); 16 | res.send('Successfully responded to sms'); 17 | }); 18 | 19 | router.get('/response', (req, res) => { 20 | // Required to get Trello webhook set up successfully 21 | res.sendStatus(200); 22 | }); 23 | 24 | router.post('/response', async (req, res) => { 25 | sendTextMessage(req.body); 26 | res.end('Successfully responded to response POST'); 27 | }); 28 | 29 | }; 30 | -------------------------------------------------------------------------------- /js/twilio.js: -------------------------------------------------------------------------------- 1 | const accountSid = process.env.TWILIO_ACCOUNT_SID; 2 | const authToken = process.env.TWILIO_AUTH_TOKEN; 3 | const fromNumber = process.env.TWILIO_NUMBER; 4 | const client = require('twilio')(accountSid, authToken); 5 | 6 | export const sendTextMessage = (request) => { 7 | if (request.action.type !== "commentCard") { 8 | return; 9 | } 10 | 11 | const sms = request.action.data.text; 12 | 13 | // When we put the customer's SMS on the card via API call, we don't want to accidentally 14 | // send that message back to the customer. 15 | if (sms.startsWith('Message from customer:')) { 16 | return; 17 | } 18 | 19 | const toNumber = `+${request.action.data.card.name}`; 20 | 21 | client.messages 22 | .create({ 23 | body: sms, 24 | from: fromNumber, 25 | to: toNumber, 26 | }) 27 | .then(message => console.log('Message sent!', sms)); 28 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Emma Goto 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. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "trello-twilio", 3 | "version": "1.0.0", 4 | "description": "Send and receive SMS notifications via Trello", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "NODE_ENV=test node_modules/.bin/mocha test" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/emgoto/trello-twilio" 12 | }, 13 | "keywords": [ 14 | "twilio", 15 | "express", 16 | "mongodb", 17 | "twiml", 18 | "webhooks", 19 | "voip" 20 | ], 21 | "author": "Emma Goto", 22 | "license": "MIT", 23 | "engines": { 24 | "node": ">=4.1.0" 25 | }, 26 | "dependencies": { 27 | "body-parser": "^1.19.0", 28 | "dotenv": "^2.0.0", 29 | "esm": "^3.2.25", 30 | "express": "^4.12.0", 31 | "express-session": "^1.10.3", 32 | "morgan": "^1.5.1", 33 | "node-fetch": "^2.6.0", 34 | "sanitize-html": "^1.10.0", 35 | "twilio": "^3.42.2", 36 | "underscore": "^1.8.3" 37 | }, 38 | "devDependencies": { 39 | "chai": "^3.5.0", 40 | "mocha": "^3.1.2", 41 | "mockery": "^2.0.0", 42 | "sinon": "^2.1.0", 43 | "supertest": "^2.0.1" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | var dotenv = require('dotenv'); 2 | var cfg = {}; 3 | 4 | if (process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test') { 5 | dotenv.config({path: '.env'}); 6 | } else { 7 | dotenv.config({path: '.env.example', silent: true}); 8 | } 9 | 10 | // HTTP Port to run our web application 11 | cfg.port = process.env.PORT || 3000; 12 | 13 | // A random string that will help generate secure one-time passwords and 14 | // HTTP sessions 15 | cfg.secret = process.env.APP_SECRET || 'keyboard cat'; 16 | 17 | // Your Twilio account SID and auth token, both found at: 18 | // https://www.twilio.com/user/account 19 | // 20 | // A good practice is to store these string values as system environment 21 | // variables, and load them from there as we are doing below. Alternately, 22 | // you could hard code these values here as strings. 23 | cfg.accountSid = process.env.TWILIO_ACCOUNT_SID; 24 | cfg.authToken = process.env.TWILIO_AUTH_TOKEN; 25 | cfg.sendingNumber = process.env.TWILIO_NUMBER; 26 | cfg.trelloKey = process.env.TRELLO_KEY; 27 | cfg.trelloAuth = process.env.TRELLO_AUTH_TOKEN; 28 | cfg.trelloBoard = process.env.TRELLO_BOARD; 29 | cfg.serverURL = process.env.SERVER_URL; 30 | 31 | var requiredConfig = [cfg.accountSid, cfg.authToken, cfg.sendingNumber, cfg.trelloKey, cfg.trelloAuth, cfg.trelloBoard, cfg.serverURL]; 32 | var isConfigured = requiredConfig.every(function(configValue) { 33 | return configValue || false; 34 | }); 35 | 36 | if (!isConfigured) { 37 | var errorMessage = 38 | 'All 6 .env variables must be set.'; 39 | 40 | throw new Error(errorMessage); 41 | } 42 | 43 | // Export configuration object 44 | module.exports = cfg; 45 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '5.0.0' 4 | - '4.0.0' 5 | install: 6 | - npm install 7 | env: 8 | global: 9 | - secure: QffYx9s6Y39fQu62BRIFX2VC9/93AajW3KgI2n9F+gzzHkzDGAULOyid6oPlGZFpldCxyWNlZSVUcI9yCVGVQyIYLZgWr4LqmEqUPm4iRmUvkoY1pNrolUD+Gad4TQGZ9DitAj3UYn8hIjJ8SFAl5XTnpj8EDcGpX/kfB1apay1BmvvE/oD6CaLNMfBWF1/IK1WtOVl4x3pPAaIsp25Y8G/aNxj33ppVxLl+DvfoFY6BGaMJXr+7aZwt+gm1D3u/ptbj7o++ao/guw5qu6vRfb2W1ZHhnWpK0XlFK0pjtzlR1mZ0+xmIQKDULeJkZvB14/YXuZZ/3DUeK+fglOEu0HoGjR+W+5fDLkucRNHkHrDjtCRAadhMdeeOw0yheYrhP7rjnH1nikZfwX7kdG98y49Jw+YtInZiCaZnw/TYRYtzqcclLQ7Ps4lWu7beGrJuC/6kf8clVrCbFO1+jFAa7B/nilyxTuA0iyD1WAYbrJh5/wBgNU3Bu13BeCqcz+9kHzdmrbr2fW3LCouCwdQI6sMzWMgs8tBuc49zX8zdvYGkJDStP/Ef45xZuWCQdavGKPSfLrt2SykiAVeVytoi/cvnmFzSUjK/0HR6dH5hVg34WMuf+Kl2E37secgdzAfqCu8RaSOWHvP5mYu9K5fP6soeyWqxJftLM5G2G7O4BBo= 10 | - secure: oZt8wa4dKc/xG14VVtBHKBreP4DrDGxXqQ0GH9qEwfOB0iwwftUJpQD1Yh4sJMWSidL/RQIpTC/AVj2vfTVBjpqCr0aZ1vwNmEdwj+sJC1WYmFsAR66L1myEjkj+rGNVK51uHe854cQj9nmqAaYo5GHE3rH2OJhCtCJ9Z/PKx5wiUTvBPloSRv5ZwxGYLmrzOP+rK8mwFrGnYTGOV6Y3Q7Jg6mApx/kBxPcFSKWQYQYHQ44xqat2MH6GjDcWnRuLSvNrAa1ilKec4TtYnqn3WOQJzVRxwKyY8qN/YjqvATpN4lVnAhbFs5ujbRi5EKXvac19xV0QsMnjYNtEHoqnjcI693GyT7Z4WTdnXGNjnwigztaWtJW7fdtlABYwEUOBCZooGBcg6jzwruu0NsEtpOZ+H68gyMzCU10EH0fL2VWogmEuIod0kqrhFu8doWPxaVytf3JkB6xTO7lsKlV0Mb8vvayYgtS6wa85kkrZjOFnB2/OF2ngeamydiGOpCcUAE9KUNPs69YzFwBtiLX0WdpyRf7gYhvT1DV5+ZKjlBFs+IxqhEfsAQ1cguYdymOhPsv1FBEawEraudYBE0e/9Juhh4GV1bJnGsNq957UDESIkouCaxsbDGGsA/C37QkkmUFVfZmtxppMM4Fp4WXINl2IY4HCHH//U7id4g30F0o= 11 | - secure: bE+k3qSvhv1XAww8fmW3UicpTzB3ei6RVu0XIRoY7O96mCFlGZBYrowRCT4sel/MEn0hNsTPZchl9o0lmynCEy1JKodVUR4Z2nCp1WoT4FB5QvZjt/wzlUeuC0cCeiv0prQk1hwg/a4zCKrUK1kxZVLsvsR7bLZ8KhhAj7zVY3WdVvlLmsxyA5LKZPJzpRhaK5pzo6pFo3FlQefBuyl5dt097EAvbwO/e0xLlQDjf8USoTfu4VpMVN1vrO+dB1xzhJR6MdoB7pxvpCHdVNP8+sYsDbwg2AX8X1YuIaKjCxvE4kaVb2P7aJvfYfKGmNX2J9Qp6EUORAItBSeMO0HMnyCuKwtf3f46NK2q+xXmvPZ1ZKJXkJHrd9KWtmvt0NmqZ0IRu8gNsGrA1Rpl8LNJ8o1qZRiKExImyk+ayLGQe40tOt7dNTlC8TUt+VZTMv2oXty5HxNAMJ9vSCTo2R7kR97H6K72nEWvYev4KyHiNswdTxZirvbrjzA/0MdCTlMaTefrR5spRvYGUervsPN5gLElzAGF01hgRdktyqjBRHJPUt9Ju0l+F6mfP/vHbvvMegH535QD587ijTEPC0vmc4yQWiy/6Goe7zoPvh9gDvyTTwz11lw6h4Xbhm0szJ39SHZvAsgeVwMl7V32A2E0tRa2T/bIS7M9ZZUZv9rz/Ps= 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Trello + Twilio 2 | This Trello + Twilio integration will allow you to manage your SMS communications via a Trello board. 3 | 4 | [Learn more from my post on DEV](https://dev.to/emma/trello-twilio-simplify-conversations-with-your-customers-32dg). 5 | 6 | ## Local development 7 | ### Setting up your Twilio number to receive messages 8 | Make sure you have installed Twilio's CLI and have logged in: 9 | ``` 10 | npm install -g twilio-cli 11 | twilio login 12 | ``` 13 | 14 | You'll need to provision a new Twilio number in the [Manage Numbers page](https://www.twilio.com/user/account/phone-numbers/incoming) under your account. 15 | 16 | Configure your Twilio phone number to call the webhook URL whenever a new message comes in: 17 | ``` 18 | twilio phone-numbers:update "+15017122661" --sms-url="http://localhost:3000/sms" 19 | ``` 20 | 21 | This will give you an ngrok URL - keep note of it as we will need it in the next section. 22 | 23 | ### Setup your .env file 24 | 25 | Open `.env.example` at the root of the project and save the file as `.env`. You'll need to fill out all six values. 26 | 27 | You can get your `TWILIO_ACCOUNT_SID` and `TWILIO_AUTH_TOKEN` values from your [Twilio console](https://www.twilio.com/console). 28 | 29 | The `TWILIO_NUMBER` variable should be the same as the one you setup above for your Twilio webhook. The phone number should be in [E.164 format](https://support.twilio.com/hc/en-us/articles/223183008-Formatting-International-Phone-Numbers). 30 | 31 | For `TRELLO_KEY`, log into your Trello account and go to Trello's [Developer API Keys](https://trello.com/app-key/) page. 32 | 33 | For `TRELLO_AUTH_TOKEN` you can generate a token using the "manually generate a Token" link on the page linked above. Make sure not to share this token with anyone else! 34 | 35 | For `TRELLO_BOARD`, please input the ID of Trello board that you want this integration to run on. To get this ID, visit your Trello board and add `.json` to the end of the URL e.g. https://trello.com/b/12345/board-name.json. Make sure your board has at least one list. 36 | 37 | For `SERVER_URL`, enter the ngrok URL you received previously when you ran the `twilio` command. 38 | 39 | Run `source .env` to export the environment variables. 40 | 41 | ### Running the app locally 42 | 43 | For first-time use, make sure you have run `npm install`. 44 | 45 | Then, to launch the application, run `node -r esm . `. This should start up your app at `http://localhost:3000`. 46 | 47 | ### Have fun! 48 | Now when your Twilio number is messaged, this will generate a new card on your Trello board and leave a comment in the first list on the board. 49 | 50 | ### References 51 | Initial code taken from [server-notifications-node](https://github.com/TwilioDevEd/server-notifications-node). -------------------------------------------------------------------------------- /js/trello.js: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | 3 | const trelloKey = process.env.TRELLO_KEY; 4 | const trelloAuth = process.env.TRELLO_AUTH_TOKEN; 5 | const trelloBoard = process.env.TRELLO_BOARD; 6 | const serverURL = process.env.SERVER_URL; 7 | 8 | const TRELLO_API = 'https://api.trello.com/1'; 9 | 10 | /** 11 | * Find card ID with a name that matches the given phone number, or return null. 12 | */ 13 | export const findCardId = async (phoneNumber) => { 14 | const url = `${TRELLO_API}/board/${trelloBoard}/cards?key=${trelloKey}&token=${trelloAuth}`; 15 | const result = await fetch(url); 16 | const cardsResult = await result.json(); 17 | 18 | let cardId = null; 19 | 20 | cardsResult.forEach((card) => { 21 | if (card.name === phoneNumber) { 22 | cardId = card.id; 23 | } 24 | }); 25 | 26 | return cardId; 27 | } 28 | 29 | /** 30 | * Create a new card with the name equalling the given phone number, and return its ID. 31 | */ 32 | export const createCard = async (phoneNumber) => { 33 | const listUrl = `${TRELLO_API}/board/${trelloBoard}/lists?key=${trelloKey}&token=${trelloAuth}`; 34 | const result = await fetch(listUrl); 35 | const listResult = await result.json(); 36 | 37 | if (listResult.length === 0) { 38 | throw Error("Your Trello board should have at least one list"); 39 | } 40 | 41 | const listId = listResult[0].id; 42 | 43 | const url = `${TRELLO_API}/cards?key=${trelloKey}&token=${trelloAuth}&name=${phoneNumber}&idList=${listId}`; 44 | const cardResult = await fetch(url, { method: 'post' }); 45 | const createdCard = await cardResult.json(); 46 | 47 | return createdCard.id; 48 | }; 49 | 50 | /** 51 | * Attach Trello webhook to given cardId 52 | */ 53 | export const attachWebHook = (cardId) => { 54 | const url = `${TRELLO_API}/webhooks?token=${trelloAuth}&key=${trelloKey}`; 55 | 56 | return fetch(url, { 57 | method: 'post', 58 | headers: { 59 | 'Content-Type': 'application/json' 60 | }, 61 | body: JSON.stringify({ 62 | description: "Webhook for card comments", 63 | callbackURL: `${serverURL}/response`, 64 | idModel: cardId 65 | }) 66 | }).then(response => { 67 | if (response && response.status && response.status === 400) { 68 | console.log("Trello webhook failed to attach on card creation. Is your serverURL env variable correct?") 69 | } 70 | }); 71 | }; 72 | 73 | /** 74 | * Add comment to Trello card 75 | */ 76 | export const addComment = (cardId, text) => { 77 | const url = `${TRELLO_API}/cards/${cardId}/actions/comments?text=Message from customer: \n\n ${text}&key=${trelloKey}&token=${trelloAuth}`; 78 | 79 | return fetch(url, { 80 | method: 'post', 81 | }); 82 | } -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "requireCurlyBraces": [ 3 | "if", 4 | "else", 5 | "for", 6 | "while", 7 | "do", 8 | "try", 9 | "catch" 10 | ], 11 | "requireOperatorBeforeLineBreak": true, 12 | "requireCamelCaseOrUpperCaseIdentifiers": true, 13 | "maximumLineLength": { 14 | "value": 80, 15 | "allExcept": ["comments", "regex"] 16 | }, 17 | "validateIndentation": 2, 18 | "validateQuoteMarks": "'", 19 | 20 | "disallowMultipleLineStrings": true, 21 | "disallowMixedSpacesAndTabs": true, 22 | "disallowTrailingWhitespace": true, 23 | "disallowSpaceAfterPrefixUnaryOperators": true, 24 | "disallowMultipleVarDecl": true, 25 | "disallowKeywordsOnNewLine": ["else"], 26 | 27 | "requireSpaceAfterKeywords": [ 28 | "if", 29 | "else", 30 | "for", 31 | "while", 32 | "do", 33 | "switch", 34 | "return", 35 | "try", 36 | "catch" 37 | ], 38 | "requireSpaceBeforeBinaryOperators": [ 39 | "=", "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=", 40 | "&=", "|=", "^=", "+=", 41 | 42 | "+", "-", "*", "/", "%", "<<", ">>", ">>>", "&", 43 | "|", "^", "&&", "||", "===", "==", ">=", 44 | "<=", "<", ">", "!=", "!==" 45 | ], 46 | "requireSpaceAfterBinaryOperators": true, 47 | "requireSpacesInConditionalExpression": true, 48 | "requireSpaceBeforeBlockStatements": true, 49 | "requireSpacesInForStatement": true, 50 | "requireLineFeedAtFileEnd": true, 51 | "requireSpacesInFunctionExpression": { 52 | "beforeOpeningCurlyBrace": true 53 | }, 54 | "disallowSpacesInAnonymousFunctionExpression": { 55 | "beforeOpeningRoundBrace": true 56 | }, 57 | "disallowSpacesInsideObjectBrackets": "all", 58 | "disallowSpacesInsideArrayBrackets": "all", 59 | "disallowSpacesInsideParentheses": true, 60 | 61 | "disallowMultipleLineBreaks": true, 62 | "disallowNewlineBeforeBlockStatements": true, 63 | "disallowKeywords": ["with"], 64 | "disallowSpacesInFunctionExpression": { 65 | "beforeOpeningRoundBrace": true 66 | }, 67 | "disallowSpacesInFunctionDeclaration": { 68 | "beforeOpeningRoundBrace": true 69 | }, 70 | "disallowSpacesInCallExpression": true, 71 | "disallowSpaceAfterObjectKeys": true, 72 | "requireSpaceBeforeObjectValues": true, 73 | "requireCapitalizedConstructors": true, 74 | "requireDotNotation": true, 75 | "requireSemicolons": true, 76 | "validateParameterSeparator": ", ", 77 | 78 | "jsDoc": { 79 | "checkAnnotations": "closurecompiler", 80 | "checkParamNames": true, 81 | "requireParamTypes": true, 82 | "checkRedundantParams": true, 83 | "checkReturnTypes": true, 84 | "checkRedundantReturns": true, 85 | "requireReturnTypes": true, 86 | "checkTypes": true, 87 | "checkRedundantAccess": true, 88 | "requireNewlineAfterDescription": true 89 | } 90 | } 91 | --------------------------------------------------------------------------------