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
Twilio number
ngrok account
NodeJS (I recommend using NVM 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.
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash
After installing NVM, restart your console and then run
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.
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
twilio serverless:init –empty voice-proxy
CD into the generated folder and add the following to your .env file
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.
/**
* 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.
…
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.
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.
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.
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
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
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
ngrok authtoken xxxxxxxxxxxxxxxxxxxxxxxxxxxx
Or, you can manually create an ngrok.yml in the following directory.
/home/<YourUsername>/.ngrok2/ngrok.yml
authtoken: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Now that you’re logged in, run this command to connect your twilio number to your local function.
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
twilio serverless:deploy
To deploy your project to the serverless network. The output should give you back a url to your functions like this:
✔ 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.
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?
Top comments (0)