Быстрое создание callback-сценариев с помощью Voximplant

  • Tutorial
Не удивительно, что во многих бизнес процессах так или иначе встречается такая задача как позвонить на один номер телефона и потом соединить его с другим номером. Чаще всего такой сервис называют callback или обратный вызов, иногда — заказ звонка. Многие встречали его на сайтах интернет-магазинов, но в большинстве случаев это не автоматизированный обратный звонок, а обычная заявка, которая падает менеджеру на email или появляется в CRM, после чего менеджер ручками набирает номер клиента и с ним беседует. Некоторые крупные компании реализуют автоматизированный callback и даже интегрируют его с очередями в контакт-центре. В данном посте мы рассмотрим как буквально за несколько минут создать callback-сценарий с помощью платформы VoxImplant, а также интегрировать это хозяйство с каким-нибудь существующим бэкендом для сохранения/получения данных по звонку, запасайтесь поп-корном и добро пожаловать под кат.
Для начала продублируем схему в более крупном размере, чтобы было проще объяснить, что, как и с чем взаимодействует при реализации нашего callback-сервиса.

Выглядит немного громоздко, но на деле все очень просто и удобно. Разработчик VoxImplant (чтобы получить аккаунт разработчика нужно зарегистрироваться на сайте, это абсолютно бесплатно) через HTTP API делает запрос к облаку VoxImplant после чего сервер сценариев запускает заранее написанный на JS сценарий, в котором описана логика совершения звонков, их соединения, а также, при желании, бизнес-логика. Перед тем как делать HTTP-запрос для запуска сценария, очевидно, нужно его написать, а также подцепить к приложению через правило (rule), id этого правила потом нужно будет указывать в HTTP-запросе, чтобы было понятно какой сценарий должен быть запущен. Итак, по порядку:
  1. Создаем приложение VoxImplant.
  2. Создаем сценарий.
  3. Привязываем сценарий к приложению с помощью правила.
  4. Делаем HTTP-запрос для запуска сценария.

После совершения HTTP-запроса для запуска сценария, если все сделано правильно, в ответ придет media_session_access_url, который позволяет после запуска сценария продолжить удаленное управление его работой. Например, если нужно в какой-то определенный момент закончить выполнение сценария, то можно написать в сценарии следующее:
VoxEngine.addEventListener(AppEvents.HttpRequest, handleHttpRequest); 
function handleHttpRequest(e) {
  VoxEngine.terminate();
}

В таком случае, сделав запрос по media_session_access_url, вы прекратите выполнение сценария. Возвращаемся к реализации нашего колбэка на телефонные номера. Создаем приложение в разделе Applications панели управления, назовем его банально callback (в итоге полное название будет callback.имя_аккаунта.voximplant.com), и в разделе Scenarios создаем новый сценарий следующего вида:
var call1, call2, data;
const callerId = "+1234567890"; // Арендованный или верифицированный номер телефона

VoxEngine.addEventListener(AppEvents.Started, handleScenarioStart);

function handleScenarioStart(e) {
  // в сценарий можно передать данные с помощью механизма customData,
  // параметр script_custom_data в HTTP-запросе StartScenarios
  // передадим в нем номера в виде строки номер1:номер2
  data = VoxEngine.customData();
  data = data.split(":");
  // начало выполнения сценария - звоним на номер 1
  call1 = VoxEngine.callPSTN(data[0], callerId);
  // обработчики событий
  call1.addEventListener(CallEvents.Connected, handleCall1Connected);
  call1.addEventListener(CallEvents.Failed, function(e) { VoxEngine.terminate(); });
  call1.addEventListener(CallEvents.Disconnected, function(e) { VoxEngine.terminate(); });
}

function handleCall1Connected(e) {
  // первый звонок соединен успешно, проигрываем сообщение
  call1.say("Здравствуйте, это звонок от сервиса Колбэк, ожидайте соединения", Language.RU_RUSSIAN_FEMALE);
  call1.addEventListener(CallEvents.PlaybackFinished, function(e1) {
     // после проигрывания сообщения пытаемся дозвониться до номера 2
    call2 = VoxEngine.callPSTN(data[1], callerId);
    // обработчики событий
    call2.addEventListener(CallEvents.Connected, handleCall2Connected);
    call2.addEventListener(CallEvents.Failed, function(e2) { 
        call1.say("К сожалению, соединение не может быть установлено", Language.RU_RUSSIAN_FEMALE);
        call1.addEventListener(CallEvents.PlaybackFinished, function(e3) { VoxEngine.terminate(); });      
    });
    call2.addEventListener(CallEvents.Disconnected, function(e2) { VoxEngine.terminate(); });
  });
}

function handleCall2Connected(e) {
  // соединяем два звонка - звук 
  VoxEngine.sendMediaBetween(call1, call2);
  // и сигнализацию
  VoxEngine.easyProcess(call1, call2);
}

Если коротко, то данный сценарий звонит на номера, которые мы передаем через специальный параметр запроса StartScenarios script_custom_data в виде строки номер1: номер2. Сначала дозваниваемся на номер1 и проигрываем сообщение с помощью TTS, после чего дозваниваемся на номер2 и соединяем два звонка друг с другом. В данном сценарии мы никак не отрабатываем некоторые случаи, например, если не дозвонились на номер1, то просто завершаем выполнение сценария. Если немного поколдовать, то можно оповещать внешний мир о том, что что-то пошло не так, меняем
call1.addEventListener(CallEvents.Failed, function(e) { VoxEngine.terminate(); });

на
call1.addEventListener(CallEvents.Failed, handleCall1Failed);

и описываем handleCall1Failed:
function handleCall1Failed(e) {
  // тут можно узнать причину 
  var code = e.code,
      reason = e.reason;
  // и отправить ее во внешний мир с помощью HTTP-запроса
  Net.httpRequest("http://somewebservice", function(e1) {
    // информация по реузльтатам запроса - e1.code, e1.text, e1.data, e1.headers
    // убиваем сессию
    VoxEngine.terminate();
  });
}

Это самый простой пример с GET-запросом. Для настройки и управления HTTP-запросом со стороны сценария доступен класс HttpRequestOptions.
Таким же образом с помощью Date можно замерять длительность отдельных звонков или сессии внутри сценария и отправлять данные во внешний мир, если по каким-то причинам лезть в историю звонков для получения этой информации не соответствует логике сервиса/приложения. Как вы уже, наверное, поняли, мы пытались сделать очень гибкую систему, чтобы механизм решения тех или иных задач разработчик мог выбирать сам.
После того как сценарий готов нам нужно его сохранить под каким-нибудь названием, например, CallbackHabr и пойти в раздел Applications, чтобы подцепить сценарий к правилу и в итоге получить его ID.

Чтобы получить ID правила нужно немного пошаманить (в будущем постараемся устранить этот недостаток панели управления) — после создания правила нужно сохранить приложение (именно приложение, а не порядок правил), то есть перейти в таб General и там нажать Save. Потом еще раз открыть приложение для редактирования и открыть таб Rules, где отобразится ID правила (его также можно получить через HTTP API):

Ну в общем и все, теперь можно делать HTTP-запрос StartScenarios, для которого потребуется еще ряд параметров, свой api_key можно узнать в разделе Profile. Запрос в итоге будет выглядеть как-то так:
https://api.voximplant.com/platform_api/StartScenarios/?account_id=1&api_key=eec36d6c-a0eb-46b5-a006-1c2b65343bac&rule_id=55033&script_custom_data=number1%3Anumber2

В результате успеха в ответ придет что-то в духе:
{
	"result" : 1,
	"media_session_access_url" : "http:\/\/1.2.3.4:12092\/request?id=93e41c6e20811b9b.1380201554.0@1.2.3.4&token=36bc7ce95edc679e32d83bb6f3ad985f"
}

Через этот media_session_access_url можно будет дальше общаться с сессией при необходимости. Для отладки работы сценария всегда доступны логи, которые находятся в разделе Calls (стрелочка в правом верхнем углу), а также наш убердебагер, доступный здесь. Для удобства мы загрузили сценарий из данного поста на GitHub, чтобы можно было быстро поэкспериментировать.

Callback-сценарий на GitHub

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Был ли данный материал Вам полезен?

Voximplant
138,94
Облачная платформа голосовой и видеотелефонии
Поделиться публикацией

