From 95a276a1749bf93401a9d5040c9532be89d24689 Mon Sep 17 00:00:00 2001 From: Dominik Kundel Date: Tue, 1 Nov 2022 09:54:34 -0700 Subject: [PATCH] feat: add new reminder-message template --- package-lock.json | 54 +++++++-- package.json | 3 +- reminder-message/.env.example | 14 +++ reminder-message/.owners | 4 + reminder-message/CHANGELOG.md | 8 ++ reminder-message/README.md | 59 ++++++++++ reminder-message/assets/index.html | 105 ++++++++++++++++++ .../functions/respond.protected.js | 34 ++++++ reminder-message/package.json | 9 ++ reminder-message/tests/respond.test.js | 83 ++++++++++++++ templates.json | 5 + 11 files changed, 368 insertions(+), 10 deletions(-) create mode 100644 reminder-message/.env.example create mode 100644 reminder-message/.owners create mode 100644 reminder-message/CHANGELOG.md create mode 100644 reminder-message/README.md create mode 100644 reminder-message/assets/index.html create mode 100644 reminder-message/functions/respond.protected.js create mode 100644 reminder-message/package.json create mode 100644 reminder-message/tests/respond.test.js diff --git a/package-lock.json b/package-lock.json index 238759e30..e65e40119 100644 --- a/package-lock.json +++ b/package-lock.json @@ -73,7 +73,8 @@ "voice-ivr", "voice-javascript-sdk", "voicemail", - "verify-sna" + "verify-sna", + "reminder-message" ], "devDependencies": { "@twilio-labs/runtime-helpers": "^0.1.2", @@ -13053,6 +13054,10 @@ "node": ">=6.5.0" } }, + "node_modules/reminder-message": { + "resolved": "reminder-message", + "link": true + }, "node_modules/remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", @@ -14841,9 +14846,9 @@ "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" }, "node_modules/twilio": { - "version": "3.82.2", - "resolved": "https://registry.npmjs.org/twilio/-/twilio-3.82.2.tgz", - "integrity": "sha512-LHqTrrpEROY3dqsSIzHTPeJ0sxjv8j0J3N0CRyrJybhs+iLIzJ3OeLrSIzlJo4x+pRFY4F1dBNCfMjTl4fwf2g==", + "version": "3.83.1", + "resolved": "https://registry.npmjs.org/twilio/-/twilio-3.83.1.tgz", + "integrity": "sha512-1ZTgPhqmO6AyXI9CAIrWMsirYVxScyjq5jmAy9CSITGiatcBmV78rWwkPn4m3v5rWwIEz7q1lWJlFxkQK00K5g==", "dependencies": { "axios": "^0.26.1", "dayjs": "^1.8.29", @@ -15717,6 +15722,13 @@ "validator": "^13.6.0" } }, + "reminder-message": { + "version": "1.0.0", + "dependencies": { + "@twilio-labs/runtime-helpers": "^0.1.2", + "twilio": "^3.83.1" + } + }, "segment-event-notification": { "version": "1.0.0" }, @@ -15813,7 +15825,16 @@ "verify-totp-sms": { "version": "1.0.1", "dependencies": { - "twilio": "^3.61.0" + "twilio": "^3.61.0", + "uuid": "^9.0.0" + } + }, + "verify-totp-sms/node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "bin": { + "uuid": "dist/bin/uuid" } }, "video": { @@ -25642,6 +25663,13 @@ "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", "dev": true }, + "reminder-message": { + "version": "file:reminder-message", + "requires": { + "@twilio-labs/runtime-helpers": "^0.1.2", + "twilio": "3.83.1" + } + }, "remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", @@ -27026,9 +27054,9 @@ "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" }, "twilio": { - "version": "3.82.2", - "resolved": "https://registry.npmjs.org/twilio/-/twilio-3.82.2.tgz", - "integrity": "sha512-LHqTrrpEROY3dqsSIzHTPeJ0sxjv8j0J3N0CRyrJybhs+iLIzJ3OeLrSIzlJo4x+pRFY4F1dBNCfMjTl4fwf2g==", + "version": "3.83.1", + "resolved": "https://registry.npmjs.org/twilio/-/twilio-3.83.1.tgz", + "integrity": "sha512-1ZTgPhqmO6AyXI9CAIrWMsirYVxScyjq5jmAy9CSITGiatcBmV78rWwkPn4m3v5rWwIEz7q1lWJlFxkQK00K5g==", "requires": { "axios": "^0.26.1", "dayjs": "^1.8.29", @@ -27380,7 +27408,15 @@ "verify-totp-sms": { "version": "file:verify-totp-sms", "requires": { - "twilio": "^3.61.0" + "twilio": "^3.61.0", + "uuid": "^9.0.0" + }, + "dependencies": { + "uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" + } } }, "verror": { diff --git a/package.json b/package.json index 0009f6ec1..c6a454843 100644 --- a/package.json +++ b/package.json @@ -137,6 +137,7 @@ "voice-ivr", "voice-javascript-sdk", "voicemail", - "verify-sna" + "verify-sna", + "reminder-message" ] } diff --git a/reminder-message/.env.example b/reminder-message/.env.example new file mode 100644 index 000000000..dd23e915a --- /dev/null +++ b/reminder-message/.env.example @@ -0,0 +1,14 @@ +# description: SID of a Twilio Messaging Service that will be used for your reminder email. +# format: sid +# link: https://console.twilio.com/us1/develop/sms/services +# required: true +MESSAGING_SERVICE_SID= + +# description: Delay of how many minutes it should take after the incoming message to sent out a reminder. Minimum: 15 minutes +# format: number +# required: false +DELAY_IN_MINUTES= + +# description: The path to the webhook +# configurable: false +TWILIO_SMS_WEBHOOK_URL=/respond \ No newline at end of file diff --git a/reminder-message/.owners b/reminder-message/.owners new file mode 100644 index 000000000..e2c1252c4 --- /dev/null +++ b/reminder-message/.owners @@ -0,0 +1,4 @@ +dkundel +alisontanu +pthirumurthi +# Insert your Github username here diff --git a/reminder-message/CHANGELOG.md b/reminder-message/CHANGELOG.md new file mode 100644 index 000000000..3982d4615 --- /dev/null +++ b/reminder-message/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +## [Unreleased] + +## [1.0.0] +### Added +- Initial release. + diff --git a/reminder-message/README.md b/reminder-message/README.md new file mode 100644 index 000000000..d6b8478b2 --- /dev/null +++ b/reminder-message/README.md @@ -0,0 +1,59 @@ +# reminder-message + +Schedule a reminder message to be sent a specified time after the initial message + +## Pre-requisites + +### Environment variables + +This project requires some environment variables to be set. A file named `.env` is used to store the values for those environment variables. To keep your tokens and secrets secure, make sure to not commit the `.env` file in git. When setting up the project with `twilio serverless:init ...` the Twilio CLI will create a `.gitignore` file that excludes `.env` from the version history. + +In your `.env` file, set the following values: + +| Variable | Description | Required | +| :------- | :---------- | :------- | +| `MESSAGING_SERVICE_SID` | The SID of a valid Messaging Service that is used to send out the scheduled message | Yes | +| `DELAY_IN_MINUTES` | Configurable time to delay the delivery of the reminder message. Defaults to the minimum of 15 minutes | No | + +### Function Parameters + +`/respond` is protected and requires a valid Twilio signature as well as the following parameters: + +| Parameter | Description | Required | +| :-------- | :---------- | :------- | +| `From` | The phone number the incoming message was sent from. Gets automatically passed by Twilio | Yes | + +## Create a new project with the template + +1. Install the [Twilio CLI](https://www.twilio.com/docs/twilio-cli/quickstart#install-twilio-cli) +2. Install the [serverless toolkit](https://www.twilio.com/docs/labs/serverless-toolkit/getting-started) + +```shell +twilio plugins:install @twilio-labs/plugin-serverless +``` + +3. Initiate a new project + +``` +twilio serverless:init example --template=reminder-message && cd example +``` + +4. Start the server with the [Twilio CLI](https://www.twilio.com/docs/twilio-cli/quickstart): + +``` +twilio serverless:start +``` + +5. Open the web page at and enter your phone number to test + +ℹ️ Check the developer console and terminal for any errors, make sure you've set your environment variables. + +## Deploying + +Deploy your functions and assets with either of the following commands. Note: you must run these commands from inside your project folder. [More details in the docs.](https://www.twilio.com/docs/labs/serverless-toolkit) + +With the [Twilio CLI](https://www.twilio.com/docs/twilio-cli/quickstart): + +``` +twilio serverless:deploy +``` diff --git a/reminder-message/assets/index.html b/reminder-message/assets/index.html new file mode 100644 index 000000000..9fcfbe430 --- /dev/null +++ b/reminder-message/assets/index.html @@ -0,0 +1,105 @@ + + + + + + + + Get started with your Twilio Functions! + + + + + + + + +
+
+ + +
+
+
+
+

