📜 ⬆️ ⬇️

Asterisk and information about incoming calls in the browser

After reading the title, you will probably think “A worn topic, but how much you can write about it,” but still could not help but share your bikes with crutches .

Introduction


In our company, clients were recorded by telephone through a mini-automatic telephone exchange (I am not strong in this matter and can be mistaken). All orders were stored in a database, the interface is a web application. The density of calls at certain moments is very high and dispatchers, due to the human factor, do not always correctly record the client’s phone (when it is displayed on the phone screen).

But progress did not stand still. The place of the old PBX was Asterisk 13. I also needed:


What wanted to achieve this:
')

Instruments


After reading several articles, for example, I decided this “why am I worse?” And found his own vision of solving the problem.

I decided to stop at a bunch of asterisk - pami - ratchet

Concept


A pami daemon listens on asterisk for incoming calls. Parallel websocket spinning server. When an incoming call arrives, the information is parsed and sent to the websocket to the client (if there is one).

Implementation


Demon asteriska
namespace Asterisk; use PAMI\Client\Impl\ClientImpl as PamiClient; use PAMI\Message\Event\EventMessage; use PAMI\Message\Event\HangupEvent; use PAMI\Message\Event\NewstateEvent; use PAMI\Message\Event\OriginateResponseEvent; use PAMI\Message\Action\OriginateAction; use React\EventLoop\Factory; class AsteriskDaemon { private $asterisk; private $server; private $loop; private $interval = 0.1; private $retries = 10; private $options = array( 'host' => 'host', 'scheme' => 'tcp://', 'port' => 5038, 'username' => 'user', 'secret' => ' password', 'connect_timeout' => 10000, 'read_timeout' => 10000 ); private $opened = FALSE; private $runned = FALSE; public function __construct(Server $server) { $this->server = $server; $this->asterisk = new PamiClient($this->options); $this->loop = Factory::create(); $this->asterisk->registerEventListener(new AsteriskEventListener($this->server), function (EventMessage $event) { return $event instanceof NewstateEvent || $event instanceof HangupEvent; }); $this->asterisk->open(); $this->opened = TRUE; $asterisk = $this->asterisk; $retries = $this->retries; $this->loop->addPeriodicTimer($this->interval, function () use ($asterisk, $retries) { try { $asterisk->process(); } catch (Exception $exc) { if ($retries-- <= 0) { throw new \RuntimeException('Exit from loop', 1, $exc); } sleep(10); } }); } public function __destruct() { if ($this->loop && $this->runned) { $this->loop->stop(); } if ($this->asterisk && $this->opened) { $this->asterisk->close(); } } public function run() { $this->runned = TRUE; $this->loop->run(); } public function getLoop() { return $this->loop; } } 


