Callback или обратный звонок на Asterisk

Сейчас в Интернет доступно немало «облачных» сервисов предлагающих услугу callback: callbackhunter.ru, redhelper.ruuptocall.com и т.п. на платной основе. Технология помогает облегчить жизнь клиента зашедшего на сайт, дав ему возможность оперативно связаться с организацией, причем бесплатно. Это некоторый аналог бесплатного номера 8-800, но с бонусом, поскольку не нужно проходить надоедливый IVR квест. Используя эту технологию организация перестает «воровать» время клиента на ожидание ответа оператора.

Выглядит все довольно просто. На сайте размещается привлекающая внимание «кнопка», нажав на которую появляется форма, куда можно вбить свой номер телефона и стартовать обратный звонок. Далее клиент может заниматься своими делами и ожидать звонка.

Реализовать функционал callback помимо «облачных» сервисов можно и самостоятельно, если в организации есть АТС, которой можно управлять программно. В данной статье будет рассмотрена реализация callback на softswitch Asterisk.

Callback на Asterisk

У Asterisk есть несколько основных способов инициирования звонка:

  • через консоль,
  • с помощью файла в /var/spool/asterisk/,
  • с помощью AMI.

Для решения задачи воспользуемся AMI, так как он изначально предназначен для работы по сети.

1. Чтобы воспользоваться AMI в /etc/asterisk/manager.conf создаем следующую запись:

[callback]  — логин
secret = XXXXXXX — пароль
deny = 0.0.0.0/0.0.0.0
permit = 192.168.XXX.XXX/255.255.255.255 — с этого адреса разрешено авторизоваться.
read = system,command,originate — разрешение на чтение.
write = system,call,originate — разрешение на выполнение.

2. Пишем скрипт, например, на php, который будет подключаться к Asterisk.
Это скрипт скорее для примера. Использовать его на реальном сайте не получится, поскольку для размещения кнопки вызова на страницах сайта запрос к серверу обрабатывающему звонки должен происходить асинхронно, с помощью AJAX запроса.

<?
$strHost = "192.168.XXX.XXX"; адрес сервера asterisk
$strUser = "callback"; - логин для подключения к ami
$strSecret = "XXXXXXX"; -пароль кот. Указали в manager.conf
$strChannel = "LOCAL/1234@callback"; канал с которого будет создаваться звонок,  тут можно по разному написать, если у вас всего один оператор будет совершать звонок то можно просто указать SIP/1234, но в таком случае вы не сможете дополнительно обрабатывать звонок,  удобнее указать did (номер назначения) и контекст, в моём примере контекст callback а номер 1234
$strContext = "callback"; контекст в которому будет совершаться исходящий звонок
$strWaitTime = "30";
$strPriority = "1";
$strMaxRetry = "2";

$strExten = $_POST['txtphonenumber'];
#specify the caller id for the call
$strCallerId = "Web Call <$strExten>";
$length = strlen($strExten);

if ($length >= 7 && is_numeric($strExten))
{
$oSocket = fsockopen($strHost, 5038, $errnum, $errdesc) or die("Connection to host failed");
fputs($oSocket, "Action: login\r\n");
fputs($oSocket, "Events: off\r\n");
fputs($oSocket, "Username: $strUser\r\n");
fputs($oSocket, "Secret: $strSecret\r\n\r\n");
fputs($oSocket, "Action: originate\r\n");
fputs($oSocket, "Channel: $strChannel\r\n");
fputs($oSocket, "WaitTime: $strWaitTime\r\n");
fputs($oSocket, "CallerId: $strCallerId\r\n");
fputs($oSocket, "Exten: $strExten\r\n");
fputs($oSocket, "Context: $strContext\r\n");
fputs($oSocket, "Priority: $strPriority\r\n\r\n");
fputs($oSocket, "Action: Logoff\r\n\r\n");
sleep (1);
fclose($oSocket);
?>
<p>
<table width="300" border="1" bordercolor="#630000" cellpadding="3" cellspacing="0">
        <tr><td>
        <font size="2" face="verdana,georgia" color="#630000">Производится вызов. Подождите пока Ваш телефон зазвонит!<br>Если телефон не позвонил в течении минуты, попробуйте ещё раз.<br><a href="<? echo $_SERVER['PHP_SELF'] ?>">Ещё раз</a></font>
        </td></tr>
</table>
</p>
<?
}
else
{
?>
<p>
<table width="300" border="1" bordercolor="#630000" cellpadding="3" cellspacing="0">
        <tr><td>
        <font size="2" face="verdana,arial,georgia" color="#630000">Введите Ваш номер без цифры 8 (XXX)XXX-XX-XX.</font>
        <form action="<? echo $_SERVER['PHP_SELF'] ?>" method="post">
                <input type="text" size="20" maxlength="11" name="txtphonenumber"><br>
                <input type="submit" value="Позвонить!">
        </form>
        </td></tr>
</table>
</p>
<?
}
?>

Скрипт в примере выкладываем на web сервер, который должен иметь доступ к серверу Asterisk по указанному адресу и порту.

3. Возвращаемся к настройке Asterisk. В /etc/asterisk/extensions.conf создадим контекст callback где и опишем нужные нам dialplan

[callback] — Здесь указываем маршрут к операторам которые будут совершать обратный звонок.
exten => 1234,1,Queue(5185,tr,,)

Далее описываем маршруты на исходящие номера. Поскольку обычно клиенты оставляют номера сотовых телефонов, коды которых начинаются с 9, а для для проверки качества работы операторов разговоры будем записывать и таким образом маршрут будет таким:.
exten => _9XXXXXXXXX,1,Set(CALLFILENAME=${STRFTIME(${EPOCH},,%y-%m-%d__%H-%M)}_${CALLERID(number)}_${EXTEN}); (формируем имя файла записи разговора)
exten => _9XXXXXXXXX,n,MixMonitor(${CALLFILENAME}.gsm); (включение записи)
exten => _9XXXXXXXXX,n,Dial(SIP/cisco/8${EXTEN},,tTm)
exten => _9XXXXXXXXX,n,StopMonitor; (выключение записи.)

Делаем проверку успешности обратного звонка, и если он не состоялся, то переходим в контекст noanswer и отсылаем уведомление по электронной почте о пропущенном звонке. Целесообразно сообщение отправлять в соотвествующую очередь в ServiceDesk системе, в которой операторы принимают заявки от клиентов.

exten => h,1,Set(WAITTIME=10)
exten => h,n,Set(CTALL=$[${CDR(duration)}])
exten => h,n,Set(CTANSWER=$[${CDR(billsec)}])
exten => h,n,Set(CTDTIME=$[${CTALL}-${CTANSWER}])
exten => h,n,NoOP(${CDR(disposition)})
exten => h,n,NoOP(${CTDTIME})
exten => h,n,GotoIf($[«${CDR(disposition)}» = «NO ANSWER»]?noanswer,s,1)
exten => h,n(hang),Hangup()

[noanswer]
exten => s,1,NoOp(UID CALL: ${UNIQUEID} / DATE: ${STRFTIME(${EPOCH},,%Y%m%d-%H%M%S)}))
exten => s,n,System(echo «Неотвеченный вызов с номера ${CALLERID(NUM)} в ${STRFTIME(${EPOCH},,%H:%M)} Время ожидания абонента на линии составило ${CTDTIME} сек» | iconv -t WINDOWS-1251 | mail -s «8800: a missing call ${STRFTIME(${EPOCH},,%d.%m.%Y)} ${STRFTIME(${EPOCH},,%H:%M)}» email@company.ru)
exten => s,n,Hangup()

Скрипт для рамещения на сайте

Как уже упоминалось ранее, для размещения скрипта на сайте обращение к серверу должно происходить асинхронно, с помощью AJAX запроса. Скрипт отображающий фоорму для ввода номера телефона выглядит  следующим образом

