Home Development of Websites Asterisk and information about incoming calls in the browser via Notifications

Asterisk and information about incoming calls in the browser via Notifications

by admin

Our company uses an 8800 phone so that customers can place an order without accessing the website.A call center is used to handle most incoming calls, and there is a redirect to an internal employee if necessary.
For the convenience of employees and the ability to provide a personalized response, a system was implemented to recognize incoming calls from an internal customer base.
Since cron jobs would be too infrequent (once per second at most), a php based daemon was used to scan channels and send call information to a temporary repository. For the temporary storage memcached was used.
The version of Asterisk in use is 11.15.1.
As an API of the php and Asterisk’a bundle – PAMI module
Basic class of 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) { // creation error exit;}elseif ($pid) { // parent, waiting for offspring to execute\{pcntl_waitpid($pid, $status, WUNTRACED);if ($status > 0) {// if there is an error in the channel, recreate$asterisk-> close();usleep(1000);$asterisk-> open();}return;} else {// child process executiontry {$asterisk-> process();exit(0);} catch ({ {Exception $e) {exit(1);}}});// restoring subprocesses$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 ways of recognition: listening to channel events and manual parsing of information in CoreShowChannel, let’s look at everything in order.

Listening to events

Add AsteriskEventListener event listener initialization to the daemon constructor:
Listener of events

..$this-> asterisk-> registerEventListener(new AsteriskEventListener($memcache), function (EventMessage $event) {// Listening only for channel operation eventsreturn $event instanceof BridgeEvent;});$this-> asterisk-> open();...

And accordingly the class itself of 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){// Doing recognition if a channel creation/deletion event has arrivedif ($event instanceof BridgeEvent) {$this-> bridges = $this-> memcache-> getKey('asterisk-bridges');$state = $event-> getBridgeState();$caller1 = $event-> getCallerID1();$caller2 = $event-> getCallerID2();if ($state == 'Link') { // Channel Creation$this-> addBridge($caller1, $caller2);} else { // Deleting a channel$this-> deleteBridge($caller1, $caller2);}$this-> memcache-> setKey('asterisk-bridges', $this-> bridges);}}}

In this variant, there may be problems when creating channels. The point is that when a call is redirected between employees or redirected from the call center to an employee both channels will be created in conjunction with the one who redirected, and no information about the resulting operator-customer connection will be available.

Manual parsing of CoreShowChannel information

This method requires some modification of the daemon, we call the CoreShowChannel event forcibly, since Asterisk itself does not generate it :
CoreShowChannels event generation

..// child process executes processtry {$message = $asterisk-> send(new CoreShowChannelsAction());$events = $message-> getEvents();$this-> parse($events);$asterisk-> process();exit(0);} catch ({\Exception $e) {exit(1);}...

Parse 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);}}}

This method has the problem of removing the phone number when the client is disconnected from the channel. To solve it, you can use the connection break event :
Connection break event

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

Connection break 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);}}...

It turned out that the second method is more effective, because when working with events asterisk often crashed, and as a result, some calls were lost. Also the first method did not recognize the calls when forwarded from the call center, because the number of employee and customer were in different channels (the first channel connects the call center and the employee, the second channel connects the call center and the customer).

Call information via Notifications

A plugin was used to get information about incoming calls event-source-polyfill and long-pull requests to the server. Let me remind you that we store incoming calls in memcached.
Practice showed that if an employee opens many tabs, then a large number of requests is generated. To prevent this the plugin was used wormhole which transfers channel information between tabs.
The following script came up :
Script to send 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;}};// server requests only on the main tabwormhole().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: *');// getting the number of the current operator$managerPhone = $_SESSION['phone'];$user = null;$clientPhone = $this-> getPhone($managerPhone);if ($clientPhone) {$user = User::find()-> where(['phone' => $clientPhone])-> one();}if ($user) { // Increase time until next call if the client is foundecho "retry: 30000\n";} else {echo "retry: 3000\n";}echo 'id: ' . $managerPhone . "\n";$data = [];if ($user) {$data = ['id' => $user['id'], 'title' => 'New call from ' . $user['name'], 'text' => 'Go to client's card, 'img' => '/phone.png', 'url' => '/user/' . $user['id']];}echo "data: " . json_encode($data) . "\n\n";}//Getting the client's phone numberpublic 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

  • Quite an interesting experience with Asterisk and the Notifications system for different browsers.
  • Personalization of incoming calls.
  • Instant number search in the database and the ability to quickly navigate to a customer card.
  • Employees got a useful notification service for incoming calls.

You may also like