Serves for a periodic survey of asterisk`a for the events we need. To be honest, I will not say if I took the right events, but everything worked with these. Just similar information can be obtained from many events depending on what you need.

Event listener
 namespace Asterisk; use PAMI\Message\Event\EventMessage; use PAMI\Listener\IEventListener; use PAMI\Message\Event\NewstateEvent; use PAMI\Message\Event\HangupEvent; use PAMI\Message\Event\OriginateResponseEvent; class AsteriskEventListener implements IEventListener { private $server; public function __construct(Server $server) { $this->server = $server; } public function handle(EventMessage $event) { // getChannelState 6 = Up getChannelStateDesc() // TODO    BridgeEnterEvent if ($event instanceof NewstateEvent && $event->getChannelState() == 6) { $client = $this->server->getClientById($event->getCallerIDNum()); if (!$client) { return; } $client->setMessage($event); // TODO    BridgeLeaveEvent } elseif ($event instanceof HangupEvent) { $client = $this->server->getClientById($event->getCallerIDNum()); if (!$client) { return; } $client->setMessage($event); } } } 


Well, here, too, everything is clear. Events we received. Now they need to be processed. Who is the server will become clearer below.

Websocket server
 namespace Asterisk; use Ratchet\MessageComponentInterface; use Ratchet\ConnectionInterface; class Server implements MessageComponentInterface { /** *   * @var SplObjectStorage */ private $clients; /** *     asterisk * @var AsteriskDaemon */ private $daemon; public function __construct() { $this->clients = new \SplObjectStorage; $this->daemon = new AsteriskDaemon($this); } function getLoop() { return $this->daemon->getLoop(); } public function onOpen(ConnectionInterface $conn) { //echo "Open\n"; } public function onMessage(ConnectionInterface $from, $msg) { //echo "Message\n"; $json = json_decode($msg); if (json_last_error()) { echo "Json error: " . json_last_error_msg() . "\n"; return; } switch ($json->Action) { case 'Register': //echo "Register client\n"; $client = $this->getClientById($json->Id); if ($client) { if ($client->getConnection() != $from) { $client->setConnection($from); } $client->process(); } else { $this->clients->attach(new Client($from, $json->Id)); } break; default: break; } } public function onClose(ConnectionInterface $conn) { //echo "Close\n"; $client = $this->getClientByConnection($conn); if ($client) { $client->closeConnection(); } } public function onError(ConnectionInterface $conn, \Exception $e) { echo "Error: " . $e->getMessage() . "\n"; $client = $this->getClientByConnection($conn); if ($client) { $client->closeConnection(); } } /** * * @param ConnectionInterface $conn * @return \Asterisk\Client or NULL */ public function getClientByConnection(ConnectionInterface $conn) { $this->clients->rewind(); while($this->clients->valid()) { $client = $this->clients->current(); if ($client->getConnection() == $conn) { //echo "Client found by connection\n"; return $client; } $this->clients->next(); } return NULL; } /** * * @param string $id * @return \Asterisk\Client or NULL */ public function getClientById($id) { $this->clients->rewind(); while($this->clients->valid()) { $client = $this->clients->current(); if ($client->getId() == $id) { //echo "Client found by id\n"; return $client; } $this->clients->next(); } return NULL; } } 


Actually our websocket server. I did not bother with the exchange format, I chose JSON. Here it is worth paying attention that at clients the connection with the server is overwritten. This allows you to not produce answers when opening many tabs in the browser.

Websocket client
 namespace Asterisk; use Ratchet\ConnectionInterface; use PAMI\Message\Event\EventMessage; use PAMI\Message\Event\NewstateEvent; use PAMI\Message\Event\HangupEvent; use PAMI\Message\Event\OriginateResponseEvent; class Client { /** *   * @var PAMI\Message\Event\EventMessage */ private $message; /** *    * @var Ratchet\ConnectionInterface */ private $connection; /** *    * @var string */ private $id; /** *   .   * @var int */ private $lastactive; public function __construct(ConnectionInterface $connection, $id=NULL) { $this->connection = $connection; if ($id) { $this->id = $id; } $this->lastactive = time(); } function getConnection() { return $this->connection; } function setConnection($connection) { $this->connection = $connection; } function closeConnection() { $this->connection->close(); $this->connection = NULL; } public function getMessage() { return $this->message; } public function setMessage(EventMessage $message) { $this->message = $message; $this->process(); } public function process() { if (!$this->connection || !$this->message) { return; } if ($this->message instanceof NewstateEvent) { $message = array('event' => 'incoming', 'value' => $this->message->getConnectedLineNum()); } elseif ($this->message instanceof HangupEvent) { $message = array('event' => 'hangup'); } else { return; } $json = json_encode($message); $this->connection->send($json); } function getId() { return $this->id; } function setId($id) { $this->id = $id; } } 


Well here I do not know what to add. id - ID of the dispatcher's phone. Needed to determine which of the dispatchers received a call.

Now we launch the rocket
 require_once implode(DIRECTORY_SEPARATOR, array(__DIR__ , 'vendor', 'autoload.php')); //use Ratchet\Server\EchoServer; use Asterisk\Server; try { $server = new Server(); $app = new Ratchet\App('192.168.0.241', 8080, '192.168.0.241', $server->getLoop()); $app->route('/asterisk', $server, array('*')); $app->run(); } catch (Exception $exc) { $error = "Exception raised: " . $exc->getMessage() . "\nFile: " . $exc->getFile() . "\nLine: " . $exc->getLine() . "\n\n"; echo $error; exit(1); } 


It's worth noting that the websocket server and our asterisk daemon use a common thread (loop). Otherwise, someone from them would not have earned.

And how are things going in the web application?


Well, everything is simple. I will not load information on how to pull out customer information by phone number and other nonsense.

Notification script
 function Asterisk(address, phone) { var delay = 3000; var isIdle = true, isConnected = false; var content = $('<div/>', {id: 'asterisk-content', style: 'text-align: center;'}); var widget = $('<div/>', {id: 'asterisk-popup', class: 'popup-box noprint', style: 'min-height: 180px;'}) .append($('<div/>', {class: 'header', text: ''})) .append(content).hide(); var input = $('#popup-addorder').find('input[name=phone]'); var client = connect(address, phone); $('body').append(widget); function show() { widget.stop(true).show(); }; function hide() { widget.show().delay(delay).fadeOut(); }; function connect(a, p) { if (!a || !p) { console.log('Asterisk: no address or phone'); return null; } var ws = new WebSocket('wss://' + a + '/wss/asterisk'); ws.onopen = function() { isConnected = true; this.send(JSON.stringify({Action: 'Register', Id: p})); }; ws.onclose = function() { isConnected = false; content.html($('<p/>', {text: ''})); hide(); }; ws.onmessage = function(evt) { var msg = JSON.parse(evt.data); if (!msg || !msg.event) { return; } switch (msg.event) { case 'incoming': var p = msg.value; content.html($('<p/>').html('<br>' + p)) .append($('<p/>').html($('<a/>', {href: '?module=clients&search=' + p, class: 'button'}) .html($('<img/>', {src: '/images/icons/find.png'})).append(' '))); input.val(p); show(); isIdle = false; break; case 'hangup': if (!isIdle) { content.html($('<p/>', {text: ''})); hide(); isIdle = true; } break; default: console.log('Unknown event' + msg.event); } }; ws.onerror = function(evt) { content.html($('<p/>', {text: ''})); hide(); console.log('Asterisk: error', evt); }; return ws; }; }; 


phone is the dispatcher's phone ID.

Conclusion


I achieved my goals. It works in places even better than I expected.

What was not included in the article, but what was done



PS


Do not judge strictly for the quality of the code. The example shows only the concept, although it works successfully in production. For me it was a wonderful experience with asterisk and websocket.

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


All Articles