Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(templates): add Personal Voicemail template #107

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added personal-voicemail/.env
Empty file.
61 changes: 61 additions & 0 deletions personal-voicemail/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# personal-voicemail

Provides a simple voice mailbox with SMS notifications and a block list. After callers leave a message you'll be sent a link to the recording via SMS.

An optional block list allows you to manually add phone numbers that you'd like to reject. Rejected callers are not allowed to leave messages.

## Pre-requisites

### Functions Configuration (Twilio Console)

This function requires that you have the "Enable ACCOUNT_SID and AUTH_TOKEN" preference set in your [Functions Settings](https://www.twilio.com/console/functions/configure).

### Function Parameters

`/personal-voicemail` expects the following parameters as defined in the function source:

| Parameter | Description | Required |
| :-------- | :---------- | :------- |
| `phoneNumber` | The phone number to which you'd like to forward incoming calls, formatted in E164. | yes |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be nice if we would support this via an environment variable as well. I think we use MY_PHONE_NUMBER in a couple of the other templates.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have opinions about environment variables. ;)

When developing functions it is very easy for generic vars to clutter the environment, often to the point of clashing with one another. I feel that unless the function actually requires shared state or configuration that it should be kept local to the file. So unless a function requires multiple files to accomplish the task, or hooks in to a larger system's globals, then using environment variables is overkill, over complicating, and unnecessary.

Just in this repo, where all the functions are independent and not related to each other, most functions specify MY_PHONE_NUMBER or TWILIO_PHONE_NUMBER as the environment variable, thus forcing someone who wants to use function-templates with different phone numbers to structurally alter the code to make it function. To the less perceptive it may cause them to change the var and change the behavior of another function.

I'd rather see us provide details on how to update a function to use shared environment rather than make them the default.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I definitely get your point. I think my only thought about environment variables vs query params is that they are more beginner friendly plus the serverless tooling is already letting people know that they should configure those variables.

Having environment variables with the same name can certainly be both a pro or a con. One of the things we considered when building the tooling was the ability to compose multiple templates together. For example you might start your project with twilio serverless:init example --template forward-call which uses the MY_PHONE_NUMBER. Later you want to be able to also forward your text messages. This would only require you to run inside the same project twilio serverless:new sms --forward-message and after that you'd only have to configure the webhook URL without any other changes.

At the same time I can definitely also see the frustration of getting a clashing behavior here.

The other benefit of environment variables is that we could further improve the tooling to ask people if they intend to use the already configured env variable or not. That's a bit harder with query params.

The other thing with query params is that the event can be both populated via query parameters and POST body and it can sometimes be confusing which one will be populated by Twilio and which one you have to set yourself.

I'm not too opinionated on either solution for your template. If you feel strongly about not offering configuration via env variables that's fine by me too. We just lack more documentation around query params than on environment variables atm.

Copy link
Author

@Gipetto Gipetto Sep 8, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think, too, I just noticed that you wrote

I think my only thought about environment variables vs query params is that they are more beginner friendly plus the serverless tooling is already letting people know that they should configure those variables.

This function uses hard coded vars in the function template and doesn't take any config options via get params. So there's nothing more needed than to update the function before it is deployed. I don't think I could advocate allowing for changing core configuration via GET params - sounds like a huge security issue. So I think this alleviates most of your concerns here around the discoverability of params... they're simply hard coded.

So maybe some clarification in this read me is in order.

| `timeout` | The dial timeout. This is the amount of time that the function will allow your phone to ring before forwarding to voicemail. Default: 12 | yes |
| `secureRecordingLinks` | By default recording urls link directly to the stored message. If you have enabled authentication on your media urls then your voicemail notifications will receive a link to recordings via the Twilio Console. Default: true | yes |
| `voiceOpts` | Control the voice and language preference for the voice prompts. See the docs for details: https://www.twilio.com/docs/voice/twiml/say#attributes-voice | yes |
| `voiceMailMessage` | Control the prompt for callers to leave you a message. Optionally this can be a full url to a recording that will be played instead of using text to speech. | yes |
| `reject` | A list of E164 formatted phone numbers that will be rejected. Calls from these numbers will not be forwarded or allowed to leave a message. | yes |
| `rejectMessage` | Control the prompt to rejected callers. Setting this value to `false` will hang up on blocked callers without informing them that they've been rejected. | 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=personal-voicemail && cd personal-voicemail
```

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 https://localhost:3000/index.html 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
```
147 changes: 147 additions & 0 deletions personal-voicemail/functions/personal-voicemail.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/**
* Personal Voicemail.
*
* See ReadMe for details on
* the configuration options.
*
**/

/*****************************/
/******* configuration *******/

const options = {
phoneNumber: '',
defaultTimeout: 12,
secureRecordingLinks: true,
voiceOpts: {
voice: 'alice',
language: 'en-US'
},
voiceMailMessage: "Hello, I can not answer the phone right now. Please leave a message. Hang up when you're finished.",
reject: [
// To block a caller, add the E164 formatted number here
],
rejectMessage: "You are calling from a restricted number. Goodbye."
}

/***** end configuration *****/
/*****************************/

exports.handler = function(context, event, callback) {
const {
phoneNumber,
defaultTimeout,
secureRecordingLinks,
voiceOpts,
voiceMailMessage,
reject,
rejectMessage
} = options;

const thisFunction = 'https://' + context.DOMAIN_NAME + '/personal-voicemail';
const timeout = event.timeout || defaultTimeout;

function shouldReject() {
return reject.length > 0 && event.From && reject.includes(event.From);
}

function rejectInbound() {
let twiml = new Twilio.twiml.VoiceResponse();
if (rejectMessage) {
twiml.say(rejectMessage, voiceOpts);
}
twiml.hangup();
return twiml;
}

function handleInbound() {
const dialParams = {
'action': thisFunction + '?handle-voicemail'
};

if (event.CallerId) {
dialParams.callerId = event.CallerId;
}

if (timeout) {
dialParams.timeout = timeout;
}

const twiml = new Twilio.twiml.VoiceResponse();
twiml.dial(dialParams, phoneNumber);

return twiml;
}

function startVoicemail() {
const twiml = new Twilio.twiml.VoiceResponse();

const prompt = voiceMailMessage.trim();

if (prompt.match(/^(https?:\/\/[^\s]+)$/)) {
twiml.play(prompt);
} else {
twiml.say(prompt, voiceOpts);
}

twiml.record({
action: thisFunction + '?notify-voicemail',
timeout: '10'
});

return twiml;
}

function notifyVoicemail() {
const message = {
to: phoneNumber,
from: event.To,
body: 'New voicemail from ' + event.From
};

if (secureRecordingLinks) {
message.body += ' - https://www.twilio.com/console/voice/logs/calls/' + event.CallSid;
} else {
message.mediaUrl = event.RecordingUrl + '.mp3';
}

context.getTwilioClient().messages.create(message)
.then(msg => callback(null, msg.sid))
.catch(callback);
}

function redirect() {
const twiml = new Twilio.twiml.VoiceResponse();
twiml.redirect(thisFunction);
return twiml;
}

function hangup() {
const twiml = new Twilio.twiml.VoiceResponse();
twiml.hangup();
return twiml;
}

switch (true) {
case event.CallStatus === 'queued':
callback(null, redirect());
break;
case event.CallStatus === 'ringing':
const rejectOrHandle = shouldReject() ?
rejectInbound() :
handleInbound();
callback(null, rejectOrHandle);
break;
case ['canceled', 'busy', 'failed'].includes(event.CallStatus):
case event.CallStatus === 'in-progress' && event.DialCallStatus === 'no-answer':
callback(null, startVoicemail());
break;
case event.CallStatus === 'completed' && event.Digits === 'hangup':
notifyVoicemail();
break;
default:
callback(null, hangup());
}
};

exports.options = options;
4 changes: 4 additions & 0 deletions personal-voicemail/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "personal-voicemail",
"dependencies": {}
}
Loading