アスタリスクとブラウザの着信コールに関する情報

見出しを読んだ後、おそらく「ハックされたトピック、それについてどれだけ書くことができるか」と思うでしょうが、それでも松葉杖と自転車を共有することはできませんでした。



はじめに



当社では、顧客はPBXを介して電話で記録されました(私はこの問題には強くなく、誤解される可能性があります)。 すべての注文はデータベースに保存され、Webアプリケーションがインターフェイスとして機能します。 特定の時間帯の通話密度は非常に高くなる可能性があり、ヒューマンファクターにより、ディスパッチャは常に(電話の画面に表示されたとき)顧客の電話を常に正しく記録したり記録したりしません。



しかし、進歩は止まりませんでした。 古い電話交換の場所は、アスタリスク13が担当しました。





彼らがこれを達成したかったもの:





ツール



たとえば、いくつかの記事を読んだ後、 これを 「なぜ私が悪いのか」と判断し、問題を解決するという私のビジョンを見つけました。



アスタリスクの束にとどまることにしました- パミ - ラチェット



コンセプト



pamiデーモンは、着信呼び出しをアスタリスクでリッスンしています。 並列回転するWebSocketサーバー。 着信コールが到着すると、情報が解析され、websocketクライアント(存在する場合)に送信されます。



実装



アスタリスクデーモン
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; } }
      
      







必要なイベントのアスタリスクを定期的にポーリングします。 正直に言うと、適切なイベントに参加したかどうかは議論しませんが、すべてがこれらのイベントで機能しました。 正確に必要なものに応じて、多くのイベントから同様の情報を取得できます。



イベントリスナー
 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); } } }
      
      







さて、ここでも、すべてが明確です。 受け取ったイベント。 今、彼らは処理する必要があります。 サーバーが誰であるかは、以下で明確になります。



Websocketサーバー
 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; } }
      
      







実際には、websocketサーバー。 私は交換フォーマットを気にしませんでした、私はJSONを選びました。 ここでは、クライアントがサーバーへの接続を書き換えていることに注意する価値があります。 これにより、ブラウザで多くのタブを開いたときに回答を作成できなくなります。



Websocketクライアント
 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; } }
      
      







さて、何を追加すればいいのかわかりません。 id-ディスパッチャーの電話ID。 呼び出しがどのディスパッチャに到達したかを判断する必要があります。



ロケットを発射します
 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); }
      
      







websocketサーバーとアスタリスクデーモンが共通のストリーム(ループ)を使用することに注意してください。 そうでなければ、それらの1つは獲得できませんでした。



そして、Webアプリケーションの状況はどうですか?



まあ、すべてが簡単です。 電話番号やその他のナンセンスでクライアントに関する情報を取得する方法に関する情報はロードしません。



通知スクリプト
 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-ディスパッチャーの電話識別子。



おわりに



目標を達成しました。 それは私が予想したよりもさらに良い場所で動作します。



記事には含まれていなかったが、何が行われたか





PS



コードの品質を厳密に判断しないでください。 この例は概念のみを示していますが、実稼働では正常に機能します。 私にとっては、アスタリスクとWebSocketの素晴らしい経験でした。



All Articles