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 voice-queue template #149

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
67 changes: 37 additions & 30 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"log-symbols": "^3.0.0",
"moment": "^2.24.0",
"stripe": "^8.20.0",
"twilio": "^3.43.0"
"twilio": "^3.50.0"
},
"jest": {
"testEnvironment": "node",
Expand Down
7 changes: 6 additions & 1 deletion templates.json
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,11 @@
"id": "verify-dashboard",
"name": "Verify Testing Dashboard",
"description": "Helpful dashboard for testing and debugging during your development with Twilio Verify."
},
{
"id": "voice-queue",
"name": "Voice Queue with hold music",
"description": "Inbound calls are queued, and hold music is played until the intended recipient answers the call."
}
]
}
}
14 changes: 14 additions & 0 deletions voice-queue/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# description: The URL for the hold music you want to play. The default is classic.mp3.
# format: url
# required: true
HOLD_MUSIC_URL=https://demo.twilio.com/docs/classic.mp3

# description: Your personal number to forward the inbound caller to.
# format: phone_number
# required: true
YOUR_PHONE_NUMBER=+12223334444

# description: The Twilio number used to forward the call.
# format: phone_number
# required: true
TWILIO_PHONE_NUMBER=+15555555555
61 changes: 61 additions & 0 deletions voice-queue/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Voice Queue

A simple call queuing system. Place the inbound caller into a queue with hold music while waiting for the intended recipient to answer.

## Pre-requisites

### Environment variables

This project requires some environment variables to be set. 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 |
| :-------------------- | :--------------------------------------------------- | :------- |
| `HOLD_MUSIC_URL` | The URL for the hold music you want to play. | true |
| `YOUR_PHONE_NUMBER` | Your personal number to forward the inbound call to. | true |
| `TWILIO_PHONE_NUMBER` | The Twilio number used to forward the call. | true |

### Function Parameters

`/dial-queue` expects the following parameters:

| Parameter | Description | Required |
| :-------- | :-------------------------------------------------------------- | :------- |
| `CallSid` | Use the inbound CallSid as the unique identifier for the Queue. | true |
| `From` | Whisper who is calling to the intended recipient. | true |

## 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=voice-queue && cd voice-queue
Copy link
Contributor

Choose a reason for hiding this comment

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

