Тема о сообщениях (аля 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'."\r\n".'Application: MessageSend'."\r\n".'Set: MESSAGE(body)='.base64_decode($message['mbody'])."\r\n".'Data: '.$message['mto'].','.$message['mfrom']."\r\n");
    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-файле.
Но я пока остановился на этом: некогда.

Удачи!
Поделиться с друзьями
-->

Комментарии (7)


  1. willyd
    27.02.2017 21:44

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

    Наверное будет как-то так.

    ${MESSAGE_SEND_STATUS} подтверждает только отправку. https://wiki.asterisk.org/wiki/display/AST/Asterisk+13+Application_MessageSend

    Получаем статус пира и uri (Status и RegContact):
    Action: SIPshowpeer
    Peer: 3030
    
    Response: Success
    Channeltype: SIP
    ObjectName: 3030
    ChanObjectType: peer
    Codecs: (ulaw|alaw|gsm|h263)
    Status: OK (19 ms)
    SIP-Useragent: Yealink SIP-T27P 45.80.0.60
    Reg-Contact: sip:3030@192.168.100.101:5060
    QualifyFreq: 60000 ms
    SIP-Use-Reason-Header: N
    Description: 
    

    Можно перед этим отправить qualify запрос. Чтобы быть уверенным в статусе.

    Отправляем сообщение.
    action: messagesend
    to: sip:3030@192.168.100.101:5060
    from: sip:3030@192.168.100.101:5060
    body: abra cadabra
    
    Response: Success
    Message: Message successfully sent
    
    

    https://wiki.asterisk.org/wiki/display/AST/Asterisk+13+ManagerAction_MessageSend

    Статус доставки не отдается. Проверяется только отправка. Как собственно и приложение MessageSend, которое вы используете в плане набора. Либо делайте проверку статуса девайса в диалплане.


  1. Veleses
    27.02.2017 21:44

    Использовать mysql_connect в 2017, как-то не актуально. Почему не PDO?


    1. whoim
      27.02.2017 21:52

      Привычка, лет 15 пишу так. Даст ли pdo прирост в скорости и насколько примерно, если да?


  1. whoim
    27.02.2017 21:49

    Спасибо, поразбираюсь.
    ${MESSAGE_SEND_STATUS} достаточен, чтобы быть уверенным, что пакет попал на нужный адрес и пришло подтверждение, по моим тестам.
    Если резко потерять клиента и тут же попытаться отправить сообщение, статус будет неудачный, хотя в пирах он ещё висит как ОК.


    1. willyd
      28.02.2017 13:47

      ${MESSAGE_SEND_STATUS} достаточен, чтобы быть уверенным, что пакет попал на нужный адрес и пришло подтверждение, по моим тестам.

      Я не пробовал из плана набора. Но попробуйте отправить на несуществующий пир. По исходникам я не вижу откуда взятся разнице в работе AMI action (action_messagesend) и приложения (ast_msg_send).
      Возможно, что в моих тестах я не так указывал uri, но не думаю…

      main/message.c
      static int action_messagesend(struct mansession *s, const struct message *m)
      {
      	const char *to = ast_strdupa(astman_get_header(m, "To"));
      	const char *from = astman_get_header(m, "From");
      	const char *body = astman_get_header(m, "Body");
      	const char *base64body = astman_get_header(m, "Base64Body");
      	char base64decoded[1301] = { 0, };
      	char *tech_name = NULL;
      	struct ast_variable *vars = NULL;
      	struct ast_variable *data = NULL;
      	const struct ast_msg_tech *msg_tech;
      	struct ast_msg *msg;
      	int res = -1;
      
      	if (ast_strlen_zero(to)) {
      		astman_send_error(s, m, "No 'To' address specified.");
      		return 0;
      	}
      
      	if (!ast_strlen_zero(base64body)) {
      		ast_base64decode((unsigned char *) base64decoded, base64body, sizeof(base64decoded) - 1);
      		body = base64decoded;
      	}
      
      	tech_name = ast_strdupa(to);
      	tech_name = strsep(&tech_name, ":");
      
      	ast_rwlock_rdlock(&msg_techs_lock);
      	msg_tech = msg_find_by_tech_name(tech_name);
      	if (!msg_tech) {
      		ast_rwlock_unlock(&msg_techs_lock);
      		astman_send_error(s, m, "Message technology not found.");
      		return 0;
      	}
      
      	if (!(msg = ast_msg_alloc())) {
      		ast_rwlock_unlock(&msg_techs_lock);
      		astman_send_error(s, m, "Internal failure\n");
      		return 0;
      	}
      
      	data = astman_get_variables_order(m, ORDER_NATURAL);
      	for (vars = data; vars; vars = vars->next) {
      		ast_msg_set_var_outbound(msg, vars->name, vars->value);
      	}
      
      	ast_msg_set_body(msg, "%s", body);
      
      	res = msg_tech->msg_send(msg, S_OR(to, ""), S_OR(from, ""));
      
      	ast_rwlock_unlock(&msg_techs_lock);
      
      	ast_variables_destroy(vars);
      	ao2_ref(msg, -1);
      
      	if (res) {
      		astman_send_error(s, m, "Message failed to send.");
      	} else {
      		astman_send_ack(s, m, "Message successfully sent");
      	}
      	return 0;
      }
      
      int ast_msg_send(struct ast_msg *msg, const char *to, const char *from)
      {
      	char *tech_name = NULL;
      	const struct ast_msg_tech *msg_tech;
      	int res = -1;
      
      	if (ast_strlen_zero(to)) {
      		ao2_ref(msg, -1);
      		return -1;
      	}
      
      	tech_name = ast_strdupa(to);
      	tech_name = strsep(&tech_name, ":");
      
      	ast_rwlock_rdlock(&msg_techs_lock);
      	msg_tech = msg_find_by_tech_name(tech_name);
      
      	if (!msg_tech) {
      		ast_log(LOG_ERROR, "Unknown message tech: %s\n", tech_name);
      		ast_rwlock_unlock(&msg_techs_lock);
      		return -1;
      	}
      
      	res = msg_tech->msg_send(msg, S_OR(to, ""), S_OR(from, ""));
      
      	ast_rwlock_unlock(&msg_techs_lock);
      
      	ao2_ref(msg, -1);
      
      	return res;
      }


      1. whoim
        01.03.2017 00:36

        Будет время — попробую, конечно. Надо бы уже довести до ума решение.


    1. willyd
      28.02.2017 13:54

      Код приложения.

      MessageSend
      /*!
       * \internal
       * \brief MessageSend() application
       */
      static int msg_send_exec(struct ast_channel *chan, const char *data)
      {
      	struct ast_datastore *ds;
      	struct ast_msg *msg;
      	char *tech_name;
      	const struct ast_msg_tech *msg_tech;
      	char *parse;
      	int res = -1;
      	AST_DECLARE_APP_ARGS(args,
      		AST_APP_ARG(to);
      		AST_APP_ARG(from);
      	);
      
      	if (ast_strlen_zero(data)) {
      		ast_log(LOG_WARNING, "An argument is required to MessageSend()\n");
      		pbx_builtin_setvar_helper(chan, "MESSAGE_SEND_STATUS", "INVALID_URI");
      		return 0;
      	}
      
      	parse = ast_strdupa(data);
      	AST_STANDARD_APP_ARGS(args, parse);
      
      	if (ast_strlen_zero(args.to)) {
      		ast_log(LOG_WARNING, "A 'to' URI is required for MessageSend()\n");
      		pbx_builtin_setvar_helper(chan, "MESSAGE_SEND_STATUS", "INVALID_URI");
      		return 0;
      	}
      
      	ast_channel_lock(chan);
      
      	if (!(ds = ast_channel_datastore_find(chan, &msg_datastore, NULL))) {
      		ast_channel_unlock(chan);
      		ast_log(LOG_WARNING, "No message data found on channel to send.\n");
      		pbx_builtin_setvar_helper(chan, "MESSAGE_SEND_STATUS", "FAILURE");
      		return 0;
      	}
      
      	msg = ds->data;
      	ao2_ref(msg, +1);
      	ast_channel_unlock(chan);
      
      	tech_name = ast_strdupa(args.to);
      	tech_name = strsep(&tech_name, ":");
      
      	ast_rwlock_rdlock(&msg_techs_lock);
      	msg_tech = msg_find_by_tech_name(tech_name);
      
      	if (!msg_tech) {
      		ast_log(LOG_WARNING, "No message technology '%s' found.\n", tech_name);
      		pbx_builtin_setvar_helper(chan, "MESSAGE_SEND_STATUS", "INVALID_PROTOCOL");
      		goto exit_cleanup;
      	}
      
      	/*
      	 * The message lock is held here to safely allow the technology
      	 * implementation to access the message fields without worrying
      	 * that they could change.
      	 */
      	ao2_lock(msg);
      	res = msg_tech->msg_send(msg, S_OR(args.to, ""), S_OR(args.from, ""));
      	ao2_unlock(msg);
      
      	pbx_builtin_setvar_helper(chan, "MESSAGE_SEND_STATUS", res ? "FAILURE" : "SUCCESS");
      
      exit_cleanup:
      	ast_rwlock_unlock(&msg_techs_lock);
      	ao2_ref(msg, -1);
      
      	return 0;
      }