Тема о сообщениях (аля SMS) в Астериске не первая на Хабре, но у всех публикаций есть один недостаток — они не обладают функционалом отложенной доставки сообщений. Когда получатель не в сети, вы получаете об этом сообщение при попытке отправки ему message, и предложение попробовать позднее.
Непорядок!
Работать будем с asterisk 11, с установленным FreePBX. Традиционно «без конфигов» в этот раз не получится.
Итак, разрешаем работу messages и указываем контекст обработки оных, в разделе вебморды Settings > Asterisk SIP Settings. В самом низу добавляем кастомные поля для sip.conf и указываем:
Создаем в extensions_custom.conf этот контекст:
В этом контексте присутствует вызов ODBC-функции, которая сохраняет «SMS-ку» в СУБД MySQL. Чтобы не морочится с отдельными базами и DSN, я создал таблицу в имеющейся базе asteriskcdrdb:
В файле func_odbc.conf добавим саму ODBC_функцию:
Как видим, текст сообщения перед сохранением кодируется в base_64. Таким нехитрым образом я обхожу глюки с кириллицей. Кстати, передачу текста в контексте messages обязательно заключать в кавычки, иначе при появлении например запятой в тексте диалпан считает это разделителем параметров :)
Итак, сообщения у нас сохраняются в базу при отсутствии абонента в сети. Осталось настроить механизм доставки ему сего сообщения. Делать будем на php, скрипт я положил в /etc/asterisk/send_delayes_messages.php:
В качестве метки для факта доставки я использую поле delivered типа timestamp, если там нули — то сообщение нуждается в доставке. Таким образом, пробегая по сохраненным недоставленным сообщениям, мы проверям по каждому наличие регистрации пира через команду cli, и если он в сети — создаем outgoing call file, который и производит доставку сего сообщения. После этого скрипт помечает в базе сообщение, устанавливая дату отправки.
Останется прикрутить скрипт через php -f /etc/asterisk/send_delayes_messages.php в поминутный крон и раз в минуту будет производится проверка и попытка доставки сообщения.
Какие минусы у этой реализации? Первый — регистрация статуса пира держится какое то время после обрыва, и вполне возможна ситуация, когда пир кратковременно зарегистрируется и отвалится, а система «отправит» в течение минуты ему сообщение, и будет считать его доставленным. Выкрутится можно, использовав не Application в call-файле, а передачу данных в контекст с проверкой статуса переменной ${MESSAGE_SEND_STATUS}. Наверное, возможно будет использовать имеющийся контекст, задав переменные через Set в call-файле.
Но я пока остановился на этом: некогда.
Удачи!
Непорядок!
Работать будем с 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)
whoim
27.02.2017 21:49Спасибо, поразбираюсь.
${MESSAGE_SEND_STATUS} достаточен, чтобы быть уверенным, что пакет попал на нужный адрес и пришло подтверждение, по моим тестам.
Если резко потерять клиента и тут же попытаться отправить сообщение, статус будет неудачный, хотя в пирах он ещё висит как ОК.willyd
28.02.2017 13:47${MESSAGE_SEND_STATUS} достаточен, чтобы быть уверенным, что пакет попал на нужный адрес и пришло подтверждение, по моим тестам.
Я не пробовал из плана набора. Но попробуйте отправить на несуществующий пир. По исходникам я не вижу откуда взятся разнице в работе AMI action (action_messagesend) и приложения (ast_msg_send).
Возможно, что в моих тестах я не так указывал uri, но не думаю…
main/message.cstatic 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; }
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; }
willyd
Наверное будет как-то так.
${MESSAGE_SEND_STATUS} подтверждает только отправку. https://wiki.asterisk.org/wiki/display/AST/Asterisk+13+Application_MessageSend
Получаем статус пира и uri (Status и RegContact):
Можно перед этим отправить qualify запрос. Чтобы быть уверенным в статусе.
Отправляем сообщение.
https://wiki.asterisk.org/wiki/display/AST/Asterisk+13+ManagerAction_MessageSend
Статус доставки не отдается. Проверяется только отправка. Как собственно и приложение MessageSend, которое вы используете в плане набора. Либо делайте проверку статуса девайса в диалплане.