jQuery(document).ready(function($) {

    // Сворачиваем поле при открытии страницы
    //$('#rc-phone-form').css('width', '4.8em');
    
    $('#rc-phone').mouseenter(function(){
        // При наведении мыши на круг - раскрывается поле
        $('#rc-phone-form').css("overflow", "visible").show().animate({width: "375px"}, 500);
        $('#rc-phone').removeClass("rc-inactive");

        // При наведении мыши на поле - показываем крестик
        if ( $('#rc-phone-form').width() == "375" ) {
            $('#rc-phone-form-close').show();
            $('#rc-phone-form').css("overflow", "visible");
        }
    });
    
    // Скрываем крестик, когда мышь удаляется от поля
    $('#rc-phone').mouseleave(function(){
        $('#rc-phone-form-close').hide();
    });   
    
    // Смена фона при наведении мыши на кнопку отправки номера
    $('#rc-phone-button').mouseenter(function(){$('#rc-phone-button').css("background-color", "#18A629").addClass("rc-pressed")}).mouseleave(function(){$('#rc-phone-button').css("background-color", "#EEEEEE").removeClass("rc-pressed")}); 
    
    // Проверка номера телефона перед отправкой формы
    $('#form-phone').submit(function(){
        if ( ! $('#rc-phone-input').val() || $('#rc-phone-input').val().match(/[^0-9]/g) ) {
            $('#rc-phone-input-warning').addClass('rc-active');
            return false;
        }
        else{
            $('#rc-phone-input-warning').removeClass('rc-active');
        }
    })
    
    // Скрываем поле после нажатия на крестик
    $('#rc-phone-form-close').click(function(){
        $('#rc-phone-form-close').hide();
        $('#rc-phone-form').animate({width: "4.8em"}, 300, function(){
            $('#rc-phone-form').css("overflow", "hidden");
            $('#rc-phone').addClass("rc-inactive");
        });

    });
    
    // Проверка на ввод чисел
    $('#rc-phone-input').bind("change keyup input click", function() {
        $('#rc-phone-input-warning').removeClass('rc-active');
        if (this.value.match(/[^0-9]/g)) {
            this.value = this.value.replace(/[^0-9]/g, '');
        }
        if (this.value.length > 10) {
            this.value = this.value.substring(0,10);
        }
    });
    
    // AJAX запрос
    jQuery("#form-phone").submit( function() {
        
        // номер телефона, который отправляем в обработчик
        var txtphonenumber = jQuery('#rc-phone-input').val();
        
        jQuery.ajax({
            type : "post",
            dataType : "json",
	    url : "http://www.test.ru/callback.php",
            data : {txtphonenumber: txtphonenumber},
            success: function(response) {               
                if( response.error_msg ) {

                    jQuery("#rc-phone-input-warning").addClass("rc-active");
                    jQuery("#rc-phone-input-warning").children().text(response.error_msg);

                }
            }
        });   
    });

});

На приемной стороне скрипт такой:

<?php
#--------------------------------------------------------------------------------------------
#Shouldn't need to edit anything below this point to make this script work
#--------------------------------------------------------------------------------------------
#get the phone number from the posted form
$strExten = !empty($_POST['txtphonenumber']) ? $_POST['txtphonenumber'] : '' ;
$default_error_msg = 'Проверьте правильность набранного номера.';
$error_msg = '';

// Проверка введенного номера
if ( ! empty($strExten) && ! preg_match('/^[0-9]{4,11}$/', $strExten) ) {
    $error_msg = 'Проверьте правильность набранного номера.';
}
elseif( ! empty($strExten) ) {

         $strHost = "192.168.XXX.XXX";
        $strUser = "test";
        $strSecret = "test";
        $strChannel = "LOCAL/1234@callback";
        $strContext = "test";
        $strWaitTime = "30000";
        $strPriority = "1";
        $strMaxRetry = "2";

    #specify the caller id for the call
    $strCallerId = "Web Call <$strExten>";

    $length = strlen($strExten);

    if ($length >= 7 && is_numeric($strExten))
    {
        $oSocket = fsockopen($strHost, 5038, $errnum, $errdesc) or die("Connection to host failed");
        fputs($oSocket, "Action: login\r\n");
        fputs($oSocket, "Events: off\r\n");
        fputs($oSocket, "Username: $strUser\r\n");
        fputs($oSocket, "Secret: $strSecret\r\n\r\n");
        fputs($oSocket, "Action: originate\r\n");
        fputs($oSocket, "Channel: $strChannel\r\n");
        fputs($oSocket, "Timeout: $strWaitTime\r\n");
        fputs($oSocket, "CallerId: $strCallerId\r\n");
        fputs($oSocket, "Exten: $strExten\r\n");
        fputs($oSocket, "Context: $strContext\r\n");
        fputs($oSocket, "Variable: var1=23|var2=24|var3=25\r\n");
        fputs($oSocket, "Priority: $strPriority\r\n\r\n");
        fputs($oSocket, "Action: Logoff\r\n\r\n");
        sleep (1);
        fclose($oSocket);

        $error_msg = 'Пожалуйста, подождите. Производится вызов.';
    }
}

// Данные, которые отправляется обратно в форму.
$result = array(
    "error_msg"     => $error_msg
);

echo json_encode( $result );
exit();

Соответственно, PHP скрипт должен иметь прямой доступ к Asterisk и к нему должен быть разрешен доступ по 80 порту. Для безопасности на firewall нужно прописать IP адрес внешнего сайта с которого JavaScript будет обращаться к PHP скрипту.

Ну и бонус в виде плагина для WordPress для размещения скрипта Callback на сайте. В footer.php темы WordPress нужно разместить следующий код для вызова формы Callback:

    <?php 
    if ( function_exists('getRidanPCallForm') ) {
        getRidanPCallForm(); 
    }
    ?>

Spread the love
Запись опубликована в рубрике IT опыт, IT рецепты, IT решения для бизнеса с метками , , , , , , . Добавьте в закладки постоянную ссылку.