Asterisk and information about incoming calls in the browser through Notifications

Our company uses the 8800 telephone so that customers can place an order without access to the site. To service most incoming calls, a call center is used, and if necessary, a redirection to an internal employee occurs.

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

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

The version of Asterisk used is 11.15.1.
As an API, php and Asteriska'a bundles are PAMI modules .

Core class demon wiretap
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 manually parsing 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 listening and working with the temporary storage class itself:

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 option, there may be problems 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 one who redirected, and there will be no information about the resulting connection between the operator and the client.

Manual analysis of information CoreShowChannel


For this method to work, it is necessary to modify the daemon somewhat, we call the CoreShowChannel event forcibly, since Asterisk itself does not generate it:

CoreShowChannels event generation
...
// дочерний процесс выполняет процесс
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 the phone number when disconnecting the client from the channel. To solve this, you can use the disconnect event:

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


Handle event handling
...
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 effective, since when working with events asterisk often crashed, and, as a result, some calls were lost. Also, in the first method, calls were not recognized when redirecting from a 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).

Call Information via Notifications


To obtain information about incoming calls, the event-source-polyfill plugin and long-pull requests to the server were used. Let me remind you, 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 transfers channel information between tabs.

The following script turned out:

Notification sending script
(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);


Long-pull request handler
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;
}


Implementation Results


  • Interesting enough experience with Asterisk and the Notifications system for various browsers.
  • Personalization of incoming calls.
  • Instant number search in the database and the ability to quickly switch to the client card.
  • Employees received a useful alert service for incoming calls.

Also popular now: