はじめに
当社では、顧客はPBXを介して電話で記録されました(私はこの問題には強くなく、誤解される可能性があります)。 すべての注文はデータベースに保存され、Webアプリケーションがインターフェイスとして機能します。 特定の時間帯の通話密度は非常に高くなる可能性があり、ヒューマンファクターにより、ディスパッチャは常に(電話の画面に表示されたとき)顧客の電話を常に正しく記録したり記録したりしません。
しかし、進歩は止まりませんでした。 古い電話交換の場所は、アスタリスク13が担当しました。
- 着信Webアプリケーションに関する情報を転送する
- Webアプリケーションから発信コールを行う機能を追加します
彼らがこれを達成したかったもの:
- 呼処理時間を短縮
- 録音クライアントのエラーを削減
- 顧客との電話時間を短縮
ツール
たとえば、いくつかの記事を読んだ後、 私はこれを 「なぜ私が悪いのか」と判断し、問題を解決するという私のビジョンを見つけました。
アスタリスクの束にとどまることにしました- パミ - ラチェット
コンセプト
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-ディスパッチャーの電話識別子。
おわりに
目標を達成しました。 それは私が予想したよりもさらに良い場所で動作します。
記事には含まれていなかったが、何が行われたか
- ami経由で接続するようにアスタリスクを設定する
- 発信による発信コール
- デーモンの動作と秋の上昇を監視するBashスクリプト
PS
コードの品質を厳密に判断しないでください。 この例は概念のみを示していますが、実稼働では正常に機能します。 私にとっては、アスタリスクとWebSocketの素晴らしい経験でした。