IP ATC Asterisk — это мощный комбайн в области IP-телефонии. А web-интерфейс FreePBX, созданный для Asterisk, значительно упрощает настройку и снижает порог вхождения в систему.
Если вы можете придумать какую-либо задачу, связанную с IP-телефонией, то почти наверняка это можно реализовать в Asterisk. Но будьте уверены, что от вас потребуется упорство и выдержка.
Перед нами встала задача настроить e-mail уведомления о пропущенных вызовах. Точнее говоря, оповещать через e-mail о тех случаях, когда входящий вызов перешёл в очередь, но никто (из агентов) так и не ответил на этот входящий вызов.
На удивление мы не обнаружили штатных средств для решения этой задачи во FreePBX. О том, как мы решили эту задачу, расскажу под катом.
Предисловие
Перед решением задачи «в лоб» мы конечно поискали информацию в интернете, но решения под ключ не нашли (возможно плохо искали, но что поделаешь… ).
Навыков работы непосредственно в Asterisk не так много, как хотелось бы, поэтому решение, предлагаемое здесь, не было до конца осмыслено и было отброшено.
Понравилось решение, предложенное здесь, хоть оно и не заработало. Отсюда подчеркнули, что работать в Asterisk нужно в контексте очередей [ext-queues]. И так как мы работаем во Freepbx, то работать нужно в файле конфигурации «extensions_override_freepbx.conf». Обратили внимание на то, что «ловить пропущенные вызовы» удобно перед событием hangupcall (окончание вызова).
Прочитав обсуждение здесь, появилась идея о том, что нужно фильтровать в CDR переменную «Disposition» по всем агентам в очереди. А после прочтения этой информации сформировались вполне конкретные шаги по решению поставленной задачи.
Что у нас есть:
Есть FreePBX 13.0.197, который использует Asterisk 13.12.1. Версия ОС SHMZ release 6.6 (Final). Дистрибутив базируется на CentOS.
В Asterisk настроен IVR (голосовое меню) раскидывающий входящие вызовы на разные Queues (очереди). Каждой очереди назначены Agents (агенты), т. е. операторы.
Теория
Что происходит в Asterisk
Когда на Asterisk поступает входящий вызов, этот вызов попадает на IVR. Звонящий делает выбор, нажав определенную цифру на телефоне, и попадает в определенную очередь. После этого всем свободным агентам очереди одновременно поступает звонок.
Для того чтобы лучше понять, что происходит в этот момент и что происходит дальше, обратимся к Report CDR (Рис.1).
Рис.1
Когда входящий вызов попал в очередь, у всех агентов значение переменной «Disposition» стало равным «NO ANSWER», если агенты в этот момент были не заняты. Переменная «Disposition» могла принять и другие значения (см. https://asterisk-pbx.ru/wiki/asterisk/cf/cdr), кроме значения «ANSWERED». А в тот момент, когда один из агентов отвечает на входящий вызов, значение переменной «Disposition» этого агента становится равной «ANSWERED».
Из Report CDR можно заметить, что когда вызов перешел в очередь (в колонке App значение становится равным «Queue»), то все события фигурируют с одинаковым «uniqueid» (колонка System).
Коротко о CDR
Важно понимать что такое CDR, и в какой именно момент в CDR заносятся данные, которые мы наблюдаем в Report CDR. CDR, относительно операционной системы — это база данных, в которую Asterisk записывает детализированный отчет вызовов (см. https://asterisk-pbx.ru/wiki/asterisk/cf/cdr). В нашем случае это база под именем asteriskcdrdb, которая находится в mysql. Опытным путем мы установили, что данные о вызове с определенным «uniqueid» заносятся в asteriskcdrdb не сразу после возникновения какого-либо события, а после события hangupcall (окончание вызова).
Принцип работы созданного решения
Так как у нас познаний в bash больше, чем познаний в Asterisk, то основная идея получилась следующей. Перед событием hangupcall вызвать bash-скрипт. В этот скрипт передать 3 параметра. Первый параметр «uniqueid», для фильтрации данных, получаемых из CDR. Второй параметр «CALLERID(num)» (номер звонившего), чтобы знать кому перезвонить. Третий параметр «NODEST» (номер очереди), в которую поступил звонок, для того, чтобы знать по какому вопросу был звонок, и кому отправить e-mail уведомление о пропущенном вызове.
Bash-скрипт должен подключиться к базе asteriskcdrdb в mysql и взять все значения переменной «Disposition» с определенным «uniqueid». Из полученных данных нужно исключить значения: «NO ANSWER», «BUSY», «FAILED», «UNKNOWN». В результате останутся либо «ANSWERED» — на входящий вызов ответили, либо вообще ни чего — пропущенный вызов.
Далее, если вызов оказался пропущенным, то скрипт должен отправить e-mail уведомление.
Забегая вперед отмечу важный момент. Asterisk выполняет команды последовательно, дожидаясь их выполнения (что в общем-то логично). А вызывать bash-скрипт мы будем до того, как выполнится команда hangupcall. Таким образом в момент непосредственного выполнения скрипта, в CDR еще не будет внесена информация об искомом нами «uniqueid». Для решения этой проблемы bash-скрипт мы будем вызывать с параметром «&», чтобы Asterisk сразу перешел к выполнению следующего шага, т. е. hangupcall. А внутри bash-скрипта, в самом начале, мы установим небольшую задержку по времени, чтобы дать время для Asterisk внести данные с интересующим нас «uniqueid» в CDR.
Практика
Перед тем как перейти к настройке Asterisk и созданию bash-скрипта, нужно настроить отправку e-mail уведомлений. Для этого мы будем использовать утилиту postfix.
Настройка postfix
У нас есть почтовый домен «lucky.ru», расположенный в Яндексе. Мы настроим postfix в режим smtp-клиента и будем отправлять письма с аккаунта asterisk@lucky.ru.
За основу взято решение отсюда: https://www.dmosk.ru/miniinstruktions.php?mini=postfix-over-yandex.
Сначала установим/обновим/проверим наличие пакетов:
yum install postfix
yum install mailx
yum install cyrus-sasl cyrus-sasl-lib cyrus-sasl-plain
Не будем затирать основной файл конфигурации postfix «/etc/postfix/main.cf», а создадим его резервную копию:
cp /etc/postfix/main.cf /etc/postfix/main.cf.sav
Редактируем файл «/etc/postfix/main.cf» и приводим его к следующему виду:
nano /etc/postfix/main.cf
#####################
relayhost =
smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = hash:/etc/postfix/private/sasl_passwd
smtp_sasl_security_options = noanonymous
smtp_sasl_type = cyrus
smtp_sasl_mechanism_filter = login
smtp_sender_dependent_authentication = yes
sender_dependent_relayhost_maps = hash:/etc/postfix/private/sender_relay
smtp_generic_maps = hash:/etc/postfix/generic
smtp_tls_CAfile = /etc/postfix/ca.pem
smtp_use_tls = yes
smtputf8_autodetect_classes = all
#####################
Не каждую строку в «/etc/postfix/main.cf» можно комментировать. Комментарии в некоторых строках не определяются парсером и передаются в обработку, а это приводит к ошибкам. Лучше отказаться от комментариев внутри этого файла. Можете поэкспериментировать с этим запустив в соседнем окне «tail -f /var/log/messages».
Отмечу строку «smtputf8_autodetect_classes = all». Эта запись включает utf-8 по умолчанию, что позволяет использовать кириллицу и в теле письма, и в теме письма без дополнительных манипуляций (См. http://www.postfix.org/SMTPUTF8_README.html).
Создадим каталог для файлов конфигураций:
mkdir /etc/postfix/private
Редактируем файл «/etc/postfix/private/sender_relay». В нем нужно указать на какой smtp-сервер нужно ссылаться при использовании нашего почтового домена:
nano /etc/postfix/private/sender_relay
#####################
@lucky.ru smtp.yandex.ru
#####################
Редактируем файл «/etc/postfix/private/sasl_passwd». В нем мы укажем e-mail адрес, который мы будем использовать для отправки писем, а так же логин и пароль от этой учетной записи (логин и пароль указываем через двоеточие):
nano /etc/postfix/private/sasl_passwd
#####################
asterisk@lucky.ru asterisk@lucky.ru:password_asterisk
#####################
Редактируем файл «/etc/postfix/generic». В нем мы пропишем правила подмены исходящего адреса (см. https://wiki.merionet.ru/ip-telephoniya/30/postfix-nastrojka-otpravki-pochty-v-asterisk/):
nano /etc/postfix/generic
#####################
root asterisk@lucky.ru
root@localhost asterisk@lucky.ru
root@localhost.localdomain asterisk@lucky.ru
root@freepbx asterisk@lucky.ru
root@freepbx.localdomain asterisk@lucky.ru
root@asterisk asterisk@lucky.ru
root@asterisk.localdomain asterisk@lucky.ru
asterisk asterisk@lucky.ru
asterisk@localhost asterisk@lucky.ru
asterisk@localhost.localdomain asterisk@lucky.ru
asterisk@freepbx asterisk@lucky.ru
asterisk@freepbx.localdomain asterisk@lucky.ru
asterisk@asterisk asterisk@lucky.ru
asterisk@asterisk.localdomain asterisk@lucky.ru
root@localdomain.localdomain asterisk@lucky.ru
#####################
Изначальный исходящий адрес зависит от содержимого «/etc/hosts» и «/etc/hostname», а также от имени пользователя, который будет отправлять письмо. Т. е. не смотря на то, что мы используем smtp-клиент и отправляем письма от asterisk@lucky.ru, все равно в адрес отправителя postfix изначально подставит «что-то своё» и это нужно исправить правилами из этого файла конфигурации.
Приведу содержимое своего файла «/etc/hosts»:
cat /etc/hosts
#####################
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4 asterisk.localdomain
127.0.0.1 localhost.localdomain localhost
::1 asterisk localhost localhost6
#####################
Важно, чтобы сервер имел какой-либо домен (значение после точки), потому что утилита mail «ищет» имя домена в «/etc/hosts» и если «не находит» его сразу, то продолжит это делать в течение еще нескольких минут и только потом отправит письмо. Т. е. если домен не прописан, то письмо будет уходить с задержкой в несколько минут.
Приведу содержимое своего файла «/etc/hostname»:
cat /etc/hostname
#####################
asterisk
#####################
Далее необходимо перевести созданные файлы конфигурации в индексированные базы данных, для этого выполним следующую команду:
postmap /etc/postfix/generic && postmap /etc/postfix/private/{sasl_passwd,sender_relay}
Далее нам необходимо скачать и разместить на сервере сертификат smtp.yandex.ru, для этого выполним следующую команду:
openssl s_client -starttls smtp -crlf -connect smtp.yandex.ru:25 > /etc/postfix/ca.pem
Но после того, как на экран выйдет техническая информация команда будет «продолжать висеть». Нажмите Ctrl+C чтобы прервать её.
Теперь вручную удалим из получившегося файла весь мусор и оставим только сертификат. Должно получиться нечто подобное:
nano /etc/postfix/ca.pem
#####################
-----BEGIN CERTIFICATE-----
MIIGazCCBVOgAwIBAgIQcUU9mJXW4OUs5Gf0JfLtsjANBgkqhkiG9w0BAQsFADBf
...
nRG0DfdqYIuPGApFORYe
-----END CERTIFICATE-----
#####################
И наконец перезапустим postfix:
service postfix restart
Отправляем тестовое письмо:
echo "Это тело письма" | mail -s "Это тема" admin@lucky.ru
admin@lucky.ru — адрес назначения
На этом настройка posfix закончена.
Пишем bash-скрипт
Создаем директорию для хранения bash-скрипта (тут кому где больше нравится):
mkdir /home/asterisk/scripts
Создаем файл bash-скрипта:
touch /home/asterisk/scripts/noanswer.sh
Выдаем файлу скрипта права на выполнение:
chmod +x /home/asterisk/scripts/noanswer.sh
Если есть сомнения в правах на файл, то на время отладки можно дать полный доступ к файлу. Но это «не безопасно».
chmod 777 /home/asterisk/scripts/noanswer.sh
Текст bash-скрипта:
nano /home/asterisk/scripts/noanswer.sh
#####################
#!/bin/bash
sleep 7
res_sql="SELECT disposition FROM cdr WHERE uniqueid = '$1'"
answer=`mysql -u freepbxuser -pPassword_freepbxuser -D asteriskcdrdb -B -N -e "$res_sql" | grep -E -v "NO ANSWER|BUSY|FAILED|UNKNOWN" | head -n 1`
error_kod=0
if [ "$answer" != "ANSWERED" ]
then
case $3 in
68800)
address="big_boss@lucky.ru"
subject="по важному вопросу"
;;
63100)
address="debian@lucky.ru"
subject="по вопросам linux debian"
;;
63200)
address="windows@lucky.ru"
subject="по вопросам windows"
;;
63300)
address="freebsd@lucky.ru"
subject="по вопросам freebsd"
;;
63400)
address="ubuntu@lucky.ru"
subject="по вопросам linux ubuntu"
;;
63500)
address="centos@lucky.ru"
subject="по вопросам linux centos"
;;
*)
address="admin@lucky.ru"
error_kod=1
;;
esac
case $error_kod in
0)
echo "Пропущен вызов от абонента $2, звонившего $subject." | mail -s "Пропущен вызов от $2" $address
echo "Пропущен вызов для $address от абонента $2, звонившего $subject. uid=$1" | mail -s "Пропущен вызов от $2" admin@lucky.ru
;;
1)
echo "Пропущен вызов от $2. Очередь неизвестна. uid=$1" | mail -s "Пропущен вызов от $2" admin@lucky.ru
;;
esac
fi
#####################
Краткий разбор скрипта:
«sleep 7»:
Это та самая задержка по времени, о которой я писал ранее. У нас установлена задержка на 7 сек. Хотя, думаю, и одной секунды вполне хватит.
«res_sql="SELECT disposition FROM cdr WHERE uniqueid = '$1'"»:
Запрос в mysql мы вынесли в отдельную переменную для удобства.
Далее мы делаем запрос в mysql и фильтруем полученный вывод. Удаляем все варианты кроме «ANSWERED», если такой вообще есть. Если же значений «ANSWERED» несколько, то нужно оставить только одно. В конце в переменную «answer» мы получим либо «ANSWERED» либо «».
Если значение переменной «answer» не равно «ANSWERED», то это пропущенный вызов. В зависимости от номера очереди, с помощью оператора case мы зададим адрес, кому именно необходимо отправить e-mail уведомление, и что в этом сообщении написать (изменяемая часть сообщения).
Далее рассмотрен вариант, когда очередь задана в Asterisk, но не описана в скрипте. В этом случае admin@lucky.ru получит письмо, о том, что очередь не известна скрипту.
Если же очередь описана, то будет отправлено письмо по назначению и дублирующее письмо на admin@lucky.ru с указанием «uniqueid», для того, чтобы можно было отследить события по этому звонку, в случае необходимости.
На этом скрипт заканчивается.
Отмечу, что для подключения к mysql мы использовали логин и пароль, которые заранее узнали. Во FreePBX для того, чтобы узнать логин пользователя Asterisk в mysql выполните следующую команду:
cat /etc/amportal.conf | grep AMPDBUSER
А для того, чтобы узнать пароль пользователя Asterisk в mysql выполните следующую команду:
cat /etc/amportal.conf | grep AMPDBPASS
Настройка Asterisk
Мы используем FreePBX. Во FreePBX есть разные типы конфигурационных файлов (см. https://asterisk-pbx.ru/wiki/freepbx/files), некоторые из них FreePBX перезаписывает при перезагрузке, а некоторые не перезаписывает (их называют custom), так как они специально предназначены для пользователя.
Мы будем работать с файлом конфигурации «extensions_override_freepbx.conf», так как он относится к типу custom.
Для начала убедимся, что в файле «/etc/asterisk/extensions.conf» подключен файл «extensions_override_freepbx.conf». Для этого выполним следующую команду:
cat /etc/asterisk/extensions.conf | grep extensions_override_freepbx.conf
#####################
#include extensions_override_freepbx.conf
#####################
Редактируем файл «/etc/asterisk/extensions_override_freepbx.conf» и приведем его к следующему виду:
nano /etc/asterisk/extensions_override_freepbx.conf
#####################
[ext-queues]
exten => h,1,System(/home/asterisk/scripts/noanswer.sh ${CDR(uniqueid)} ${CALLERID(num)} ${NODEST} &)
exten => h,2,Macro(hangupcall,)
#####################
Как я и писал ранее символ «&» в конце обязателен. Так как мы будем работать в bash-скрипте с данными CDR непосредственно из базы mysql, а эти данные заносятся в mysql только после выполнения «exten => h,2,Macro(hangupcall,)», то необходимо не ждать окончания отработки bash-скрипта, а перейти к выполнению следующего шага в Asterisk. А сам bash-скрипт должен содержать задержку по времени, перед выполнением основной своей части.
Для того, чтобы изменения в конфигурационном файле «/etc/asterisk/extensions_override_freepbx.conf» вступили в силу необходимо перезагрузить ядро Asterisk следующей командой:
/usr/sbin/asterisk -rx "core restart now"
Это нужно сделать после того как bash-скрипт будет создан.
Заключение
Наверное, это 1001-й способ «отлова пропущенных вызовов» в Asterisk. Поделитесь в комментариях как эту задачу решаете вы. И что, по вашему мнению, можно доработать/переделать/оптимизировать. Будем признательны за конструктивные идеи.
Комментарии (11)
lryzhik
16.08.2019 00:48простите, а почему select | grep, а не добавить в where условие? думаю, что при наличии индекса на поле, будет быстрее. проверяли ли этот вариант?
vladimir1211 Автор
16.08.2019 05:07Согласен с вами полностью, просмотрели. Считаю, что так действительно будет быстрее:
res_sql="SELECT disposition FROM cdr WHERE uniqueid = '$1' AND disposition = 'ANSWERED'" answer=`mysql -u freepbxuser -pPassword_freepbxuser -D asteriskcdrdb -B -N -e "$res_sql" | head -n 1`
awsswa59
16.08.2019 08:54Не пользуйтесь postfix если вам надо просто отправлять почту… есть ssmtp
конфигурация из 5 строк.
И используйте mutt при отправке письма
awsswa59
16.08.2019 08:57Отлавливание пропущенных делается проще. Добавляете в Dial вызов gosub при ответе вызова.
В gosub ставите метку что вызов отвечен.
По hangupcause проверяете наличие метки.
Отправляете почту при отсутствии.
Очередь, не очередь, группа, номер… везде работает.
И не надо в mysql лезь и bash изобретать
Rsa97
16.08.2019 09:16А почему не сделать проще?
У команды QUEUE есть ключ 'c', при котором диалплан продолжается после того, как вызывающий положил трубку, и переменная ABANDONED, которая выставляется в TRUE, если никто из агентов не ответил на вызов.
same => n,Queue(callcenter,CcRt,,,,,,callcenter-queue-connected)
same => n,GotoIf($["${ABANDONED}"="TRUE"]?send-email)
NikolaBY
16.08.2019 10:04Очень сложное решение и непрактичное решение. Понятное дело, что нехватку практического опыта надо компенсировать и это похвально, решение все же работает :) Rsa97 выше предложил очень хорошее решение :) на практике мы что-то подобное используем уже довольно долго, и поток из тысяч звонков в день показывают его надежность и эффективность.
Ovoshlook
19.08.2019 05:58Откажитесь от вызова System. Любой внешний вызов через System/Exec и тд очень сильно грузит систему, что особенно заметно при хорошей нагрузке.
Если нужно выполнение внешнего скрипта в любой задаче — пишите нормальный демон скрипта и кидайте ему данные через curl, например, или слушайте астериск через AMI/ARI.
arheops
Не делайте так, используйте queue_log.
Также астериск вообще говоря не гарантирует запись CDR в таблицу до запуска h-exten. Вы вроде как решили это через sleep 7, НО астериск не гарантирует и за 7 секунд вообще говоря.
А еше на нагруженной системе скрипты с sleep 7 будут убиваться случайно.
Корректным решением был бы скрипт, который мониторит в mysql queue_logs и по записи ABANDON шлет сообщение.
volos4
а еще queue_log можно писать в mysql и там настроить триггер на ABANDON
arheops
Правильно, писать в mysql и потом писать тригер.
Тригеры mysql както странно работают на внешние скрипты. Я просто проверяю табличку.
vladimir1211 Автор
А зачем писать в mysql еще что-то, если имеющихся в нём данных и так достаточно для решения задачи?