В предыдущей статье я описал как настроил и собрал GSM <> SIP систему на базе Asterisk. В этой статье расскажу как быть с входящими SMS, если получатель не в сети (не прошел регистрацию на PBX).
Проблема
Если посмотреть предыдушую статью, видно, что SMS (MESSAGE(body)) преобразуется из BASE64 в plane text на системе с модемом. Это остаток от дебага, когда нужно было видеть что пришло на модем в консоли Asterisk. Я не стал менять этого поведения, так как у меня сохраняется проблема составных SMS (когда сообщение разбито на несколько частей). С ней я буду разбераться позже.
Далее MESSAGE(body) передается уже в виде SIP Message на центральную PBX, где обрабатывется согласно dial plan.
И тут возникала проблема — если получатель сообщения (он же extension) был в момент его прихода не в сети — сообщение безвозвратно теряется.
Я считаю что GSM у нас есть всегда, по этому не стал заморачиваться с исходящими сообщениями.
Задача
Сегодня поговорим как создать очередь SIP сообщений, обеспечить их хранение и повторную доставку.
Решение
У Asterisk есть приложение очереди (message queue). Но само по себе это приложение не может писать Message(body) куда либо «на потом», так как во первой нужно для роутинга голосовых вызовов, а так же доставляемых сообщений (из серии — у нас поток, а приемник медленный или занят).
Придется использовать костыль — систему внешних скриптов, которые будут создавать файлы очереди и помещать их в специальную дирректорию, откуда Asterisk их будет читать. Задача состоит в том, чтоб помещать недоставленные сообщения в call file, который будет вгружаться в очередь, и повторять попытку доставки несколько (N) раз через определенные (T) промежутки времени.
Я рекомендую обращаться к справкам по всем упоминаемым мной фишкам Asterisk’а. Читая описание синтаксиса очень часто понятно где что не сработало.
Оба действия делаются двумя приложениями: app_system и pbx_spool. Первое — за вызовы скриптов, второе — за обработку call file.
Нужно проверять несколько условий:
-
Зарегистрирован ли целевой получатель на сервере.
-
Удалось ли доставить сообщение.
-
Сообщение которое мы пытаемся доставить пришло из очереди, или новое.
Так же важно помнить, что так как мы используем скрипт, ему придется передавать аргументы, а по сути — вызывать строчку в shell, что несет серьезные риски. По этому придется опять преобразовать MESSAGE(body) в BASE64 и расшифровывать его обратно перед доставкой в SIP клиент.
Не забываем подгрузить func_base64.
Диалплан
Ну что, поехали создавать? Все что мы делаем — делаем на центральной PBX.
[incoming-sms] exten = _1.,1,Verbose(1, "Incoming SMS from ${CALLERID(num)} GSM Gateway to ${EXTEN}") same = n,NoOp(To ${MESSAGE(to)}) same = n,NoOp(From ${MESSAGE(from)}) same = n,NoOp(Body ${MESSAGE(body)}) same = n,MessageSend(${MESSAGE(to)},${MESSAGE(from)}) same = n,NoOp(Send status is ${MESSAGE_SEND_STATUS}) ;same = n,GotoIf($["${MESSAGE_SEND_STATUS}" != "SUCCESS"]?handlefailedmsg) same = n,GotoIf($["${MESSAGE_SEND_STATUS}" != "SUCCESS"]?chktq) same = n,Hangup() ; Notify sender that message was not delivered ;same = n(handlefailedmsg),NoOp(Sending error back to user) ;same = n,Set(SRC=${MESSAGE(from)}) ;same = n,Set(DST=${MESSAGE(to)}) ;same = n,Set(MSG=${MESSAGE(body)}) ;same = n,Set(MESSAGE(body)="[${STRFTIME(${EPOCH},,%d%m%Y-%H:%M:%S)}] Your message to ${EXTEN} has failed. Sending when available") ;same = n,ExecIf($["${CUT(MESSAGE(from),<,2)}" != "" ]?Set(ME_1=${CUT(MESSAGE(from),<,2)}):Set(ME_1=${MESSAGE(from)})) ;same = n,Set(ACTUALFROM=${ME_1}) ;same = n,MessageSend(${ACTUALFROM},ServiceCenter) ;same = n,GotoIf($["${INQUEUE}" != "1"]?startq) ;same = n,Hangup() ; Check that we are not in queue same = n(chktq),GotoIf($["${INQUEUE}" != 1 ]?startq) same = n,Hangup() ; Starting Queue for messages same = n(startq),NoOp(Queueing message for offline) same = n,Set(MSGTIME=${STRFTIME(${EPOCH},,%d%m%Y-%H:%M:%S)}) same = n,Set(SRC="${BASE64_ENCODE(${MESSAGE(from)})}") same = n,Set(DST=${BASE64_ENCODE(${MESSAGE(to)}}) same = n,Set(MSG=${MESSAGE(body)}) same = n,Set(DUMP_MSG="${MSGTIME} ${MSG}") ; We do not want our BASH to execute strange things... so BASE64 encoding same = n,Set(MSG="${BASE64_ENCODE(${DUMP_MSG})}") same = n,System(/var/lib/asterisk/agi-bin/astqueue.sh -SRC ${SRC} -DST ${DST} -MSG ${MSG}) same = n,Hangup() [app-fakeanswer] exten = _1.,1,Verbose(1, "Processing offline message queue for ${EXTEN}") same = n,Set(DESTDEV=${EXTEN}) same = n,Set(THISDEVSTATE=${DEVICE_STATE(PJSIP/${DESTDEV})}) same = n,GotoIf($["${THISDEVSTATE}" = "UNAVAILABLE"]?hang) same = n,GotoIf($["${THISDEVSTATE}" = "UNKNOWN"]?hang) same = n,Answer same = n,Hangup same = n(hang),Hangup()
Давайте разберем по блокам.
Вход в очередь
Начало стандартное, там ничего интересного, пока мы не попадаем к строкам
;same = n,GotoIf($["${MESSAGE_SEND_STATUS}" != "SUCCESS"]?handlefailedmsg) same = n,GotoIf($["${MESSAGE_SEND_STATUS}" != "SUCCESS"]?chktq)
Мы проверяем смогли ли мы доставить сообщение. У него есть статус и мы будем продолжать если он не в значении SUCCESS.
Первая строка (я не использую) выкидывает нас в блок отправки уведомления отправителю. Так как никакого реального механизма уведомить о судьбе сообщения нет (у нас SMS доставилось на модем), нужно ручками описать процедуру формирования и отправки такого уведомления. Весь блок строк 11-21 я не сильно проверял. Что-то там будет работать, но это здесь чисто на будущее, если я решу это использовать.
Проверка источника сообщения
Второй блок проверяет, откуда это сообщение в контексте и направляет в соответвующие обработчики
; Check that we are not in queue same = n(chktq),GotoIf($["${INQUEUE}" != 1 ]?startq) same = n,Hangup()
Вторая строка — отсылка к проверке не пришло ли сообщение из очереди. Нужно понимать, что нам делать с шифрованием BASE64, так как сообщения из очереди у нас зашифрованы (будет видно ниже)
Т.е. если переменная INQUEU не в состоянии 1, считаем что сообщение пришло к нам из предыдущего блока, т.е. было передано оконечным PBX, но не доставлено адресату. И да, эта переменная появится значительно позже и будет прочитана из call файла, а установлена — скрпитом.
Создание очереди
; Starting Queue for messages same = n(startq),NoOp(Queueing message for offline) same = n,Set(MSGTIME=${STRFTIME(${EPOCH},,%d%m%Y-%H:%M:%S)}) same = n,Set(SRC="${BASE64_ENCODE(${MESSAGE(from)})}") same = n,Set(DST=${BASE64_ENCODE(${MESSAGE(to)}}) same = n,Set(MSG=${MESSAGE(body)}) same = n,Set(DUMP_MSG="${MSGTIME} ${MSG}")
Второй строкой мы генерируем уникальный идентификатор для последующиего использования в сообщении. См строку четыре.
Третьей строкой шифруем получателя. Shell обычно не любит всякие там + и < и мы все это превращаем в безобидную абракадабру для дельнейшего использования.
Четвертой строкой мы формируем текст, который будет помещен в очередь. Суть проста — когда мы получим сообщение из очереди, в нем будет указана дата-время когда оно в очередь поместилось и сам текст. В простивном случае мы просто получили бы текст сообщения без возможности узнать когда же мы его пропустили.
Запрос скрипта и помещение в очередь
; We do not want our BASH to execute strange things... so BASE64 encoding same = n,Set(MSG="${BASE64_ENCODE(${DUMP_MSG})}") same = n,System(/var/lib/asterisk/agi-bin/astqueue.sh -SRC ${SRC} -DST ${DST} -MSG ${MSG}) same = n,Hangup()
Следующий блок у нас кодирует всю строку сообщения (DATE+Message(body)) вместе в BASE64 для получения единой строки.
А далее мы вызываем внешний скрипт управления, которому в качестве аргументов передаем источник, получателя и текст сообщения.
Все, или нет?
Нет. Пока я не показал вам сам скрипт, не ясно зачем у нас есть новый контекст app-fakeanswer
[app-fakeanswer] exten = _1.,1,Verbose(1, "Processing offline message queue for ${EXTEN}") same = n,Set(DESTDEV=${EXTEN}) same = n,Set(THISDEVSTATE=${DEVICE_STATE(PJSIP/${DESTDEV})}) same = n,GotoIf($["${THISDEVSTATE}" = "UNAVAILABLE"]?hang) same = n,GotoIf($["${THISDEVSTATE}" = "UNKNOWN"]?hang) same = n,Answer same = n,Hangup same = n(hang),Hangup()
Ответ до безобразия прост. Когда сообщение будет обрабатываться message_queue оно попадает в этот контекст и идет проверка на доступность получателя (extension). Если получатель не зарегистрирован на сервере, мы выходим, если зарегистрирован — передаем сообщение обратно в обработку в соответствующий контекст incoming-sms
Скрипт создания call file
И так, теперь самое интересное.
Нам нужно воспользоваться так называемыми AGI приложениями, а по сути — bash скриптом, в котором мы пережуем аргументы, проверим есть ли у нас уже сообщение для этого получателя и создадим call file , который потом положим в специальную дирректорию, из которой его будет вычитывать Asterisk.
Оба скрипта закидываем в /var/lib/asterisk/agi-bin/ или туда, где у вас расположена agi директория, а так же не забываем придать им +x (право исполнения) и владельца из-под которого у вас работает Asterisk (chmod & chown).
Я модифицировал найденный в сети скрипт несколько адаптировав его под свои пути и исправив пару косяков.
#!/bin/bash ############################################################################## # v0.2 # # copyleft Sanjay Willie sanjayws@gmail.com # # SCRIPT PURPOSE: GENERATE SMS OFFLINE QUEUE # # GEN INFO: Change variables sections # ############################################################################## # This script was edit by Michael A. Gates # # because it didn't work in freepbx 5.11 # # I am by no means a Linux guy or a Asterisk # # guy. Without Sanjay Willie's work I could # # not have done this. # # # #Contact:michael.allen.gates@gmail.com # #added message ordering from # #http://www.irishvoip.com/w/knowledgebase.php?action=displayarticle&id=13 # ############################################################################## #VARIABLES maxretry=10000#Number of Atempts for sending the sms retryint=60#Number of Seconds between Retries #CONSTANTS ERRORCODE=0 d_unique=`date +%s` d_friendly=`date +%T_%D` astbin=`which asterisk` myrandom=$[ ( $RANDOM % 1000 ) + 1 ] # function bail() { echo "SMS:[$ERRORCODE] $MSGOUT. Runtime:$d_friendly. UniqueCode:$d_unique" exit $ERRORCODE } function gencallfile(){ filename=$1 destexten=$2 source=$3 dest=$4 message=$5 mydate=`date +%d%m%y` logdate=`date` #dest=echo $dest | grep -d # echo -e "Channel: Local/$destexten@app-fakeanswer CallerID: $source Maxretries: $maxretry RetryTime: $retryint Context: incoming-sms Extension: $destexten Priority: 1 Set: MESSAGE(body)=$message Set: MESSAGE(to)=$dest Set: MESSAGE(from)=$source Set: INQUEUE=1 "> /var/spool/asterisk/tmp/$filename # move files chmod 777 /var/spool/asterisk/tmp/$filename sleep 3 # # Check to see if there is already a message for this extension queued # if so then move to the hold folder and let the cron job astcron.sh check for delivery of the queued message # and only then deliver the hold messages. This will make sure the messages are delivered in order # ifexist=`ls /var/spool/asterisk/outgoing/|grep call | grep -c $destexten` if [[ "$ifexist" == "0" ]]; then # # move file to outgoing folder # mv /var/spool/asterisk/tmp/$filename /var/spool/asterisk/outgoing/ #echo "moved" else # # move file to hold folder # mv /var/spool/asterisk/tmp/$filename /var/spool/asterisk/hold/ #echo "holded" fi # #exit $ERRORCODE bail } while test -n "$1"; do case "$1" in -SRC) source="$2" echo $source shift ;; -DST) dest="$2" echo $dest shift ;; -MSG) message="$2" echo $message shift ;; -TIME) originaltime="$2" echo $originaltime shift ;; esac shift done # decoding BASE64 source=`echo $source | base64 -d` dest=`echo $dest | base64 -d` message=`echo $message | base64 -d` originaltime=`echo $originaltime | base64 -d` #[checking for appropriate arguments] if [[ "$source" == "" ]]; then echo "ERROR: No source. Quitting." ERRORCODE=1 bail fi if [[ "$dest" == "" ]]; then echo "ERROR: No usable destination. Quitting." ERRORCODE=1 bail fi if [[ "$message" == "" ]]; then echo "ERROR: No message specified.Quitting." ERRORCODE=1 bail fi #[End Argument checking] # Check to see if extension exist destexten=`echo $dest | cut -d\@ -f1 | cut -d\: -f2` ifexist=`$astbin -rx "pjsip show endpoints" | grep -c $destexten` if [[ "$ifexist" == "0" ]]; then echo "Destination extension don't exist, exiting.." ERRORCODE=1 baduser=$destexten destexten=`echo $source | cut -d\@ -f1 | cut -d\: -f2` temp=$source source=$dest dest=$temp message="The user $baduser does not exist, please try your message again using a different recipient.:(" filename="$destexten-$d_unique.$myrandom.NoSuchUser.call" gencallfile "$filename" "$destexten" "$source" "$dest" "$message" bail fi #End of Check # If that conditions pass, then we will queue, # you can write other conditions too to keep the sanity of the looping destexten=`echo $dest | cut -d\@ -f1 | cut -d\: -f2` filename="$destexten-$d_unique.$myrandom.call" gencallfile "$filename" "$destexten" "$source" "$dest" "$message" bail
Что тут интересного:
-
Строки 20 и 21: задают количество повторов и время между попытками.
-
Строка 50: контекст куда выкидывать сообщения.
-
Строка 56: это установка статуса очереди (переменная INQUEUE), а так же мы указываем путь к временной директории, доступной Asterisk’у для создания временного файла.
-
Строки 113-116: декодируют все аргументы.
-
Строка 138: проверяет а есть ли вообще получатель и если нет, выходит с сообщением об ошибке. Сообщение теряется. Именно тут была основная закоыврка, так как нужно верно вызвать комманду в Asterisk согласно вашему конкретному случаю.
-
Строка 159 и далее: формируют файл, приделывают ему псевдо-случайное название (для правильного порядка в очереди и избегания перезаписи).
А как быть с порядком доставки?
А очень просто. Есть еще один скрпит, который проверяет поступающие call file’ы и распределяет их по порядку для доставки.
#!/bin/bash # This file should go in /var/lib/asterisk/agi-bin # make sure you change your permissions of astcron.sh to 775 #Change to hold directory # cd /var/spool/asterisk/hold # # Check through all queued files in sort order (so we always get the oldest first) # for filename in `ls -v *.call`; do destexten=`echo $filename | cut -d - -f1 ` echo "Checking extension" $destexten "for file" $filename "for existing messages" ifexist=`ls /var/spool/asterisk/outgoing/| grep call | grep -c $destexten` # # if extension doesnt exist then queued message has been delivered # so we can move the waiting message now # if [[ "$ifexist" == "0" ]]; then echo "No existing message for " $destexten # # move file to outgoing folder # echo "Moving filename" $filename "to outgoing" mv /var/spool/asterisk/hold/$filename /var/spool/asterisk/outgoing/ # # If we actually do a move then delay just in case # there is more than one waiting message for that extension # sleep 3 fi done
Тут нужно не забыть создать еще одну дирркеторию hold, в которой будут храниться call фвйлы до момнета когда их можно перемещать на доставку в папку outgoing.
Опять же не забывайте редактировать пути согласно вашим реалиям.
И последним, добавляем в crontab следующую строчку
* * * * * cronic /var/lib/asterisk/agi-bin/astcron.sh /dev/null 2>&1 || true
Эта штука дергает ежесекундно скрипт проверки и организации очереди.
На закуску
Сохранив ваш extensions.conf идем в астериск и делаем dialplan reload.
Отключаем клиент или sip-телефон от сети и шлам сообщение. Подлкючаемся — вуаля (если все сделано верно).
P.S. пока что в очереди застреают многосегментные сообщения, как я уже говорил — это на следующий раз.
Комментарии и замечания очень приветствуются.
ссылка на оригинал статьи https://habr.com/ru/post/679582/
Добавить комментарий