Should name this project voice-queue not example (or change to cd into 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 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
```
101 changes: 101 additions & 0 deletions voice-queue/assets/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Get started with your Twilio Functions!</title>
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic">
<link rel="stylesheet" href="https://unpkg.com/normalize.css/normalize.css">
<link rel="stylesheet" href="https://unpkg.com/milligram/dist/milligram.min.css">
<style>
body {
padding: 20px;
display: flex;
flex-direction: column;
min-height: 100vh;
}

div[role="main"] {
flex: 1;
}

footer {
text-align: center;
}

footer p {
margin-bottom: 0;
}

#twilio-logo {
width: 50px;
height: 50px;
}
</style>
</head>
<body>
<header>
<h1>Welcome to your new Twilio App</h1>
</header>
<div role="main">
<section>
<h3>Get started with your app</h3>
<p>Now that your code is deployed, here are the last steps you need to do to finish your app.</p>
<ol>
<li>Have anyone call your Twilio app.</li>
<li>The caller will hear music while they are on hold.</li>
<li>An outbound call will dial your personal number, and connect you to the initiated caller.</li>
</ol>
</section>
<section>
<h3>Troubleshooting</h3>
<ul>
<li>Make sure the Twilio phone number you want your app has a voice webhook configured to point at
</li>
<p>
<pre><code><span class="function-root"></span>/enqueue</code></pre>
</p>
<li>Check the
<code>README.md</code>
of your project</li>
</ul>
</section>
</div>
<footer>
<p>
<a href="https://www.twilio.com/">
<svg id="twilio-logo" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewbox="0 0 60 60">
<defs>
<style>
.cls-1 {
fill: #f22f46;
}
</style>
</defs>
<title>Twilio Logo</title><path class="cls-1" d="M30,15A15,15,0,1,0,45,30,15,15,0,0,0,30,15Zm0,26A11,11,0,1,1,41,30,11,11,0,0,1,30,41Zm6.8-14.7a3.1,3.1,0,1,1-3.1-3.1A3.12,3.12,0,0,1,36.8,26.3Zm0,7.4a3.1,3.1,0,1,1-3.1-3.1A3.12,3.12,0,0,1,36.8,33.7Zm-7.4,0a3.1,3.1,0,1,1-3.1-3.1A3.12,3.12,0,0,1,29.4,33.7Zm0-7.4a3.1,3.1,0,1,1-3.1-3.1A3.12,3.12,0,0,1,29.4,26.3Z"/></svg>
</a>
</p>
<p>
<a href="https://www.twilio.com/code-exchange">Learn about other things you can build with Twilio</a>
</p>
</footer>

<script>
// This code will replace the content of any <span class="function-root"></span> with the base path of the URL
const baseUrl = new URL(location.href);
baseUrl.pathname = baseUrl
.pathname
.replace(/\/index\.html$/, '');
delete baseUrl.hash;
delete baseUrl.search;
const fullUrl = baseUrl
.href
.substr(0, baseUrl.href.length - 1);
const functionRoots = document.querySelectorAll('span.function-root');

functionRoots.forEach(element => {
element.innerText = fullUrl
})
</script>
</body>
</html>
22 changes: 22 additions & 0 deletions voice-queue/functions/dial-queue.protected.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
exports.handler = function (context, event, callback) {
const fromSid = event.fromCallSid;
// If no fromSid is set, error out.
if (!fromSid) {
return callback({
status: 500,
message: 'No CallSid found. Inbound voice webhook should be set to /enqueue'
Copy link
Contributor

Choose a reason for hiding this comment

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

Returning a string here should be sufficient to trigger a 500 error from the Function. Runtime, as far as I know, won't do anything special with this as an object.

}, null);
}

// Split the phone number out so that <Say> reads every number when identifying the caller.
const from = event.from;
const fromFormatted = from.split('').join(' ');

// Announce who is calling.
const response = new Twilio.twiml.VoiceResponse();
response.say(`You have an incoming call from ${fromFormatted}`);
// Dial into the queue where the initiated caller is waiting.
response.dial().queue(fromSid);

callback(null, response);
};
23 changes: 23 additions & 0 deletions voice-queue/functions/enqueue.protected.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
exports.handler = function (context, event, callback) {
const { CallSid, From } = event;

if (From) {
const client = context.getTwilioClient();
const path = `https://${context.DOMAIN_NAME}`;
// Make an outbound call to your phone number to connect the two legs.
// Use the inbound CallSid as the unique identifier for the queue.
client.calls.create({
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a chance of a race condition here between creating the call and the function ending, as we don't wait for this asynchronous call to complete before calling back?

Also, should the function just fail if there's no From?

So could this all be?

exports.handler = async function (context, event, callback) {
  const { CallSid, From } = event;

  if (!From) {
    callback("Calls must have a `From`.");
  } else {
    const client = context.getTwilioClient();
    const path = `https://${context.DOMAIN_NAME}`;
    // Make an outbound call to your phone number to connect the two legs.
    // Use the inbound CallSid as the unique identifier for the queue.
    try {
      await client.calls.create({
        to: context.YOUR_PHONE_NUMBER,
        from: context.TWILIO_PHONE_NUMBER,
        url: `${path}/dial-queue?fromCallSid=${CallSid}&from=${From}`
      });
      // Enqueue the inbound caller.
      // waitUrl points to an endpoint to <Play> the mp3 set in the env variables.
      const response = new Twilio.twiml.VoiceResponse();
      response.enqueue({
        waitUrl: '/hold-music'
      }, CallSid);
      callback(null, response);
    } catch (error) {
      callback(error);
    }
  }
}; 

to: context.YOUR_PHONE_NUMBER,
from: context.TWILIO_PHONE_NUMBER,
url: `${path}/dial-queue?fromCallSid=${CallSid}&from=${From}`
});
}
// Enqueue the inbound caller.
// waitUrl points to an endpoint to <Play> the mp3 set in the env variables.
const response = new Twilio.twiml.VoiceResponse();
response.enqueue({
waitUrl: '/hold-music'
}, CallSid);

callback(null, response);
};
6 changes: 6 additions & 0 deletions voice-queue/functions/hold-music.protected.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
exports.handler = function (context, event, callback) {
const response = new Twilio.twiml.VoiceResponse();
// HOLD_MUSIC_URL points to the audio file set in your environment variables.
response.play(context.HOLD_MUSIC_URL);
callback(null, response);
};
Loading