Скрываем номера курьеров и клиентов с помощью key-value хранилища

В условиях пандемии курьерские сервисы стали востребованы как никогда прежде. Чтобы клиент и курьер могли созвониться для уточнения информации по заказу, им нужно знать номера телефонов друг друга. А что насчет соблюдения прайваси? Многие сервисы доставок уже озаботились этим вопросом после не очень приятных инцидентов, о которых вы могли читать в новостях.
Каждый сервис использует свои решения для маскировки номеров клиентов и курьеров. В данной статье я расскажу, как сделать это с помощью key-value хранилища в Voximplant.
Как это будет работать
Мы создадим сценарий, который позволит курьеру и клиенту созваниваться, не зная при этом личные номера телефонов друг друга.
У нас будет только один «нейтральный» номер, на который будут звонить и клиент, и курьер. Номер мы арендуем в панели Voximplant. Затем создадим некую структуру данных, где клиент и курьер будут связаны между собой номером заказа (то есть ключом в терминологии key-value storage).
Так при звонке на арендованный номер звонящий введёт номер заказа, и если такой заказ есть в базе, наш сценарий проверит номера телефонов, привязанные к нему. Далее если номер звонящего будет идентифицирован как номер клиента, произойдет соединение с курьером, ответственным за заказ, и наоборот.
Например, звонок курьера клиенту будет выглядеть следующим образом:

Если номер телефона звонящего не будет найден в базе, ему предложат перезвонить с того номера, который использовался при оформлении заказа, или переключиться на оператора.
Перейдем непосредственно к реализации.
Вам понадобятся
- верифицированный аккаунт Voximplant, который можно создать здесь;
- приложение Voximplant со сценарием и правилом для сценария (их мы создадим вместе);
- телефонные номера для тестов: арендованный в панели номер, номер клиента, курьера и оператора (в тестовой реализации номер оператора можно не указывать).
1) Чтобы начать разработку, войдите в свой аккаунт: manage.voximplant.com/auth. В меню слева нажмите «Приложения», затем «Создать приложение» в правом верхнем углу. Дайте ему имя, например, numberMasking и снова кликните «Создать».
2) Зайдите в новое приложение, переключитесь на вкладку «Сценарии» и создайте сценарий, нажав на «+». Назовём его kvs-scenario. Здесь мы будем писать код, но об этом чуть позже.
3) Сначала перейдем во вкладку «Роутинг» и создадим правило для нашего сценария. Маску (регулярное выражение) оставим «.*» по умолчанию, так правило будет срабатывать для всех номеров.

4) Далее арендуем реальный городской номер. Для этого перейдем в раздел «Номера», выберем и оплатим номер. На него будут звонить и клиент, и курьер, и он будет отображаться вместо их настоящих номеров.
В Voximplant вы можете приобретать в том числе тестовые номера, которые удобно использовать при знакомстве с платформой, но в нашем случае потребуется реальный для совершения исходящего звонка с платформы.

