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

от автора

В условиях пандемии курьерские сервисы стали востребованы как никогда прежде. Чтобы клиент и курьер могли созвониться для уточнения информации по заказу, им нужно знать номера телефонов друг друга. А что насчет соблюдения прайваси? Многие сервисы доставок уже озаботились этим вопросом после не очень приятных инцидентов, о которых вы могли читать в новостях.

Каждый сервис использует свои решения для маскировки номеров клиентов и курьеров. В данной статье я расскажу, как сделать это с помощью key-value хранилища в Voximplant.

Как это будет работать

Мы создадим сценарий, который позволит курьеру и клиенту созваниваться, не зная при этом личные номера телефонов друг друга.

У нас будет только один «нейтральный» номер, на который будут звонить и клиент, и курьер. Номер мы арендуем в панели Voximplant. Затем создадим некую структуру данных, где клиент и курьер будут связаны между собой номером заказа (то есть ключом в терминологии key-value storage).

Так при звонке на арендованный номер звонящий введёт номер заказа, и если такой заказ есть в базе, наш сценарий проверит номера телефонов, привязанные к нему. Далее если номер звонящего будет идентифицирован как номер клиента, произойдет соединение с курьером, ответственным за заказ, и наоборот.

Например, звонок курьера клиенту будет выглядеть следующим образом:

Если номер телефона звонящего не будет найден в базе, ему предложат перезвонить с того номера, который использовался при оформлении заказа, или переключиться на оператора.

Перейдем непосредственно к реализации.

Вам понадобятся

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:

Полный код сценария
require(Modules.ApplicationStorage);  /**  * @param {boolean} repeatAskForInput - была ли просьба ввода произнесена повторно  * @param longInputTimerId - таймер на отсутствие ввода  * @param shortInputTimerId - таймер на срабатывание фразы для связи с оператором  * @param {boolean} firstTimeout - индикатор срабатывания первого таймаута  * @param {boolean} wrongPhone - индикатор совпадения номера звонящего с номером, полученным из хранилища  * @param {boolean} inputRecieved - получен ли ввод от пользователя  *   */  let repeatAskForInput; let longInputTimerId; let shortInputTimerId; let firstTimeout = true; let wrongPhone; let inputRecieved;  const store = {     call: null,     caller: '',     callee: '',     callid: '74990000000',     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: 'Здр+авствуйтте. Пожалуйста, -- введите пятизначный номер заказa в тт+ооновом режиме.',     repeat: 'Пожалуйста , , - - введите пятизначный номер заказа в т+оновом режиме,, или нажмите решетку для соединения со специалистом',     noInputGoodbye: 'Вы - ничего не выбрали. Вы можете посмотреть номер заказа в смс-сообщении и позвонить нам снова. Всего д+обровоо до свидания.',     connectToOpearator: 'Для соединения со специалистом,, нажмите решетку',     connectingToOpearator: 'Ожидайте, соединяю со специалистом',     operatorUnavailable: 'К сожалению,, все операторы заняты. Пожалуйста,,, перезвоните позднее. Всего д+обровоо до свидания.',     wrongOrder: 'Номер заказа не найден. Посмотрите номер заказа в смс-сообщении и введите его в т+оновом режиме. Или свяжитесь со специалистом,, нажав клавишу решетка.',     wrongOrderGoodbye: 'Вы ничего не выбрали, всего д+обровоо до свидания.',     wrongPhone: 'Номер телефона не найден. Если вы кли+ент, перезвоните с номера, который использовали для оформления заказа. Если вы курьер, перезвоните с номера, который зарегистрирован в нашей системе. Или свяжитесь со специалистом,,- нажав клавишу решетка.',     wrongPhoneGoodbye: 'Вы ничего не выбрали. Всего доброго, до свидания!',     courierIsCalling: `Вам звонит курьер по поводу доставки вашего заказа, - - ${store.data.order_number}`,     clientIsCalling: `Вам звонит клиент по поводу доставки заказа, - - ${store.data.order_number} `,     courierUnavailable: 'Похоже,,, курь+ер недоступен. Пожалуйста,,, перезвоните через п+ару мин+ут. Всего д+обровоо до свидания.',     clientUnavailable: 'Похоже,,, абонент недоступен. Пожалуйста,,, перезвоните через пп+ару мин+ут. Всего д+обровоо до свидания.',     waitForCourier: 'Ожидайте на линии,, - соединяю с курьером.',     waitForClient: 'Ожидайте на линии,, соединяю с клиентом.' }   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('Введена цифра ' + e.tone)     Logger.write('Полный код ' + store.input)     if (e.tone === '#') {         store.data.need_operator = "Да";         store.call.removeEventListener(CallEvents.ToneReceived);         store.call.handleTones(false);         callOperator();         return;     }      if (!wrongPhone) {         if (store.input.length >= 5) {             repeatAskForInput = true;             Logger.write(`Получен код ${store.input}. `);             store.call.handleTones(false);             store.call.removeEventListener(CallEvents.ToneReceived);             handleInput(store.input);             return;         }     }     addInputTimeouts(); }  function addInputTimeouts() {     clearInputTimeouts();     if (firstTimeout) {         Logger.write('Запущен таймер на срабатывание фразы для связи с оператором');         shortInputTimerId = setTimeout(async () => {             await say(phrases.connectToOpearator);         }, 1500);         firstTimeout = false;     }      longInputTimerId = setTimeout(async () => {         Logger.write('Сработал таймер на отсутствие ввода от пользователя ' + longInputTimerId);         store.call.removeEventListener(CallEvents.ToneReceived);         store.call.handleTones(false);         if (store.input) {             handleInput(store.input);             return;         }         if (!repeatAskForInput) {             Logger.write('Просим пользователя повторно ввести код');             store.call.handleTones(true);             store.call.addEventListener(CallEvents.ToneReceived, dtmfHandler);             await say(phrases.repeat);             addInputTimeouts();             repeatAskForInput = true;         } else {             Logger.write('Код не введен. Завершаем звонок.');             await say(inputRecieved ? phrases.wrongOrderGoodbye : phrases.noInputGoodbye);             store.call.hangup();         }      }, 8000);     Logger.write('Запущен таймер на отсутствие ввода от пользователя ' + longInputTimerId); }  function clearInputTimeouts() {     Logger.write(`Очищаем таймер ${longInputTimerId}. `);     if (longInputTimerId) clearTimeout(longInputTimerId);     if (shortInputTimerId) clearTimeout(shortInputTimerId); }  async function handleInput() {     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);          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();      }  }  async function callCourierOrClient() {     clearInputTimeouts();     Logger.write('Начинаем звонок курьеру/клиенту');     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();     }); }  async function callOperator() {     Logger.write('Начинаем звонок оператору');     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 = 'Оператор свободен';         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 = 'Оператор занят';         await say(phrases.operatorUnavailable, store.call);         store.call.hangup();     }); }   async function sendResultToDb() {     Logger.write('Данные для отправки в БД');     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.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));         });     }); };

Код тщательно прокомментирован, но в некоторые моменты углубимся подробнее.

Вводим номер заказа

Первое, что мы делаем при звонке – просим звонящего ввести номер заказа и обрабатываем введенное значение с помощью функции 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). Это очень важно для бизнеса, поскольку позволяет анализировать статистику, обеспечивать контроль качества и оперативно решать спорные ситуации, которые могли возникнуть в процессе доставки заказа.

Тестируем

Когда полный код добавлен в сценарий, а данные по заказу – в хранилище, смело начинайте тестировать. 

Позвоним с телефона клиента или курьера на номер, арендованный в панели. Затем введем номер заказа (в нашем случае это 12345) и будем ждать соединения со второй стороной.

Если все сделано верно, клиент и курьер смогут созваниваться и обсуждать детали заказа, не зная настоящих номеров друг друга, а значит, не нарушая прайваси. Круто, не так ли?) Желаем вам успешной разработки и беспроблемной доставки!

P.S. Также мой коллега недавно рассказал, как обезопасить общение клиента и курьера с помощью Voximplant Kit (наш low-code/no-code продукт). Если эта тема вас заинтересовала, переходите по ссылке 🙂

ссылка на оригинал статьи https://habr.com/ru/company/Voximplant/blog/563082/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *