Asterisk и информация о входящих звонках в браузере через Notifications

от автора

В нашей компании используется телефон 8800, для того, чтобы клиенты могли сделать заказ без доступа к сайту. Для обслуживания большинства входящих звонков используется call-центр, а также при необходимости происходит перенаправление на внутреннего сотрудника.

Для удобства сотрудников и возможности персонализированного ответа была внедрена система распознавания входящего звонка по внутренней базе клиентов.

Так как cron задания были бы слишком редкими (максимум 1 раз в секунду), то за основу был взят демон на php, который сканирует каналы и отправляет информацию о звонке во временное хранилище. Для временного хранилища был использован memcached.

Используемая версия Asterisk’a — 11.15.1.
В качестве API связки php и Asteriska’a — модуль PAMI.

Основной класс демона прослушки:

class AsteriskDaemon {     private $asterisk;     private $memcache;      public function __construct()     {         $this->asterisk = new ClientImpl([             ...         ]);          $memcache = new Memcached;         $memcache->connect('127.0.0.1', '11211');                  $this->memcache = $memcache;     }      public function start()     {         $asterisk = $this->asterisk;          $loop = Factory::create();          // add periodic timer         $loop->addPeriodicTimer(1, function () use (&$asterisk) {             $pid = \pcntl_fork();             if ($pid < 0) { // ошибка создания exit;             }elseif ($pid) { // родитель, ждет выполнения потомков                 \pcntl_waitpid($pid, $status, WUNTRACED);                 if ($status > 0) {                     // если произошла ошибка в канале, пересоздаем                     $asterisk->close();                     usleep(1000);                     $asterisk->open();                 }                  return;             } else {                 // выполнение дочернего процесса                 try {                     $asterisk->process();                     exit(0);                 } catch (\Exception $e) {                     exit(1);                 }             }         });          // восстановление подпроцессов         $loop->addPeriodicTimer(30, function () {             while (($pid = \pcntl_waitpid(0, $status, WNOHANG)) > 0) {                 echo "process exit. pid:" . $pid . ". exit code:" . $status . "\n";             }         });          $loop->run();     } } 

Существует два возможных варианта распознавания: прослушивание событий каналов и ручной разбор информации в CoreShowChannel, рассмотрим все по порядку.

Прослушивание событий

В конструктор демона добавляем инициализацию слушателя событий AsteriskEventListener:

... $this->asterisk->registerEventListener(new AsteriskEventListener($memcache), function (EventMessage $event) {     // Прослушивание только события операций с каналами     return $event instanceof BridgeEvent; }); $this->asterisk->open(); ... 

И соответственно сам класс прослушивания и работы с временным хранилищем:

class AsteriskEventListener implements IEventListener {     private $memcache;     private $bridges = [];      public function __construct($memcache)     {         $this->memcache = $memcache;     }      private function addBridge($phone1, $phone2)     {         $bFind = false;          if ($this->bridges) {             foreach ($this->bridges as $bridge) {                 if (in_array($phone1, $bridge) && in_array($phone2, $bridge)) {                     $bFind = true;                 }             }         }          if (!$bFind) {             $this->bridges[] = [                 $phone1,                 $phone2             ];             $bFind = true;         }          return $bFind;     }      private function deleteBridge($phone1, $phone2 = null)     {         if ($this->bridges) {             foreach ($this->bridges as $key => $bridge) {                 if (in_array($phone1, $bridge) && (!$phone2 || ($phone2 && in_array($phone2, $bridge)))) {                     unset($this->bridges[$key]);                 }             }         }     }      public function handle(EventMessage $event)     {         // Делаем распознавание, если пришло событие создания/удаления канала         if ($event instanceof BridgeEvent) {             $this->bridges = $this->memcache->getKey('asterisk-bridges');              $state = $event->getBridgeState();             $caller1 = $event->getCallerID1();             $caller2 = $event->getCallerID2();             if ($state == 'Link') { // Создание канала                 $this->addBridge($caller1, $caller2);             } else { // Удаление канала                 $this->deleteBridge($caller1, $caller2);             }              $this->memcache->setKey('asterisk-bridges', $this->bridges);         }     } } 

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

Ручной разбор информации CoreShowChannel

Для работы данного способа необходимо несколько модифицировать демон, вызываем событие CoreShowChannel принудительно, так как сам Asterisk его не генерирует:

... // дочерний процесс выполняет процесс try {     $message = $asterisk->send(new CoreShowChannelsAction());     $events = $message->getEvents();     $this->parse($events);     $asterisk->process();      exit(0); } catch (\Exception $e) {     exit(1); } ... <source> И добавляем функцию разбора:  <source lang="php"> private function parse($events) {     foreach ($events as $event) {         if ($event instanceof CoreShowChannelEvent) {             $caller1 = $event->getKey('CallerIDnum');             $caller2 = $event->getKey('ConnectedLineNum');                          $this->bridges = $this->memcache->getKey('asterisk-bridges');              $this->addBridge($caller1, $caller2);              $this->memcache->setKey('asterisk-bridges', $this->bridges);         }      } } 

В данном способе есть проблема удаления номера телефона при отключении клиента от канала. Для решения можно использовать событие разрыва соединения:

... $this->asterisk->registerEventListener(new AsteriskEventListener(), function (EventMessage $event) {     return $event instanceof HangupEvent; }); $this->asterisk->open(); ... 

Обработка события разрыва соединения:

... public function handle(EventMessage $event) {     if ($event instanceof HangupEvent) {         $this->bridges = $this->memcache->getKey('asterisk-bridges');          $caller1 = $event->getKey('CallerIDNum');         $caller2 = $event->getKey('ConnectedLineNum');         $this->deleteBridge($caller1);         $this->deleteBridge($caller2);          $this->memcache->setKey('asterisk-bridges', $this->bridges);     } } ... 

В итоге оказалось, что второй способ является более эффективным, так как при работе с событиями asterisk часто падал, и, в результате, терялись некоторые звонки. Так же в первом способе не распознавались звонки при перенаправлении с call-центра, так как номер сотрудника и клиента были в разных каналах (Первый канал связывает call-центр и сотрудника, второй канал связывает call-центр и клиента).

Информация о звонке через Notifications

Для получения информации о входящих звонках был использован плагин event-source-polyfill и long-pull запросы на сервер. Напомню мы храним входящие звонки в memcached.

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

Получился следующий скрипт:

(function ($) {     $.getCall = function () {         if (localStorage.callTitle !== undefined && localStorage.callSuccess === undefined) {             var notification,                 title = localStorage.callTitle,                 options = {                     body: localStorage.callText,                     icon: localStorage.callImage                 },                 eventNotification = function () {                     window.open(localStorage.callUrl);                 };              if (!('Notification' in window)) {                 console.error('This browser does not support desktop notification');             } else if (Notification.permission === 'granted') {                 notification = new Notification(title, options);                 notification.onclick = eventNotification;             } else if (Notification.permission !== 'denied') {                 Notification.requestPermission(function (permission) {                     if (permission === 'granted') {                         notification = new Notification(title, options);                         notification.onclick = eventNotification;                     }                 });             }              localStorage.callSuccess = true;         }     };      // запросы к серверу только на главной вкладке     wormhole().on('master', function () {         var es = new EventSource('/check-call');         es.addEventListener('message', function (res) {             var data = JSON.parse(res.data);             if (data['id']) {                 localStorage.callTitle = data['title'];                 localStorage.callText = data['text'];                 localStorage.callImage = data['img'];                 localStorage.callUrl = data['url'];             } else {                 delete localStorage.callTitle;                 delete localStorage.callText;                 delete localStorage.callImage;                 delete localStorage.callUrl;                 delete localStorage.callSuccess;             }         });     }); })(jQuery);  setInterval(function () {     $.getCall(); }, 1000); 

И собственно сам обработчик запросов:

public function checkCall() {     header('Content-Type: text/event-stream');     header('Cache-Control: no-cache');     header('Access-Control-Allow-Origin: *');      // получение номера текущего оператора     $managerPhone = $_SESSION['phone'];      $user = null;     $clientPhone = $this->getPhone($managerPhone);     if ($clientPhone) {         $user = User::find()->where(['phone' => $clientPhone])->one();     }      if ($user) { // Увеличиваем время до следующего вызова если клиент найден         echo "retry: 30000\n";     } else {         echo "retry: 3000\n";     }      echo 'id: ' . $managerPhone . "\n";      $data = [];     if ($user) {         $data = [             'id' => $user['id'],             'title' => 'Новый звонок от ' . $user['name'],             'text' => 'Перейти к карточке клиента',             'img' => '/phone.png',             'url' => '/user/' . $user['id']         ];     }      echo "data: " . json_encode($data) . "\n\n"; }  // Получение телефона клиента public function getPhone($managerPhone) {     $memcache = new Memcached;     $memcache->addServer('127.0.0.1', '11211');          $extPhone = '';      if (!$managerPhone) {         return $extPhone;     }      $bridges = $memcache->getKey('asterisk-bridges');     if (!isset($bridges) || !is_array($bridges)) {         return $extPhone;     }      foreach ($bridges as $bridge) {         if (($key = array_search($managerPhone, $bridge)) !== false) {             $extPhone = $bridge[!$key];             break;         }     }      return $extPhone; } 

Итоги внедрения

  • Достаточно интересный опыт работы с Asterisk’ом и системой Notifications для различных браузеров.
  • Персонализация входящих звонков.
  • Мгновенный поиск номера в базе и возможность быстро перейти к карточке клиента.
  • Сотрудники получили полезный сервис оповещения о входящих звонках.

ссылка на оригинал статьи https://habrahabr.ru/post/278859/


Комментарии

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

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