sip messages: отложенная доставка

в 10:22, , рубрики: asterisk, message, php, sms

Тема о сообщениях (аля SMS) в Астериске не первая на Хабре, но у всех публикаций есть один недостаток — они не обладают функционалом отложенной доставки сообщений. Когда получатель не в сети, вы получаете об этом сообщение при попытке отправки ему message, и предложение попробовать позднее.

image
Непорядок!

Работать будем с asterisk 11, с установленным FreePBX. Традиционно «без конфигов» в этот раз не получится :(

Итак, разрешаем работу messages и указываем контекст обработки оных, в разделе вебморды Settings -> Asterisk SIP Settings. В самом низу добавляем кастомные поля для sip.conf и указываем:
accept_outofcall_message = yes
outofcall_message_context = messages
auth_message_requests = no

Создаем в extensions_custom.conf этот контекст:
[messages]
exten => _.,1,Set(MSG_TO=${CUT(MESSAGE(to),@,1)})
exten => _.,n,MessageSend(${MSG_TO},${MESSAGE(from)})
exten => _.,n,GotoIf($["${MESSAGE_SEND_STATUS}" != "SUCCESS"]?sendfailedmsg)
exten => _.,n,Hangup()
exten => _.,n(sendfailedmsg),Set(MSG_TMP=${CUT(MESSAGE(from),<,2)})
exten => _.,n,Set(MSG_FROM=${CUT(MSG_TMP,@,1)})
exten => _.,n,Set(ODBC_SAVE_MESSAGE("${MESSAGE(from)}","${MSG_TO}","${MESSAGE(body)}")=1)
exten => _.,n,Set(MESSAGE(body)="[${STRFTIME(${EPOCH},,%d%m%Y-%H:%M:%S)}] Ваше сообщение для ${EXTEN} не доставлено. Оно будет доставлено, когда абонент появится в сети.")
exten => _.,n,MessageSend(${MSG_FROM}, SYSTEM)
exten => _.,n,Hangup()

В этом контексте присутствует вызов ODBC-функции, которая сохраняет «SMS-ку» в СУБД MySQL. Чтобы не морочится с отдельными базами и DSN, я создал таблицу в имеющейся базе asteriskcdrdb:

CREATE TABLE IF NOT EXISTS `messages` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `dt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `mfrom` varchar(100) CHARACTER SET utf8 NOT NULL,
  `mto` varchar(100) CHARACTER SET utf8 NOT NULL,
  `mbody` text CHARACTER SET utf8 NOT NULL,
  `delivered` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
  PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ;

В файле func_odbc.conf добавим саму ODBC_функцию:
[SAVE_MESSAGE]
writesql = INSERT INTO messages (mfrom,mto,mbody) VALUES ('${ARG1}','${ARG2}','${BASE64_ENCODE(${ARG3})}')
dsn = asteriskcdrdb

Как видим, текст сообщения перед сохранением кодируется в base_64. Таким нехитрым образом я обхожу глюки с кириллицей. Кстати, передачу текста в контексте messages обязательно заключать в кавычки, иначе при появлении например запятой в тексте диалпан считает это разделителем параметров :)

Итак, сообщения у нас сохраняются в базу при отсутствии абонента в сети. Осталось настроить механизм доставки ему сего сообщения. Делать будем на php, скрипт я положил в /etc/asterisk/send_delayes_messages.php:

<?php
#asteriskcdrdb database
#mysql settings
$hostname = "localhost"; 
$username = "asteriskuser"; 
$password = "password"; 
$dbName = "asteriskcdrdb";

mysql_connect($hostname,$username,$password) or die("no connect to MySQL.");
mysql_select_db($dbName) or die("ERROR: ".mysql_error());
mysql_query("set names 'utf8'");

$messages_query = mysql_query('SELECT `id`,`mfrom`,`mto`,`mbody` FROM `messages` WHERE `delivered` = "0000-00-00 00:00:00" ORDER BY `dt`') or die("ERROR: ".mysql_error());
while($message = mysql_fetch_array($messages_query, MYSQL_ASSOC)) {
  $peer_to = explode(":", $message['mto']);
  if (peer_online($peer_to[1])) {
    //print_r($message);
    file_put_contents('/tmp/delayed_message_'.$peer_to[1].'_'.time(), 'Channel: Local/s@default'."rn".'Application: MessageSend'."rn".'Set: MESSAGE(body)='.base64_decode($message['mbody'])."rn".'Data: '.$message['mto'].','.$message['mfrom']."rn");
    exec("mv /tmp/delayed_message_* /var/spool/asterisk/outgoing/");
    mysql_query('UPDATE `messages` SET `delivered` = "'.date("Y-m-d H:i:s").'" WHERE `id` = '.$message['id']." LIMIT 1") or die("ERROR: ".mysql_error());
  }
}

function peer_online($peer) {
  $raw = shell_exec('asterisk -rx "sip show peer '.$peer.'" | grep "Status" | grep "OK"');
  print $raw;
  if(!empty($raw)) return true; else return false;
}
?>

В качестве метки для факта доставки я использую поле delivered типа timestamp, если там нули — то сообщение нуждается в доставке. Таким образом, пробегая по сохраненным недоставленным сообщениям, мы проверям по каждому наличие регистрации пира через команду cli, и если он в сети — создаем outgoing call file, который и производит доставку сего сообщения. После этого скрипт помечает в базе сообщение, устанавливая дату отправки.

Останется прикрутить скрипт через php -f /etc/asterisk/send_delayes_messages.php в поминутный крон и раз в минуту будет производится проверка и попытка доставки сообщения.

Какие минусы у этой реализации? Первый — регистрация статуса пира держится какое то время после обрыва, и вполне возможна ситуация, когда пир кратковременно зарегистрируется и отвалится, а система «отправит» в течение минуты ему сообщение, и будет считать его доставленным. Выкрутится можно, использовав не Application в call-файле, а передачу данных в контекст с проверкой статуса переменной ${MESSAGE_SEND_STATUS}. Наверное, возможно будет использовать имеющийся контекст, задав переменные через Set в call-файле.
Но я пока остановился на этом: некогда.

Удачи!

Автор: whoim

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js