Make a serverless phone proxy and voicemail in Twilio
Tags: serverless, twilio, javascript, tutorial
Published: 2023-04-06 19:16:22

Communication in the current world is complicated. We want to be connected to everyone, but at the same time, we want boundaries and space. We don’t want to juggle 7 different phones for work, relationships, and personal friends, but we want a central place to be able to communicate with them all. It would be nice if we could have a proxy phone number, allowing people to contact us without having our personal number.

That’s exactly what we will be doing in this post. We are going to make a phone number and voicemail proxy that allows us to only receive calls from a certain time and can leave a voicemail for us. If the owner calls, it will let you listen to voicemails, and if anyone else calls, it will forward the call or let the caller leave a voicemail.

We will be developing and testing the application locally before launching this as a serverless application on twilio’s cloud.

Pre-reqs
[Twilio account](https://console.twilio.com/)
[Twilio number](https://console.twilio.com/)
[ngrok account](https://ngrok.com/)
NodeJS (I recommend using [NVM](https://github.com/nvm-sh/nvm#installing-and-updating) to manage Node Versions)

### Configure your environment

First, we want to make sure that you have NodeJS v14 installed. We will install nvm and use it to set version 14 manually as it is not the most recent version of NodeJS.

``` bash
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash
```

After installing NVM, restart your console and then run

``` bash
nvm install 14
nvm use 14
```

Now we need to install and configure the Twilio Cli. You’ll need to sign up for a Twilio account if you haven’t already. It requires putting in your account id and secret found on the [twilio dashboard](https://console.twilio.com/).

``` bash
npm install twilio-cli -g
twilio login
twilio plugins:install @twilio-labs/plugin-serverless
```

If this is your first time using the Twilio CLI and you don’t have an active profile set yet, run twilio profiles:use PROFILE_ID once before trying any of the following serverless commands.

### Create the service

Let’s generate a new serverless twilio project called voice-proxy by running 

``` bash
twilio serverless:init –empty voice-proxy
```

CD into the generated folder and add the following to your .env file

``` bash
ACCOUNT_SID=(autofilled from generation)
AUTH_TOKEN=//get from twilio website console
MY_PHONE_NUMBER='+1xxxxxxxxxx' (Your phone number)
TWILIO_PHONE_NUMBER='+1xxxxxxxxxx'
```

### Write the proxy
Let’s start writing our functions. All of the functions we create will go into the `functions` folder in our project.

Our main file will check who is calling. If the owner of the voicemail is calling, it will forward them to their voicemail. Otherwise, if a regular person is calling, it will transfer them to the call handling function.

``` javascript
/**
 * Returns TwiML that prompts the users to make a choice.
 * If the user enters something it will trigger the handle-user-input Function and otherwise go in a loop.
 */
 exports.handler = function (context, event, callback) {
    const OWNERPHONE = context.MY_PHONE_NUMBER
    
    const twiml = new Twilio.twiml.VoiceResponse();
  
    // When the owner of the voicemail calls in, we transfer
    // them right to the voicemail
    if (event.From === OWNERPHONE)  {
      twiml.redirect("./voicemail-loop?index=0")
    } else {
      // We connect the unknown caller to us with twiml.dial
      // if the call completes or times out we move to the handler in the action option
      twiml.dial({
        action: 'handle-call',
        timeout: 17
      }, OWNERPHONE)
    }
    
    callback(null, twiml);
  };
```

### Code voicemail loop

First, we will make an api call to pull down the voicemails connected to our account and play them.

``` javascript
…
const VOICEMAIL_NUMBER = context.TWILIO_PHONE_NUMBER;

// Get list of voicemails. Sort by newest first
  const voicemailClean = [];
  let voicemailList = await client.recordings.list({to: VOICEMAIL_NUMBER});
  // split string, add mp3 at the end, loop through playing them
  voicemailList.forEach(function (c) {
    let audioUri = `https://api.twilio.com${c.uri.slice(0, -4)}mp3`;
    voicemailClean.push(audioUri);
  });

voicemailClean.forEach(item => twiml.play(item))
```

We want to add gather commands so that we can delete or replay messages. In order to integrate gather commands, we need to be able to redirect back to this function after each option select. To make this work, we’ll make this function be able to call back to itself and keep track of the index of which voicemail we are on by carrying the index over.

``` javascript
exports.handler = async function(context, event, callback) {
  // Here's an example of setting up some TWiML to respond to with this function
	let twiml = new Twilio.twiml.VoiceResponse();
  // Initiate a Twilio API client. Make sure you enabled exposing username and password
  const client = context.getTwilioClient();

  let Index = parseInt(event.index) || 0;
  let UserInput = event.Digits || event.SpeechResult || '1';
  const VOICEMAIL_NUMBER = context.TWILIO_PHONE_NUMBER;


  // Get list of voicemails. Sort by newest first
  const voicemailClean = [];
  let voicemailList = await client.recordings.list({to: VOICEMAIL_NUMBER});
  // split string, add mp3 at the end, loop through playing them
  voicemailList.forEach(function (c) {
    let audioUri = `https://api.twilio.com${c.uri.slice(0, -4)}mp3`;
    voicemailClean.push(audioUri);
  });


  if (UserInput.length > 1) {
    if (UserInput.toLowerCase().includes('next')) {
      UserInput = '1';
    } else if (UserInput.toLowerCase().includes('replay')) {
      UserInput = '2';
    } else if (UserInput.toLowerCase().includes('delete')) {
      UserInput = '3';
    } else if (UserInput.toLowerCase().includes('restart')) {
      UserInput = '9';
    }
  }

  switch (UserInput) {
    case '1':
      // Do nothing
      break;
    case '2':
      Index = Index - 1;
      break;
    case '3':
      let deleteResponse = await client.recordings(voicemailList[Index - 1].sid).remove();
      voicemailList.splice(Index - 1, 1);
      voicemailClean.splice(Index - 1, 1)
      Index = Index - 1;
      twiml.say('Message deleted');
      break;
    case '9':
      Index = 0
      break;
    default:
      twiml.say('We are sorry, we did not recognize your option. Please try again.');
      twiml.redirect('voicemail-loop');
  }


  // Someone accidentally hit 1 on the last voicemail
  if (Index === voicemailList.length) {
    Index = 0;
  }

  if (voicemailList.length === 0) {
    twiml.say('You have no messages. Goodbye');
    twiml.hangup();
  }

  if (Index === 0) {
    twiml.say(`You have ${voicemailList.length} messages`);
  }


  twiml.say(`Message ${Index + 1} of ${voicemailList.length}`);
  twiml.play(voicemailClean[Index]);

  const gather = twiml.gather({
    numDigits: 1,
    action: `https://voice-ivr1-3843-cmbffn.twil.io/voicemail-loop?index=${Index + 1}`,
    hints: 'next, replay, delete, restart',
    input: 'speech dtmf',
  });


  if (Index + 1 !== voicemailList.length) {
    gather.say('Press 1 or say next to play the next message');
  }
  gather.say('Press 2 or say replay to replay this message');
  gather.say('Press 3 or say delete to delete this message');
  if (Index + 1 === voicemailList.length) {
    gather.say('To hear your voicemails again, press 9 or say restart');
  }


  // This callback is what is returned in response to this function being invoked.
  // It's really important! E.g. you might respond with TWiML here for a voice or SMS response.
  // Or you might return JSON data to a studio flow. Don't forget it!
  return callback(null, twiml);
};
```

### Handle call

Now that the voicemail reading is handled, let’s handle when a caller leaves a voicemail.

``` javascript
 exports.handler = function (context, event, callback) {
    
    const twiml = new Twilio.twiml.VoiceResponse();
    // The call timed out or didn't complete. We will take a voicemail.
    // Otherwise, the call completed and we exit
    if (event.DialCallStatus === 'no-answer' || event.DialCallStatus === 'failed' || event.DialCallStatus === 'busy' || event.DialCallStatus === 'canceled') {
        twiml.say('Please record your message after the tone. Press 1 when youre done recording');
        twiml.record({
          transcribe: true,
          timeout: 10,
          finishOnKey: '1',
          action: 'voicemail-complete',
          maxLength: 30
        });
    }
    
    callback(null, twiml);
  };
```

### Complete voicemail

We need a handler for when the voicemail is recorded, so let’s make it here.

``` javascript
exports.handler = function(context, event, callback) {
    // Here's an example of setting up some TWiML to respond to with this function
    let twiml = new Twilio.twiml.VoiceResponse();
    twiml.say('Your message has been saved. Goodbye!');
    twiml.hangup();
    return callback(null, twiml);
  };
```

### Test functions locally

Now, let’s run our functions locally and test them. Run

``` bash
twilio serverless:start
```

In the root directory of the project. If you want to run it in the background and use the same tab, run

``` bash
twilio serverless:start &
```

Open a second terminal tab and configure ngrok. Log into your ngrok account and run this to add your credentials. twilio-run uses v2 of ngrok. You can [download v2 of ngrok from here](https://dl.equinox.io/ngrok/ngrok/stable/archive)

``` bash
ngrok authtoken xxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

Or, you can manually create an ngrok.yml in the following directory.

`/home/<YourUsername>/.ngrok2/ngrok.yml`

``` bash
authtoken: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

Now that you’re logged in, run this command to connect your twilio number to your local function.

``` bash
twilio phone-numbers:update "+1xxxxxxxxxx" --voice-url http://localhost:3000/voicemail-proxy
```

Now you can call your twilio number and execute your function.

### Test call

Use another phone you have or ask a friend to call your twilio number and leave a voicemail. Now try calling from your phone number and listen to the voicemail. Pretty cool, huh?

### Deploy

Now that you know it works, let’s deploy it to twilio’s network. Run

``` bash
twilio serverless:deploy
```

To deploy your project to the serverless network. The output should give you back a url to your functions like this:

``` bash
✔ Serverless project successfully deployed
……..
Functions:
   https://proxy-test-1300-dev.twil.io/handle-call
   https://proxy-test-1300-dev.twil.io/voicemail-complete
   https://proxy-test-1300-dev.twil.io/voicemail-loop
   https://proxy-test-1300-dev.twil.io/voicemail-proxy
Assets:
```

Copy the output to your `voicemail-proxy` url and connect it to your twilio number.

``` bash
twilio phone-numbers:update "+1xxxxxxxxxx" --voice-url http://your-url/voicemail-proxy
```

Now try calling the number and testing it. Congratulations! Your project works and is deployed serverlessly.

### Conclusion

A voicemail proxy like this can be used for multiple reasons. It can allow you to have a phone number for each area of your life, like a number for managing business calls, a number for meeting new people, etc, all without giving out your personal phone number. But honestly I just like it because I think it's kinda cool. What do you think?