+ +
+

Welcome!

+

Your live application with Twilio is ready to use!

+
+

+
+

Get started with your application

+

+ Follow these steps to try out your new app: +

+

+ This app is configured to handle incoming messages to your Twilio phone number, respond with a + default message or if you send "Y" to the number it will schedule reminder message to be sent out in + 15 minutes (or a different delay if you used another delay). +

+
    +
  1. Text any message to your Twilio phone number.
  2. +
  3. You should receive a welcoming response.
  4. +
  5. Text "Y" to your number.
  6. +
  7. You should receive a confirmation message and another message will be scheduled.
  8. +
  9. After 15 minutes (or whatever delay you configured) you'll receive another message to your + number.
  10. +
+
+
+ +
+
+

Troubleshooting

+ +
+
+
+
+ We can't wait to see what you build. +
+ + + \ No newline at end of file diff --git a/reminder-message/functions/respond.protected.js b/reminder-message/functions/respond.protected.js new file mode 100644 index 000000000..ee5a85ab5 --- /dev/null +++ b/reminder-message/functions/respond.protected.js @@ -0,0 +1,34 @@ +exports.handler = async function (context, event, callback) { + const twiml = new Twilio.twiml.MessagingResponse(); + + if (typeof event.Body === 'string' && event.Body.toLowerCase() === 'y') { + let delay = parseInt(context.DELAY_IN_MINUTES, 10); + if (isNaN(delay)) { + delay = 15; + } + + const client = context.getTwilioClient(); + try { + await client.messages.create({ + from: context.MESSAGING_SERVICE_SID, + to: event.From, // sending to the phone number that sent us a message + body: "Here's the reminder for your appointment. Have a great day!", + scheduleType: 'fixed', + sendAt: new Date(Date.now() + delay * 60 * 1000 + 10000), // add the delay to the current time and add a 10 second buffer to avoid hitting the minimum delay of 15 minutes + }); + twiml.message( + `Thank you! We will send you a reminder in ${delay} minutes` + ); + } catch (err) { + console.error(err); + twiml.message( + 'Something went wrong with scheduling the message. Please check your Twilio logs.' + ); + } + } else { + twiml.message( + 'Hello there! Please send "Y" to confirm your appointment and we will send you a reminder.' + ); + } + return callback(null, twiml); +}; diff --git a/reminder-message/package.json b/reminder-message/package.json new file mode 100644 index 000000000..a371d1e2e --- /dev/null +++ b/reminder-message/package.json @@ -0,0 +1,9 @@ +{ + "name": "reminder-message", + "version": "1.0.0", + "private": true, + "dependencies": { + "@twilio-labs/runtime-helpers": "^0.1.2", + "twilio": "^3.83.1" + } +} diff --git a/reminder-message/tests/respond.test.js b/reminder-message/tests/respond.test.js new file mode 100644 index 000000000..c2d22b533 --- /dev/null +++ b/reminder-message/tests/respond.test.js @@ -0,0 +1,83 @@ +const helpers = require('../../test/test-helper'); +const { handler } = require('../functions/respond.protected'); +const Twilio = require('twilio'); + +const mockTwilioClient = { + messages: { + create: jest.fn(() => { + return Promise.resolve({ + sid: 'my-new-sid', + }); + }), + }, +}; + +const context = { + getTwilioClient: () => mockTwilioClient, +}; +const event = {}; + +beforeAll(() => { + helpers.setup(context); +}); + +afterAll(() => { + helpers.teardown(); +}); + +test('returns a MessagingResponse', (done) => { + const callback = (_err, result) => { + expect(result).toBeInstanceOf(Twilio.twiml.MessagingResponse); + done(); + }; + + handler(context, event, callback); +}); + +test('sends default message', (done) => { + const callback = (_err, result) => { + expect(result.toString()).toMatch( + 'Hello there! Please send "Y" to confirm your appointment and we will send you a reminder.' + ); + done(); + }; + + handler(context, event, callback); +}); + +test('sends scheduled message confirmation with 5 minutes default', (done) => { + const callback = (_err, result) => { + expect(result.toString()).toMatch( + 'Thank you! We will send you a reminder in 15 minutes' + ); + done(); + }; + + handler(context, { ...event, Body: 'Y' }, callback); +}); + +test('sends confirmation with configured delay', (done) => { + const callback = (_err, result) => { + expect(result.toString()).toMatch( + 'Thank you! We will send you a reminder in 30 minutes' + ); + done(); + }; + + handler( + { ...context, DELAY_IN_MINUTES: '30' }, + { ...event, Body: 'Y' }, + callback + ); +}); + +test('handles lower case y as confirmation', (done) => { + const callback = (_err, result) => { + expect(result.toString()).toMatch( + 'Thank you! We will send you a reminder in 15 minutes' + ); + done(); + }; + + handler(context, { ...event, Body: 'y' }, callback); +}); diff --git a/templates.json b/templates.json index 6e11f5d71..e232331a5 100644 --- a/templates.json +++ b/templates.json @@ -324,6 +324,11 @@ "id": "verify-sna", "name": "Verify Silent Network Auth Sample Backend", "description": "Backend to communicate with Verify API. This backend starts and validates login requests against the Verify API by using the SNA channel functionality. The results will be presented on the UI dashboard." + }, + { + "id": "reminder-message", + "name": "Send scheduled reminder messages", + "description": "Schedule a reminder message to be sent a specified time after the initial message" } ] }