Комментарии 41

    +1
    ID правила можно увидеть только при выборе английского языка. В русском интерфейсе ID правила отсутствует
      0
      Спасибо за замечание, сейчас создадим тикет и постараемся поправить в самое ближайшее время.
        0
        Пробую… но пока некоторые вещи не очень понятны в документации. Например, где взять API Key?
          0
          В посте есть про это отдельно
          свой api_key можно узнать в разделе Profile
            +1
            Да, понял. Я уже прочитал :) Только это в вашем посте есть. А в документации — нету совсем.

            Ну и account_id — тоже не очень понятно где брать. Я плюс-минус разобрался, но вот документацию бы чуть-чуть дополнить.

            Вообще хорошо, что вы пишете такие статьи. Потому что примеров у вас на сайте практически нет — писать неимоверно сложно. Мне очень все нравится. Я готов запустить и начать пользоваться реальным серивисом вот прямо сейчас. Осталось только написать :)
              0
              account_id — это один из вариантов, вместо него можно указать account_name или email-адрес. Мы знаем, что наша дкоументация далека от совершенства на данный момент, мы работаем над ее улучшением и продолжим писать статьи как в блоге, так и на Хабре. Комментарии очень помогают сделать платформу лучше.
                0
                Дак мы с удовольствием бы! Только вот дальше куда смотреть? Вот я взял этот сценарий… а что мне дальше делать? Потому что пока не очень ясно…
                  0
                  После HTTP-запроса StartScenarios в облаке запустится данный сценарий и сделает звонок сначала на 1 номер, а потом на другой и соединит их.
                    0
                    Сорри, оно заработало. Оказывается, я неправильно вводил номер телефона. Правильный формат: 74951234567

                    Та-а-а-а-ак… а еще ма-а-а-аленький вопрос. Мне пришел звонок сейчас на мобильный. С некоего номера в коде 499. А что это за номер?
                      0
                      Это номер платформы в России по умолчанию, чтобы был другой номер надо поменять сценарий, а именно в VoxEngine.callPSTN указать второй параметр. Надо учесть что абы какой номер подставлять нельзя. Сначала нужно его авторизовать в разделе CallerIDs
                        0
                        Ага. Ну для начала я должен буду купить номер в разделе Phone Numbers?

                        И еще — сколько одновременных соединений потянет такая схема с двумя вызовами? Если у меня 100-150 звонков в течение дня — это как? Много, мало?
                          0
                          Не обязательно, купить номер — это один из вариантов, второй — валидировать уже существующий у вас, придет автоматизированный вызов с кодом для валидации.

                          Мы не ограничиваем количество вызовов, 100-150 звонков в течение дня — это очень незначительная нагрузка для нашей инфраструктуры :)
                            0
                            Я все-таки решил для тестов купить у вас номер. Купил, указал, для какого приложения он. Но все-равно определяется другой. Как привязать?
                              0
                              В каком формате вы его указываете в методе callPSTN?
                                0
                                Нет-нет, вопрос другой: как сделать, чтобы при звонке на мобильный определялся в качестве обратного номера мой, который я только что купил?
                                  0
                                  Именно так и сделать, указать его вторым параметров в функции callPSTN
                                    0
                                    Разбираемся дальше.

                                    Вообще, крутую штуку вы сделали! Респект! :)
      0
      Здравствуйте. Пытаемся разобраться с вашим сервисом. На странице voximplant.com/docs/references/websdk/ есть vox.addEventListener(VoxImplant.Events.AuthResult, handleAuthResult); vox.login(«voximplant_username», «voximplant_password»). Значит у меня в коде в открытом виде на странице должен быть логин и пароль, это же не безопасно. Как решается вопрос чтобы мои логин-пароль не использовал кто угодно для совершения звонков?
        0
        Ну в целом идея такая, что эти данные клиент должен вводить :) в форме авторизации. Но есть и другой вариант — voximplant.com/docs/references/websdk/VoxImplant.Client.html#loginWithOneTimeKey, вместо пароля используется специальный хэш, который считается у вас на сервере после получения ключа.
          0
          Понял что webSDK удобно, но использовать в нашем случае можно исключительно на свой страх и риск. Смотрим в сторону voximplant.com/docs/references/httpapi/.
        0
        Есть ли пример сценария когда при недозвоне на телефон кол центра идет дозвон на другой и т.д. (т.е. у меня входящих Х номеров в массиве)
          0
          Отлавливаете событие недозвона (http://voximplant.com/docs/references/appengine/CallEvents.Failed.html) и по его факту инициируете следующий вызов. На одну сессию можно сделать не более 10 звонков.
          0
          Выше был вопрос про X номеров колл центра в массиве тут все понятно. А есть ли пример сценария, когда может быть от 1 до N номеров колл центра и 1 номер абонента. Script_custom_data у вас через двоеточие — и в этом сложность. Помню про JSON, но не разобрались с вашим отладчиком. Дадим ваш хорошую загрузку. Помогите разобраться с сценарием.
            +1
            data = VoxEngine.customData();
            data = data.split(":");

            а что мешает data[0] какой нить собрать с разделителями "| " и его снова phones= data[0].split("|"); — получаем массив телефонов
              0
              Можно прямо массив в JSON передать, в чем проблема?
              0
              Андрей, а отладчик у вас сейчас работает? Запускаю ваш же пример, пишет «ожидание начала сессии отладки» и все. Ничего не происходит. Что я делаю не так? Как отладить самописный сценарий без слива денег?
                0
                Отладчик работает, чтобы началась сессия отладки нужно сделать HTTP-запрос StartScenario. Сессия запускается на платформе или по факту HTTP-запроса (в случае callback-сценария) или по факту звонка пришедшего на платформу.
                0
                Разобрались с каскадной переадресацией. Вопрос по записи разговоров voximplant.com/docs/references/appengine/Recorder.html. Сценарий запускает бекенд и получает в ответ media_session_access_url и на этом все. Как бекенду в этом же сеансе получить ссылку на запись разговора?
                  0
                  В сценарии можно получить URL записи из voximplant.com/docs/references/appengine/CallEvents.RecordStarted.html, параметр url. Бэкенд может вызывать функции сценария через media_session_access_url, передавая туда параметры, которые будут приходить сюда voximplant.com/docs/references/appengine/AppEvents.HttpRequest.html, соответственно возвращать данные можно просто сделав return из хэндлера этого события.
                    0
                    Пожалуйста, добавьте в ваш же пример сценария выше (в статье) как с помощью voximplant.com/docs/references/appengine/CallEvents.RecordStarted.html включить запись и по факту отработки получить ссылку на файл. Из сценария ссылку дальше мы самостоятельно с помощью voximplant.com/docs/references/appengine/AppEvents.HttpRequest.html пробросим на свой бекенд.

                    Без примеров кода справится очень затруднительно.
                      0
                      Да вроде все вполне просто (если нужно писать разговор 2х соединенных звонков, то все это делается у любого одного из них):

                      var record_url;
                      call.addEventListener(CallEvents.RecordStarted, handleRecordStarted);
                      call.record();
                      
                      function handleRecordStarted(e) {
                         record_url = e.url;
                      }
                      
                        0
                        Спасибо. Я правильно понимаю, что единственный способ бэкенду получить record_url это повесить в функцию из примера http вызов с передачей параметра? или есть возможность, все же, получить урл с записью после вызова сценария с бэкенда (делаем http запрос для запуска сценария, делаем http запрос для получения урла записи)?
                        т.е. что-то вроде этого (пример для проброса урла записи из сценария на бэкенд)

                        function handleRecordStarted(e) { record_url = e.url; Net.httpRequest("http://somewebservice?url=encodeURIComponent(record_url)", function(e1) { }); }
                          0
                          или есть возможность все же получить урл с записью после вызова сценарий с бэкенда (делаем http запрос для запуска сценарий, делаем http запрос для получения урла записи)?

                          Есть, я же написал что можно делать потом HTTP-запросы к медиа-сессии по адресу media_session_access_url, которые на стороне сценария будут вызывать voximplant.com/docs/references/appengine/AppEvents.HttpRequest.html

                          На стороне сценария будет:
                          VoxEngine.addEventListener(AppEvents.HttpRequest, function (e) {
                              // в e.path будет строка запроса , которая идет после media_session_access_url, можно там передавать название вызываемой функции, как вариант.
                              return record_url;
                          }
                          


                            0
                            ну мы же понимаем, что это не совсем красивое решение. Ибо один итак запущенный процесс (который запускаем сценарий) вместо отработки в самом себе породить еще один, который придет из сценария через AppEvents.HttpRequest

                            Итого: 1 запрос на запуск сценария, второй запрос на вызов нашей функции из сценария через media_session_access_url который породит третий (по счету, а по факту второе соединение) запрос на бэкенд из сценария.

                            По-моему не совсем удачное решение, когда вопрос решается всего то лишь возвращением урла с потенциальной записью при вызове самого сценария там же где приходит media_session_access_url в ответ, а уж существует запись или нет — итог отработки сценария, но мы заранее будем знать где 'если что' искать контент и все в 1 запрос, не правда ли? :)
                              0
                              Вы по-моему неправильно поняли. Запрос функции (media_session_access_url) вернет сразу URL на запись, когда этот URL будет сформирован. Про потенциальный URL даж комментировать не хочется… Вы же понимаете что URL записи формируется только в случае запуска в сценарии записи я надеюсь?
                                0
                                Хотя удобнее вызвать сценарий сразу с флагом записи и получить в ответ адрес где ее можно получить по факту. Это гораздо удобнее для сервисов, которые будут подключены. Тем более все входящее данные при запросе, который включает сценарий у вас все есть. Одно дело глобальные опции, другое дело нюансы сценария, так вот в большинстве случаев глобальной опции на включение записи будет достаточно.
                                  0
                                  Вы плохо себе представляете кол-во сценариев, которые делают на платформе, у вас частный случай и есть все необходимые инструменты для решения задачи. В сценарии может быть до 10 звонков — и какие же из них нужно писать при включении флага?
                                    0
                                    Все правильно. Наш случай это много телефонов колцентра и 1 клиентский, писать нужно сам разговор клиента. Понятно, что ожидание и пробросы на другие звонки колцентра не интересы. Интересует сам разговор, он один (либо он есть либо его нет, поэтому говорю о потенциальном разговоре) Но для удобства использования, глобальную опцию можно вытащить, это частный случай, но думаю, он удобнее в использование будет и не для одного клиента.
                                    Кстати, Спасибо за быстрые ответы.
                                0
                                Ну и в дополнение URL записи всегда можно вытащить из HTTP API уже после завершения звонка, если не горит.

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое