From f79b5fc5852600107a0e407a0d0f97ed943aa460 Mon Sep 17 00:00:00 2001 From: Oliver Spryn <2717340+oliverspryn@users.noreply.github.com> Date: Sun, 29 May 2022 19:34:38 -0400 Subject: [PATCH] Fixes #31 Release Cleanup and Documentation * Add more info to README, move action runner to service class * Clarify webhook usage * Fix table column verbiage * Change pipeline names --- .github/workflows/dev-deploy.yml | 2 +- .github/workflows/prod-deploy.yml | 2 +- .github/workflows/stage-deploy.yml | 2 +- README.md | 49 ++++++++--- assets/services/MessagingService.private.js | 97 +++++++++++++++++++++ functions/sms.protected.js | 87 +----------------- 6 files changed, 139 insertions(+), 100 deletions(-) create mode 100644 assets/services/MessagingService.private.js diff --git a/.github/workflows/dev-deploy.yml b/.github/workflows/dev-deploy.yml index 5106354..f2af9af 100644 --- a/.github/workflows/dev-deploy.yml +++ b/.github/workflows/dev-deploy.yml @@ -1,4 +1,4 @@ -name: Deploy Develop +name: Deploy to the Dev Environment on: pull_request: diff --git a/.github/workflows/prod-deploy.yml b/.github/workflows/prod-deploy.yml index fb06a31..c50e3d4 100644 --- a/.github/workflows/prod-deploy.yml +++ b/.github/workflows/prod-deploy.yml @@ -1,4 +1,4 @@ -name: Deploy Production +name: Deploy to the Production Environment on: release: diff --git a/.github/workflows/stage-deploy.yml b/.github/workflows/stage-deploy.yml index 3410335..ac03b82 100644 --- a/.github/workflows/stage-deploy.yml +++ b/.github/workflows/stage-deploy.yml @@ -1,4 +1,4 @@ -name: Deploy Staging +name: Deploy to the Staging Environment on: push: diff --git a/README.md b/README.md index 3936046..c798117 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,15 @@ Twilio Functions application used to manage our centralized messaging and automa Here are all of the commands it supports: - `subscribe`: Add a phone number to a channel used for general announcements and calendar event updates. -- `broadcast `: Sends a message to all numbers subscribed to the announcments and calendar events channel. +- `broadcast `: Sends a message to all numbers subscribed to the announcements and calendar events channel. +- `help`\*, `info`\*, `information`\*: Determine whether your number is enrolled in any active channels. - `status`: Determine whether your number is enrolled in any active channels. +- `start`\*, `unstop`\*: Restart a user's subscription to any existing channels. +- `cancel`\*, `stop`\*, `unsubscribe`\*: Stop receiving all messages from this service. -The `broadcast` command is gated to prevent unauthorized phone numbers from sending broadcast messages. All new subscribers and any unrecognized messages are collected and sent [via a webhook](#webhooks) for any additional post-processing by the service owner. +The `broadcast` command is gated to prevent unauthorized phone numbers from sending broadcast messages. Several commands dispatch an event [via a webhook](#webhooks) for any additional post-processing by the service owner. + +\* = Acts as a follow-up message to Twilio's built-in, automatic handling of these regulatory-required keywords. That mean's the messages specified in a Messaging application's Opt-Out Managment portal will be sent first, followed by a more user-specific response from this application. ## Get Started @@ -48,21 +53,30 @@ To run your project, select either the Run Local configuration for `localhost` t While most of the configuration is stored inside of the [Config.private.js](/assets/Config.private.js) file, more sensitive information is offloaded into environment variables. -Here are all of the environment variables this application expects during development and production. All are required, but Twilio can automatically add in some of them, as indicated by the last column. +Here are all of the environment variables this application expects during development and production. All are required, but Twilio can automatically add in some of them, as indicated by the Added By Twilio Automatically column. -| Variable Name | Purpose | Added By Twilio Automatically | -|--------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------:| -| `ACCOUNT_SID` | Acts like a username to authorize this application to your Twilio account | :white_check_mark: | -| `AUTH_TOKEN` | Acts like your API key, or account password | :white_check_mark: | -| `AUTHORIZED_BROADCAST_PHONE_NUMBERS` | A comma-separated list of [E.164 formatted phone numbers](https://www.twilio.com/docs/glossary/what-e164) that are authorized to perform a `broadcast` command | :x: | -| `NOTIFY_SERVICE_SID` | The ID of the Notify service which will collect phone numbers and send messages | :x: | -| `WEBHOOK_URL` | A webhook which can process events of interest, as described below | :x: | +| Variable Name | Purpose | Added By Twilio Automatically | Used By | +|--------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------|------------| +| `ACCOUNT_SID` | Acts like a username to authorize this application to your Twilio account | :white_check_mark: | App | +| `AUTH_TOKEN` | Acts like your API key, or account password | :white_check_mark: | App | +| `AUTHORIZED_BROADCAST_PHONE_NUMBERS` | A comma-separated list of [E.164 formatted phone numbers](https://www.twilio.com/docs/glossary/what-e164) that are authorized to perform a `broadcast` command | :x: | App | +| `NOTIFY_SERVICE_SID` | The ID of the Notify service which will collect phone numbers and send messages | :x: | App | +| `SERVICE_NAME` | The name of the Twilio Functions service to deploy to | :x: | Deployment | +| `TWILIO_API_KEY` | Like an `ACCOUNT_SID`, but [created specifically for the purpose of CI deployment](https://www.twilio.com/console/project/api-keys), to prevent compromise of the master key | :x: | Deployment | +| `TWILIO_API_SECRET` | Like an `AUTH_TOKEN`, but [created specifically for the purpose of CI deployment](https://www.twilio.com/console/project/api-keys), to prevent compromise of the master key | :x: | Deployment | +| `WEBHOOK_URL` | A webhook which can process events of interest, as described below | :x: | App | ## Webhooks -Webhooks are triggered under two circumstances: a new subscriber is recorded or an unknown message is received. That webhook is determined by the `WEBHOOK_URL` environment variable. +Webhooks are triggered under a few circumstances: + +- a new subscriber is recorded, or a user resubscribes +- an unknown message is received +- a user unsubscribes from the messaging service + +That webhook is determined by the `WEBHOOK_URL` environment variable. -When receiving new subscriber, a POST request with the following payload is sent: +When receiving new subscriber or resubscribing a user, a POST request with the following payload is sent: ```json { @@ -82,11 +96,20 @@ For an unknown message event, a POST request with the following payload is sent: } ``` +For unsubscribe events, a POST request with the following payload is sent: + +```json +{ + "phoneNumber": "", + "type": "Invalid Message" +} +``` + ## Usage on Twilio [Twilio has a guide](https://www.twilio.com/blog/2017/12/how-to-set-up-sms-broadcasts-in-five-minutes.html) for setting up their services to work with a broadcast application, like this. The major differences between that article and this application are as follows: 1. Create a new Functions service on Twilio, not a Classic Functions service 2. Ensure that your Functions instance on Twilio has all of the environment variables, as [described above](#environment-variables) -3. Deploy this service from your machine to Twilio functions with: `twilio serveless:deploy` +3. Deploy this service from your machine to Twilio functions with: `npm run deploy:prod` 4. Proceed to use your new Functions service in place of the Classic service used in that article diff --git a/assets/services/MessagingService.private.js b/assets/services/MessagingService.private.js new file mode 100644 index 0000000..530ffba --- /dev/null +++ b/assets/services/MessagingService.private.js @@ -0,0 +1,97 @@ +"use strict"; + +const assets = Runtime.getAssets(); +const BroadcastAction = require(assets["/actions/BroadcastAction.js"].path); +const Config = require(assets["/Config.js"].path); +const InputCommandEnum = require(assets["/enums/InputCommandEnum.js"].path); +const InputParserService = require(assets["/services/InputParserService.js"].path); +const ResubscribeAction = require(assets["/actions/ResubscribeAction.js"].path); +const StatusAction = require(assets["/actions/StatusAction.js"].path); +const SubscribeAction = require(assets["/actions/SubscribeAction.js"].path); +const UnsubscribeAction = require(assets["/actions/UnsubscribeAction.js"].path); +const WebhookService = require(assets["/services/WebhookService.js"].path); + +class MessagingService { + run(event) { + const parser = new InputParserService(event); + const model = parser.commandInputModel; + let response = null; + + switch (model.command) { + case InputCommandEnum.BROADCAST_ANNOUNCEMENT_CALENDAR: + const broadcast = new BroadcastAction(model); + + response = broadcast.run( + Config.AnnouncementCalendar.Broadcast.Success, + Config.AnnouncementCalendar.Broadcast.EmptyMessage, + Config.AnnouncementCalendar.Broadcast.Unauthorized, + Config.AnnouncementCalendar.Broadcast.AuthorizedPhoneNumbers, + Config.AnnouncementCalendar.BindingType, + Config.AnnouncementCalendar.Tags + ); + + break; + + case InputCommandEnum.HELP: + const help = new StatusAction(model); + + response = help.run( + Config.Help.IsRegistered, + Config.Help.IsNotRegistered + ); + + break; + + case InputCommandEnum.RESUBSCRIBE: + const resubscribe = new ResubscribeAction(model); + + response = resubscribe.run( + Config.Resubscribe.Message + ); + + break; + + case InputCommandEnum.STATUS: + const status = new StatusAction(model); + + response = status.run( + Config.Status.IsRegistered, + Config.Status.IsNotRegistered + ); + + break; + + case InputCommandEnum.SUBSCRIBE_ANNOUNCEMENT_CALENDAR: + const subscribe = new SubscribeAction(model); + + response = subscribe.run( + Config.AnnouncementCalendar.Subscribe.NewUser, + Config.AnnouncementCalendar.Subscribe.ExistingUser, + Config.AnnouncementCalendar.BindingType, + Config.AnnouncementCalendar.Tags + ); + + break; + + case InputCommandEnum.UNSUBSCRIBE: + const unsubscribe = new UnsubscribeAction(model); + response = unsubscribe.run(); + break; + + default: + const webhook = new WebhookService(); + + response = webhook + .sendInvalidMessage(model.message, model.phoneNumber) + .then(() => { + return Config.InvalidRequest; + }); + + break; + } + + return response; + } +} + +module.exports = MessagingService; diff --git a/functions/sms.protected.js b/functions/sms.protected.js index 5d13b5a..78db7db 100644 --- a/functions/sms.protected.js +++ b/functions/sms.protected.js @@ -1,93 +1,12 @@ "use strict"; const assets = Runtime.getAssets(); -const BroadcastAction = require(assets["/actions/BroadcastAction.js"].path); const Config = require(assets["/Config.js"].path); -const InputCommandEnum = require(assets["/enums/InputCommandEnum.js"].path); -const InputParserService = require(assets["/services/InputParserService.js"].path); -const ResubscribeAction = require(assets["/actions/ResubscribeAction.js"].path); -const StatusAction = require(assets["/actions/StatusAction.js"].path); -const SubscribeAction = require(assets["/actions/SubscribeAction.js"].path); -const UnsubscribeAction = require(assets["/actions/UnsubscribeAction.js"].path); -const WebhookService = require(assets["/services/WebhookService.js"].path); +const MessagingService = require(assets["/services/MessagingService.js"].path); exports.handler = (context, event, callback) => { - const parser = new InputParserService(event); - const model = parser.commandInputModel; - let outcome = null; - - switch (model.command) { - case InputCommandEnum.BROADCAST_ANNOUNCEMENT_CALENDAR: - const broadcast = new BroadcastAction(model); - - outcome = broadcast.run( - Config.AnnouncementCalendar.Broadcast.Success, - Config.AnnouncementCalendar.Broadcast.EmptyMessage, - Config.AnnouncementCalendar.Broadcast.Unauthorized, - Config.AnnouncementCalendar.Broadcast.AuthorizedPhoneNumbers, - Config.AnnouncementCalendar.BindingType, - Config.AnnouncementCalendar.Tags - ); - - break; - - case InputCommandEnum.HELP: - const help = new StatusAction(model); - - outcome = help.run( - Config.Help.IsRegistered, - Config.Help.IsNotRegistered - ); - - break; - - case InputCommandEnum.RESUBSCRIBE: - const resubscribe = new ResubscribeAction(model); - - outcome = resubscribe.run( - Config.Resubscribe.Message - ); - - break; - - case InputCommandEnum.STATUS: - const status = new StatusAction(model); - - outcome = status.run( - Config.Status.IsRegistered, - Config.Status.IsNotRegistered - ); - - break; - - case InputCommandEnum.SUBSCRIBE_ANNOUNCEMENT_CALENDAR: - const subscribe = new SubscribeAction(model); - - outcome = subscribe.run( - Config.AnnouncementCalendar.Subscribe.NewUser, - Config.AnnouncementCalendar.Subscribe.ExistingUser, - Config.AnnouncementCalendar.BindingType, - Config.AnnouncementCalendar.Tags - ); - - break; - - case InputCommandEnum.UNSUBSCRIBE: - const unsubscribe = new UnsubscribeAction(model); - outcome = unsubscribe.run(); - break; - - default: - const webhook = new WebhookService(); - - outcome = webhook - .sendInvalidMessage(model.message, model.phoneNumber) - .then(() => { - return Config.InvalidRequest; - }); - - break; - } + const messaging = new MessagingService(); + const outcome = messaging.run(event); outcome .then(message => {