Skip to content

Commit

Permalink
Fixes #31 Release Cleanup and Documentation
Browse files Browse the repository at this point in the history
* Add more info to README, move action runner to service class

* Clarify webhook usage

* Fix table column verbiage

* Change pipeline names
  • Loading branch information
oliverspryn authored May 29, 2022
1 parent ba24b61 commit f79b5fc
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 100 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/dev-deploy.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Deploy Develop
name: Deploy to the Dev Environment

on:
pull_request:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/prod-deploy.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Deploy Production
name: Deploy to the Production Environment

on:
release:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/stage-deploy.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Deploy Staging
name: Deploy to the Staging Environment

on:
push:
Expand Down
49 changes: 36 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Message text>`: Sends a message to all numbers subscribed to the announcments and calendar events channel.
- `broadcast <Message text>`: 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

Expand Down Expand Up @@ -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
{
Expand All @@ -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": "<E.164 formatted phone number>",
"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
97 changes: 97 additions & 0 deletions assets/services/MessagingService.private.js
Original file line number Diff line number Diff line change
@@ -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;
87 changes: 3 additions & 84 deletions functions/sms.protected.js
Original file line number Diff line number Diff line change
@@ -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 => {
Expand Down

0 comments on commit f79b5fc

Please sign in to comment.