5) Осталось привязать его к нашему приложению. Заходим в приложение, открываем вкладку «Номера» → «Доступные» и нажимаем «Прикрепить». В открывшемся окне можно также прикрепить наше правило, тогда оно будет автоматически назначено для входящих вызовов, а все остальные правила будут проигнорированы.
6) Далее необходимо верифицировать аккаунт, чтобы использовать этот номер для звонков.
Отлично, структура готова, осталось заполнить key-value хранилище и добавить код в сценарий.
Key-value хранилище
Чтобы сценарий заработал, нужно положить что-то в хранилище. Это можно сделать, воспользовавшись Voximplant Management API. Я буду использовать Python API client, он работает с Python 2.x или 3.x с установленным pip и setuptools> = 18.5.
1) Зайдем в папку проекта и установим SDK, используя pip:
python -m pip install --user voximplant-apiclient
2) Создадим файл с расширением .py и добавим в него код, при выполнении которого данные о заказе попадут в key-value хранилище. Применим метод set_key_value_item:
from voximplant.apiclient import VoximplantAPI, VoximplantException
if __name__ == "__main__":
voxapi = VoximplantAPI("credentials.json")
# SetKeyValueItem example.
KEY = 12345
VALUE = '{"courier": "79991111111", "client": "79992222222"}'
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))
Файл с необходимыми credentials вы сможете сгенерировать при создании сервисного аккаунта в разделе «Служебные аккаунты» в настройках панели.
APPLICATION_ID появится в адресной строке при переходе в ваше приложение.
В качестве ключа (KEY) будет использоваться пятизначный номер заказа, а в качестве значений телефонные номера: courier – номер курьера, client – номер клиента. TTL нам здесь необходимо для указания срока хранения значений.
3) Осталось запустить файл, чтобы сохранить данные заказа:
python3 kvs.py
Если мы больше не захотим, чтобы клиент и курьер беспокоили друг друга, можно будет удалить их данные из хранилища. Информацию о всех доступных методах key-value storage вы найдёте в нашей документации: management API и VoxEngine.
Код сценария
Код, который необходимо вставить в сценарий kvs-scenario, представлен ниже, его можно смело копировать as is:
Полный код сценария
Код тщательно прокомментирован, но в некоторые моменты углубимся подробнее.
Вводим номер заказа
Первое, что мы делаем при звонке – просим звонящего ввести номер заказа и обрабатываем введенное значение с помощью функции dtmfHandler.
store.input += e.tone;
Если звонящий ввел #, сразу соединяем его с оператором:
if (e.tone === '#') {
store.data.need_operator = "Да";
store.call.removeEventListener(CallEvents.ToneReceived);
store.call.handleTones(false);
callOperator();
return;
}
Если он ввел последовательность из 5 цифр, вызываем функцию handleInput:
if (store.input.length >= 5) {
repeatAskForInput = true;
Logger.write('Получен код ${store.input}. ');
store.call.handleTones(false);
store.call.removeEventListener(CallEvents.ToneReceived);
handleInput(store.input);
return;
}
Ищем заказ в хранилище
Здесь мы будем сравнивать введенный номер заказа с номером в хранилище, используя метод ApplicationStorage.get(), в качестве ключа используем введенную последовательность:
store.data.order_number = store.input;
Logger.write('Ищем совпадение в kvs по введенному коду: ' + store.input)
inputRecieved = true;
let kvsAnswer = await ApplicationStorage.get(store.input);
Если заказ найден, получаем для него номера клиента и курьера:
if (kvsAnswer) {
store.data.order_search = 'Заказ найден';
Logger.write('Получили ответ от kvs: ' + kvsAnswer.value)
let { courier, client } = JSON.parse(kvsAnswer.value);
Теперь осталось разобраться, кому звонить. Если номер звонящего принадлежит курьеру, будем выполнять переадресацию на клиента, если клиенту – на курьера. В этом нам поможет функция callCourierOrClient:
if (store.caller == courier) {
Logger.write('Звонит курьер')
store.callee = client;
store.data.sub_status = 'Курьер';
store.data.phone_search = 'Телефон найден';
callCourierOrClient();
} else if (store.caller == client) {
Logger.write('Звонит клиент')
store.callee = courier;
store.data.sub_status = 'Клиент';
store.data.phone_search = 'Телефон найден';
callCourierOrClient();
}
Если номера нет в хранилище, просим перезвонить с другого номера, который указывался при оформлении заказа:
else {
Logger.write('Номер звонящего не совпадает с номерами, полученными из kvs');
wrongPhone = true;
store.data.phone_search = 'Телефон не найден';
store.input = '';
store.call.handleTones(true);
store.call.addEventListener(CallEvents.ToneReceived, dtmfHandler);
await say(phrases.wrongPhone);
addInputTimeouts();
}
И наконец, обрабатываем вариант, когда номер заказа не был найден в базе. В этом случае просим попробовать ввести его снова, предварительно удостоверившись, что номер верный:
else {
Logger.write('Совпадение в kvs по введенному коду не найдено');
store.data.order_search = 'Заказ не найден';
store.input = '';
store.call.handleTones(true);
store.call.addEventListener(CallEvents.ToneReceived, dtmfHandler);
await say(phrases.wrongOrder);
Logger.write(`Очищаем таймер ${longInputTimerId}. `);
addInputTimeouts();
}
Звоним клиенту/курьеру
Переходим непосредственно к звонку клиенту/курьеру, то есть к логике функции callCourierOrClient. Здесь мы сообщим звонящему, что переводим его звонок на курьера/клиента, и включим музыку на ожидание. С помощью метода callPSTN позвоним клиенту или курьеру (в зависимости от того, чей номер был ранее идентифицирован как номер звонящего):
await say(store.data.sub_status === 'Курьер' ? 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 = 'Да';
await say(store.data.sub_status === 'Курьер' ? 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 = 'Нет';
store.call.stopPlayback();
await say(store.data.sub_status === 'Курьер' ? phrases.clientUnavailable : phrases.courierUnavailable, store.call);
store.call.hangup();
});
За все фразы, который произносит робот, отвечает функция say, а сами фразы перечислены в ассоциативном массиве phrases. В качестве TTS провайдера мы используем Yandex, голос Alena:
function say(text, call = store.call, lang = VoiceList.Yandex.Neural.ru_RU_alena) {
return new Promise((resolve) => {
call.say(text, lang);
call.addEventListener(CallEvents.PlaybackFinished, function callback(e) {
resolve(call.removeEventListener(CallEvents.PlaybackFinished, callback));
});
});
};
Кроме всего прочего, наш сценарий записывает звонки, используя метод record, и показывает, как можно сохранить статистику в базу данных (в нашем коде за это отвечает функция sendResultToDb). Это очень важно для бизнеса, поскольку позволяет анализировать статистику, обеспечивать контроль качества и оперативно решать спорные ситуации, которые могли возникнуть в процессе доставки заказа.
Оригинал статьи