Как стать автором
Обновить

Asterisk. Доставка SIP Message после возврата абонента из off-line'а

Время на прочтение9 мин
Количество просмотров3K

В предыдущей статье я описал как настроил и собрал 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.

Нужно проверять несколько условий:

  1. Зарегистрирован ли целевой получатель на сервере.

  2. Удалось ли доставить сообщение.

  3. Сообщение которое мы пытаемся доставить пришло из очереди, или новое.

Так же важно помнить, что так как мы используем скрипт, ему придется передавать аргументы, а по сути - вызывать строчку в 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. пока что в очереди застреают многосегментные сообщения, как я уже говорил - это на следующий раз.

Комментарии и замечания очень приветствуются.

Теги:
Хабы:
+5
Комментарии7

Публикации

Истории

Работа

Ближайшие события