📜 ⬆️ ⬇️

Asterisk and information about incoming calls in the browser via Notifications

Our company uses the 8800 phone number so that customers can place an order without access to the site. For the service of most incoming calls, the call-center is used, and if necessary, it is redirected to an internal employee.

For the convenience of employees and the possibility of a personalized response, an incoming call recognition system was introduced for the internal customer base.

Since cron tasks would be too rare (maximum 1 time per second), a daemon for php was taken as the basis, which scans the channels and sends information about the call to the temporary storage. Memcached was used for temporary storage.

The version of Asterisk used is 11.15.1.
As an API, php and Asteriska bundles are the PAMI module .
')
The main class of the wiretapping demon
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(); } } 


There are two possible recognition options: listening to channel events and manual analysis of information in CoreShowChannel, let's look at everything in order.

Listening to events


In the daemon constructor, add the initialization of the AsteriskEventListener event listener:

Event listener
 ... $this->asterisk->registerEventListener(new AsteriskEventListener($memcache), function (EventMessage $event) { //       return $event instanceof BridgeEvent; }); $this->asterisk->open(); ... 


And accordingly, the class itself listening and working with temporary storage:

Listening class
 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); } } } 


In this embodiment, there may be problems when creating channels. The fact is that when a call is redirected between employees or redirected from a call center to an employee, both channels will be created in conjunction with the person who redirected, and there will be no information about the resulting operator and client bundle.

Manual parsing of information CoreShowChannel


In order for this method to work, it is necessary to modify the daemon somewhat, we trigger the CoreShowChannel event forcibly, since Asterisk itself does not generate it:

Generating a CoreShowChannels Event
 ... //     try { $message = $asterisk->send(new CoreShowChannelsAction()); $events = $message->getEvents(); $this->parse($events); $asterisk->process(); exit(0); } catch (\Exception $e) { exit(1); } ... 


Parsing function
 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); } } } 


In this method there is a problem of deleting a phone number when a client disconnects from the channel. To solve, you can use the disconnect event:

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


Handling a disconnect event
 ... 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); } } ... 


As a result, it turned out that the second method is more efficient, since when working with asterisk events it often fell, and, as a result, some calls were lost. Also, in the first method, calls were not recognized when redirecting from the call-center, since the employee and client numbers were in different channels (the first channel connects the call-center and the employee, the second channel connects the call-center and the client).

Information about the call through Notifications


For information about incoming calls, the event-source-polyfill plugin and long-pull requests to the server were used. Let me remind you that we store incoming calls in memcached.

Practice has shown that if an employee opens many tabs, then a large number of requests are generated. To prevent this, the wormhole plugin was used, which transmits information about the channel between the tabs.

The result is the following script:

The script to send a notification
 (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); 


Handler for long-pull requests
 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; } 


Results of implementation


Source: https://habr.com/ru/post/278859/


All Articles