DEV Community

Cover image for Masking phone numbers of couriers and customers using a key-value store
Irina Maximova
Irina Maximova

Posted on • Edited on

Masking phone numbers of couriers and customers using a key-value store

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:
Scheme

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

  1. 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.

  2. 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.

  3. 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.
    Rule

  4. 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.

    Phone number purchase

    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.

  5. The last thing is to attach the phone number to your application. To do that, open your application, go to the NumbersAvailable 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.

  1. Go to your project folder and install the SDK using pip:
    python -m pip install --user voximplant-apiclient

  2. 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.

  3. 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));
        });
    });
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

Then, handle the disconnect event:

secondCall.addEventListener(CallEvents.Disconnected, () => {
    store.call.hangup();
});
Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode

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));
        });
    });
};
Enter fullscreen mode Exit fullscreen mode

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.