The pandemic has greatly affected delivery services, and they have become more in demand than ever before. As we all know, a courier and a customer need each other's phone numbers to make a call and discuss the order details. But what about privacy? Many delivery services have already thought this question through, and each uses its own number masking solution. I, in turn, want to tell you how to mask phone numbers with the Voximplant key-value store. So let’s get the party started!
How it works
We will create a scenario that allows a customer and a courier to make calls without knowing each other’s phone numbers.
Meanwhile, we’ll have only one so-called “neutral” phone number to call for both a courier and a customer. We’ll rent this number in the Voximplant panel. Then, we’ll create some data structure to connect a courier and a customer with each other by an order number (a key if we are referring to a key-value store).
When calling a rented number, a caller enters their order number. If such an order exists in the database, our scenario checks the numbers attached to it. Then, if it identifies a phone number as a customer’s, we put them through to the courier responsible for the order, and vice versa.
For example, a call from a courier to a customer looks like this:
If it doesn’t find the caller’s phone number in the database, we suggest they call again from the number they used when placing the order. Or we simply transfer them to an operator.
Let’s go straight to the implementation.
What you need
- A Voximplant account which you can create here;
- A Voximplant application with a scenario and a rule for this scenario (we’ll create it all together);
- Test phone numbers: a number rented from Voximplant, a courier’s, a customer’s, and an operator’s numbers. In the test version, we can omit an operator’s number.
To begin, log into your Voximplant account: manage.voximplant.com/auth. In the menu on the left, click Applications and then click Create application in the upper right corner. Give it a name (for example, numberMasking) and click Create.
Open a newly created app and go to Scenarios. Create a scenario by clicking the “+” button and name it kvs-scenario. You will work with the code here, but everything is not quite ready yet; wait until we add code to the scenario.
Go to the Routing tab and create a rule for the scenario. Leave the pattern (regular expression) “.*” as default. This way your rule will work for all phone numbers.
-
Rent a real phone number. To do that, go to the Numbers section, select one and purchase it. This number will be called by a courier and a customer and will be displayed to them instead of their personal numbers.
Please note that phone numbers are offered on-demand, except in countries where regulations require additional steps. You will find more information when renting a phone number.
In Voximplant, you can also rent test numbers to see how everything works. In our case, we need a real phone number to initiate a call from the platform.
The last thing is to attach the phone number to your application. To do that, open your application, go to the Numbers → Available and click Attach. Here you can also attach your rule so it works for incoming calls, and all other rules are ignored.
Great! The structure is ready, now you just need to fill the key-value store and add some code to the scenario.
Key-value store
The scenario works properly if the key-value store isn’t empty. To fill it up, use the Voximplant Management API. I use the Python API client. It requires Python 2.x or 3.x with pip and setuptools> = 18.5 installed.
Go to your project folder and install the SDK using
pip
:
python -m pip install --user voximplant-apiclient
-
Create a .py file and write the code that adds the order details to the key-value store. The set_key_value_item will help you do that:
from voximplant.apiclient import VoximplantAPI, VoximplantException if __name__ == "__main__": voxapi = VoximplantAPI("credentials.json") # SetKeyValueItem example KEY = 12345 VALUE = '{"courier": "12222222222", "client": "13333333333"}' APPLICATION_ID = 1 TTL = 864000 try: res = voxapi.set_key_value_item(KEY, VALUE, APPLICATION_ID, ttl=TTL) print(res) except VoximplantException as e: print("Error: {}".format(e.message))
You can generate a credentials.json file yourself when creating a service account in the Service accounts section. Don’t forget to choose a role that will allow you to call the
set_key_value_item
method. Owner, for example, will do well.Find the APPLICATION_ID in the address bar when navigating to your app.
We use a five-digit order number as a key (KEY) and phone numbers as values. TTL here is to specify the values' storage period.
-
Finally, run the file to save the order details:
python kvs.py
In case you no longer want a client and a courier to disturb each other, you can delete the order details from the store. Find all the available key-value store methods in our documentation: management API and VoxEngine.
Scenario code
The kvs-scenario code is below, you can copy it as is. You just need to do one more thing – specify the number you rented in the Voximplant control panel as callid in the form of "10000000000":
Full scenario code
require(Modules.ApplicationStorage);
/**
* @param {boolean} repeatAskForInput - whether the input request was repeated
* @param longInputTimerId - timer for the absence of input
* @param shortInputTimerId - timer for triggering the phrase about contacting an operator
* @param {boolean} firstTimeout - indicator of the first timeout
* @param {boolean} wrongPhone - indicator that the caller's number matches the number from storage
* @param {boolean} inputRecieved - whether the input was received from the user
*
*/
let repeatAskForInput;
let longInputTimerId;
let shortInputTimerId;
let firstTimeout = true;
let wrongPhone;
let inputRecieved;
const store = {
call: null,
caller: '',
callee: '',
callid: 'phone number rented in the panel',
operator_call: null,
operatorNumber: '',
input: '',
data: {
call_operator: '',
order_number: '',
order_search: '',
phone_search: '',
sub_status: '',
sub_available: '',
need_operator: '',
call_record: ''
}
}
const phrases = {
start: 'Hello. Please -- enter the five-digit order number in tone mode.',
repeat: 'Please -- enter the five-digit order number in tone mode, or press pound to contact an operator.',
noInputGoodbye: 'You have not chosen anything. You can look up the order number in the text message and call us again. Goodbye, have a nice day!',
connectToOpearator: 'To contact an operator, press pound.',
connectingToOpearator: 'Stay on the line, putting you through to an operator.',
operatorUnavailable: 'Unfortunately, all operators are busy.. Please,,, call back later. Goodbye, have a nice day!',
wrongOrder: 'Order number is not found. Look up the order number in the text message and enter it in tone mode. Or contact an operator by pressing pound.',
wrongOrderGoodbye: 'You didn’t choose anything.. Goodbye, have a nice day!',
wrongPhone: 'Phone number is not found. If you are a customer, please call back from the number you used to place your order. If you are a courier, please call back from the number that is registered in our system. Or contact an operator by pressing pound.',
wrongPhoneGoodbye: 'You didn’t choose anything.. Goodbye, have a nice day!',
courierIsCalling: `A courier is calling you about the order delivery, - - ${store.data.order_number}`,
clientIsCalling: `A customer is calling you about the order delivery, - - ${store.data.order_number}`,
courierUnavailable: 'It seems like the courier is unavailable at the moment. Please call back in a couple of minutes. Goodbye, have a nice day!',
clientUnavailable: 'It seems like the customer is unavailable at the moment. Please call back in a couple of minutes. Goodbye, have a nice day!',
waitForCourier: 'Stay on the line, putting you through to the courier.',
waitForClient: 'Stay on the line, putting you through to the customer.'
}
VoxEngine.addEventListener(AppEvents.Started, async e => {
VoxEngine.addEventListener(AppEvents.CallAlerting, callAlertingHandler);
})
async function callAlertingHandler(e) {
store.call = e.call;
store.caller = e.callerid;
store.call.addEventListener(CallEvents.Connected, callConnectedHandler);
store.call.addEventListener(CallEvents.Disconnected, callDisconnectedHandler);
store.call.answer();
}
async function callDisconnectedHandler(e) {
await sendResultToDb();
VoxEngine.terminate();
}
async function callConnectedHandler() {
store.call.handleTones(true);
store.call.addEventListener(CallEvents.RecordStarted, (e) => {
store.data.call_record = e.url;
});
store.call.record();
store.call.addEventListener(CallEvents.ToneReceived, dtmfHandler);
await say(phrases.start);
addInputTimeouts();
}
function dtmfHandler(e) {
clearInputTimeouts();
store.input += e.tone;
Logger.write('Entered digit is ' + e.tone)
Logger.write('Full number ' + store.input)
if (e.tone === '#') {
store.data.need_operator = "Yes";
store.call.removeEventListener(CallEvents.ToneReceived);
store.call.handleTones(false);
callOperator();
return;
}
if (!wrongPhone) {
if (store.input.length >= 5) {
repeatAskForInput = true;
Logger.write(`Received number is ${store.input}. `);
store.call.handleTones(false);
store.call.removeEventListener(CallEvents.ToneReceived);
handleInput(store.input);
return;
}
}
addInputTimeouts();
}
function addInputTimeouts() {
clearInputTimeouts();
if (firstTimeout) {
Logger.write('Timer for the phrase about contacting an operator is triggered');
shortInputTimerId = setTimeout(async () => {
await say(phrases.connectToOpearator);
}, 1500);
firstTimeout = false;
}
longInputTimerId = setTimeout(async () => {
Logger.write('Timer for no input from the user is triggered ' + longInputTimerId);
store.call.removeEventListener(CallEvents.ToneReceived);
store.call.handleTones(false);
if (store.input) {
handleInput(store.input);
return;
}
if (!repeatAskForInput) {
Logger.write('Asking the caller to re-enter the number');
store.call.handleTones(true);
store.call.addEventListener(CallEvents.ToneReceived, dtmfHandler);
await say(phrases.repeat);
addInputTimeouts();
repeatAskForInput = true;
} else {
Logger.write('Number is not entered. Ending the call');
await say(inputRecieved ? phrases.wrongOrderGoodbye : phrases.noInputGoodbye);
store.call.hangup();
}
}, 8000);
Logger.write('Timer for no input from the user is triggered ' + longInputTimerId);
}
function clearInputTimeouts() {
Logger.write(`Clearing the timer ${longInputTimerId}. `);
if (longInputTimerId) clearTimeout(longInputTimerId);
if (shortInputTimerId) clearTimeout(shortInputTimerId);
}
async function handleInput() {
store.data.order_number = store.input;
Logger.write('Looking for a match in the key-value store by the entered number: ' + store.input)
inputRecieved = true;
let kvsAnswer = await ApplicationStorage.get(store.input);
if (kvsAnswer) {
store.data.order_search = 'Order is found';
Logger.write('Received response from kvs: ' + kvsAnswer.value)
let { courier, client } = JSON.parse(kvsAnswer.value);
if (store.caller == courier) {
Logger.write('Courier is calling')
store.callee = client;
store.data.sub_status = 'Courier';
store.data.phone_search = 'Phone number is found';
callCourierOrClient();
} else if (store.caller == client) {
Logger.write('Customer is calling')
store.callee = courier;
store.data.sub_status = 'Customer';
store.data.phone_search = 'Phone number is found';
callCourierOrClient();
} else {
Logger.write('Number of the caller does not match the numbers received from kvs');
wrongPhone = true;
store.data.phone_search = 'Phone number is not found';
store.input = '';
store.call.handleTones(true);
store.call.addEventListener(CallEvents.ToneReceived, dtmfHandler);
await say(phrases.wrongPhone);
addInputTimeouts();
}
} else {
Logger.write('No match in kvs for the entered number');
store.data.order_search = 'Order is not found';
store.input = '';
store.call.handleTones(true);
store.call.addEventListener(CallEvents.ToneReceived, dtmfHandler);
await say(phrases.wrongOrder);
Logger.write(`Clearing the timer ${longInputTimerId}. `);
addInputTimeouts();
}
}
async function callCourierOrClient() {
clearInputTimeouts();
Logger.write('Starting a call to the courier/customer');
await say(store.data.sub_status === 'Courier' ? phrases.waitForClient : phrases.waitForCourier, store.call);
const secondCall = VoxEngine.callPSTN(store.callee, store.callid);
store.call.startPlayback('http://cdn.voximplant.com/toto.mp3');
secondCall.addEventListener(CallEvents.Connected, async () => {
store.data.sub_available = 'Yes';
await say(store.data.sub_status === 'Courier' ? phrases.courierIsCalling : phrases.clientIsCalling, secondCall);
store.call.stopPlayback();
VoxEngine.sendMediaBetween(store.call, secondCall);
});
secondCall.addEventListener(CallEvents.Disconnected, () => {
store.call.hangup();
});
secondCall.addEventListener(CallEvents.Failed, async () => {
store.data.sub_available = 'No';
store.call.stopPlayback();
await say(store.data.sub_status === 'Courier' ? phrases.clientUnavailable : phrases.courierUnavailable, store.call);
store.call.hangup();
});
}
async function callOperator() {
Logger.write('Starting a call to an operator');
await say(phrases.connectingToOpearator, store.call);
store.call.startPlayback('http://cdn.voximplant.com/toto.mp3');
store.operator_call = VoxEngine.callPSTN(store.operatorNumber, store.callid);
store.operator_call.addEventListener(CallEvents.Connected, async () => {
store.data.call_operator = 'Operator is free';
VoxEngine.sendMediaBetween(store.call, store.operator_call);
});
store.operator_call.addEventListener(CallEvents.Disconnected, () => {
store.call.hangup();
});
store.operator_call.addEventListener(CallEvents.Failed, async () => {
store.data.call_operator = 'Operator is busy';
await say(phrases.operatorUnavailable, store.call);
store.call.hangup();
});
}
async function sendResultToDb() {
Logger.write('Data to be sent to the database');
Logger.write(JSON.stringify(store.data));
const options = new Net.HttpRequestOptions();
options.headers = ['Content-Type: application/json'];
options.method = 'POST';
options.postData = JSON.stringify(store.data);
await Net.httpRequestAsync('https://voximplant.com/', options);
}
function say(text, call = store.call, lang = VoiceList.Amazon.
en_AU_Nicole) {
return new Promise((resolve) => {
call.say(text, lang);
call.addEventListener(CallEvents.PlaybackFinished, function callback(e) {
resolve(call.removeEventListener(CallEvents.PlaybackFinished, callback));
});
});
};
The code is thoroughly commented on but let's go into more detail on some points.
Enter the order number
The first thing we do when a call arrives is ask a caller to enter the order number and handle it using the dtmfHandler
function.
store.input += e.tone;
If the caller enters #, put them through to the operator:
if (e.tone === '#') {
store.data.need_operator = "Yes";
store.call.removeEventListener(CallEvents.ToneReceived);
store.call.handleTones(false);
callOperator();
return;
}
If they enter a 5-digit number, call the handleInput
method:
if (store.input.length >= 5) {
repeatAskForInput = true;
Logger.write('Received number is ${store.input}. ');
store.call.handleTones(false);
store.call.removeEventListener(CallEvents.ToneReceived);
handleInput(store.input);
return;
}
Search the order
It’s time to compare the entered number with order numbers in the store using the ApplicationStorage.get() method and the entered number as a key here:
store.data.order_number = store.input;
Logger.write('Looking for a match in the key-value store by the entered number: ' + store.input)
inputRecieved = true;
let kvsAnswer = await ApplicationStorage.get(store.input);
If the order is found, get the courier and customer phone numbers connected with it:
if (kvsAnswer) {
store.data.order_search = 'Order is found';
Logger.write('Received response from kvs: ' + kvsAnswer.value)
let { courier, client } = JSON.parse(kvsAnswer.value);
Now we need to figure out to whom to call. If the caller’s number is the courier’s, forward a call to the customer, if it’s the customer’s – to the courier. The callCourierOrClient
function is intended for this:
if (store.caller == courier) {
Logger.write('Courier is calling')
store.callee = client;
store.data.sub_status = 'Courier';
store.data.phone_search = 'Phone number is found';
callCourierOrClient();
} else if (store.caller == client) {
Logger.write('Customer is calling')
store.callee = courier;
store.data.sub_status = 'Customer';
store.data.phone_search = 'Phone number is found';
callCourierOrClient();
}
If the number is not in the store, ask a caller to call again from the number they used when placing the order:
else {
Logger.write('Number of the caller does not match the numbers received from kvs');
wrongPhone = true;
store.data.phone_search = 'Phone number is not found';
store.input = '';
store.call.handleTones(true);
store.call.addEventListener(CallEvents.ToneReceived, dtmfHandler);
await say(phrases.wrongPhone);
addInputTimeouts();
}
Finally, handle what happens when the order number is not in the store. In such a case, ask the caller to make sure the number is correct and enter it again:
else {
Logger.write('No match in kvs for the entered number');
store.data.order_search = 'Order is not found';
store.input = '';
store.call.handleTones(true);
store.call.addEventListener(CallEvents.ToneReceived, dtmfHandler);
await say(phrases.wrongOrder);
Logger.write(`Clearing the timer ${longInputTimerId}. `);
addInputTimeouts();
}
Call the customer/courier
Let's go directly to the call, i.e. to the callCourierOrClient
function. Here we tell the caller that we're transferring their call to the courier/client and play music on hold. We use the callPSTN method to call the client or the courier (depending on whose number was previously identified as the caller's number):
await say(store.data.sub_status === 'Courier' ? phrases.waitForClient : phrases.waitForCourier, store.call);
const secondCall = VoxEngine.callPSTN(store.callee, store.callid);
store.call.startPlayback('http://cdn.voximplant.com/toto.mp3');
At the same time, we tell the callee that the call is about clarification of information on the order:
secondCall.addEventListener(CallEvents.Connected, async () => {
store.data.sub_available = 'Yes';
await say(store.data.sub_status === 'Courier' ? phrases.courierIsCalling : phrases.clientIsCalling, secondCall);
store.call.stopPlayback();
VoxEngine.sendMediaBetween(store.call, secondCall);
});
Then, handle the disconnect event:
secondCall.addEventListener(CallEvents.Disconnected, () => {
store.call.hangup();
});
Notify the caller if the callee is unavailable:
secondCall.addEventListener(CallEvents.Failed, async () => {
store.data.sub_available = 'No';
store.call.stopPlayback();
await say(store.data.sub_status === 'Courier' ? phrases.clientUnavailable : phrases.courierUnavailable, store.call);
store.call.hangup();
});
The say
method is responsible for all the phrases the robot utters. The phrases themselves are in the phrases associative array. We use Amazon as a TTS provider, the voice of Nicole:
function say(text, call = store.call, lang = VoiceList.Amazon.
en_AU_Nicole) {
return new Promise((resolve) => {
call.say(text, lang);
call.addEventListener(CallEvents.PlaybackFinished, function callback(e) {
resolve(call.removeEventListener(CallEvents.PlaybackFinished, callback));
});
});
};
Among other things, our scenario records the calls using the record method and enables you to save statistics to the database. In our code, the sendResultToDb
function handles this. This is very important for business because it allows you to analyze statistics, provide quality control and quickly resolve any issues that might arise during the delivery process.
Test the app
When you add the full code to the scenario and the order details to the storage, feel free to start testing.
Let's call from the customer's or courier's phone number to the number rented in the panel. Then enter the order number (in our case, it is 12345) and wait for the connection with the other party.
If we do everything correctly, the customer and the courier will be able to call each other and discuss the details of the order without knowing each other's personal numbers, and thus without any privacy issues.
I’m glad you read all the way to the end of this article. It means it was gripping, right? :) Anyway, I wish you successful development and trouble-free order delivery! Stay tuned for more helpful articles in the future.
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.