- PVSM.RU - https://www.pvsm.ru -
Представляю вам вторую часть из серии статей по созданию своего шлюза.
В первой части мы настроили Gammu, рассмотрели особо интересные параметры и успешно произвели отправку SMS сообщения. Сейчас нам предстоит задача посложнее – создать некую программную прослойку (API), для того, чтобы можно было работать со шлюзом путем отправки запросов на этот API. В первую очередь это комфорт, во вторую – большое количество дополнительных возможностей.
Если вы не знакомы с первой частью, советую сначала ознакомиться с ней:
Свой личный SMS-шлюз. Часть 1 – цели, задачи, сборка и тестирование [1]
Первое, что нужно для себя понять, какими свойствами и возможностями должен обладать наш код, что мы хотим от него получить и как с ним взаимодействовать. Для этого я поставил для себя пару вопросов и постарался максимально на них ответить.
На чем будем писать backend
Тут все просто, что умеем, на том и пишем, поэтому в моем случае – PHP
Авторизация
Конечно. Сервис будет смотреть в интернет, поэтому авторизация обязательна.
Один пользователь – одна sim-карта?
Конечно нет. У нас сервис для личного пользования и мы хотим иметь один логин, но при этом отправлять с нескольких номеров. Но если появится необходимость выделить один шлюз под конкретный сервис, мы должны иметь возможность добавления пользователей.
Как мы хотим общаться с этим API, откуда будут попадать запросы
Общение будет через POST/GET. Запросы могут отправляться различными устройствами, в том числе и теми, которые не умеют POST или заморочно реализовать, поэтому принимать и обрабатывать будем $_REQUEST. Также мы хотим иметь возможность отправки сообщений через простую форму на сайте.
Один запрос – один адресат?
Нет. В одном запросе с одним текстовым сообщением должна быть возможность указать несколько адресатов. Суть этого понятна. Например я отслеживаю наличие ЭЭ на даче и в случае отключения хочу получить уведомление на все свои телефоны, а может даже телефон супруги… почему бы и нет, ведь уведомление важное.
История отправленных сообщений
Конечно, история это наша важная составляющая жизни, поэтому ее мы всегда храним
Балансировка нагрузки на карты
Да. Мы обладаем чувством меры и будем отправлять с одной карты не более какого-то числа сообщений, а значит их нужно считать и перед выбором шлюза проверять на исчерпание лимита.
Первое что мы сделаем, определим структуру база данных. Без нее, при наших потребностях никак. Использовать будем MySQL.
Дальше нужно будет написать пару классов, к которым мы были обращаться.
Приступим к созданию БД и создание таблиц
Я буду использовать несколько таблиц для:
# Дамп таблицы users
# ------------------------------------------------------------
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`uuid` varchar(10) NOT NULL,
`login` varchar(50) NOT NULL,
`password` varchar(32) NOT NULL,
`comment` varchar(200) NOT NULL,
`status` varchar(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
# Дамп таблицы smsc_gateway
# ------------------------------------------------------------
CREATE TABLE `smsc_gateway` (
`id` int(20) NOT NULL AUTO_INCREMENT,
`gw_phone` varchar(11) NOT NULL DEFAULT '',
`uuid` varchar(10) NOT NULL,
`host` varchar(15) NOT NULL DEFAULT '',
`port` varchar(5) NOT NULL,
`password` varchar(12) DEFAULT '',
`maxcount` varchar(6) NOT NULL,
`status` int(1) NOT NULL,
`gateway_id` int(2) DEFAULT NULL,
`state` varchar(11) DEFAULT NULL,
`comment` varchar(100) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
# Дамп таблицы gateway_smscount
# ------------------------------------------------------------
CREATE TABLE `gateway_smscount` (
`id` int(20) NOT NULL AUTO_INCREMENT,
`gw_phone` varchar(11) NOT NULL DEFAULT '',
`date` varchar(10) NOT NULL,
`count` int(6) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
# Дамп таблицы sms_queue
# ------------------------------------------------------------
CREATE TABLE `sms_queue` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`uuid` varchar(11) NOT NULL DEFAULT '',
`dateTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`status` varchar(10) DEFAULT NULL,
`data` varchar(500) NOT NULL DEFAULT '',
`phone` varchar(11) NOT NULL DEFAULT '',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
# Дамп таблицы sms_archive
# ------------------------------------------------------------
CREATE TABLE `sms_archive` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`gateway_id` varchar(11) DEFAULT NULL,
`dateTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`data` varchar(500) DEFAULT NULL,
`status` varchar(11) DEFAULT NULL,
`phone` varchar(11) DEFAULT NULL,
`uuid` varchar(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Стоит подробнее остановиться на таблице с данными шлюза и используемых полей – smsc_gateway. Здесь мы используем:
Классов будет всего 5:
Дальше я буду объяснять как поток данных будет ходить по API опираясь на базу данных. Мне кажется так нагляднее и понятнее. Также я приведу куски этого кода под спойлерами.
В итоге мы получаем такую последовательность действий.
Пользователь отправляет запрос с параметрами:
.../smsc.php?login={user_name}&pass={user_password}&tel={phone_number}&msg={message}&flash=1&replacemessages_id=1
Значения flash и replacemessages мы рассматривали в прошлой статье. В {phone_number} можно указать несколько номеров телефонов через ",". + в номере телефона указывать не нужно, но обязательно указывать номер в международном формате (для России это 7…). Так же в стоку можно добавить еще один параметр – &attempts={число} – количество попыток внутри одной отправки, то есть, если можем при отправке вернул ошибку, пытаться ли отправить тут же еще раз?
Вот, что происходит под капотом smsc.php
<?php
require_once __DIR__.'/functions/config.php';
XSS_secure();
if (!Users_Auth::do($PDO)) http_response::return(401, ["description" => "User not found or login / password is incorrect"]);
$sms_handle = new SMS_data_handle($PDO);
$sms_handle->save();
function XSS_secure() {
function replace($arr) {
$filter = array("<", ">");
$filter_replace = array("<", ">");
for ($i=0; $i < count($filter) ; $i++) {
$str = str_replace($filter[$i], $filter_replace[$i], $arr);
}
return $str;
}
if ($_GET) $_GET = replace($_GET);
if ($_POST) $_POST = replace($_POST);
}
?>
Первым делом мы подключаем файл с настройками – config.php:
require_once __DIR__.'/functions/config.php';
Содержание файла:
<?php
// ini_set('error_reporting', E_ALL);
// ini_set('display_errors', 1);
// ini_set('display_startup_errors', 1);
spl_autoload_register(function ($class_name) {
require_once "classes/{$class_name}.class.php";
});
$PDO_param = [
'db_host' => '__', // database hostname or ip
'db_name' => '__', // database name
'db_user' => '__', // database username
'db_pass' => '__', // databse user password
'db_charset' => 'utf8',
'pdo_opt' => [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_LAZY,
PDO::ATTR_EMULATE_PREPARES => false,
]
];
$PDO = new MYSQL_PDO($PDO_param);
?>
делаем небольшую проверку на XSS, а далее проверяем авторизацию, вызывая метод класса Users_Auth::do($PDO):
class Users_Auth
{
static function do($PDO)
{
if (!@$_REQUEST['login'] || !@$_REQUEST['pass']) http_response::return(403, ['description' => 'Check your login and password']);
// Проверяем авторизацию
$user = $PDO->query("SELECT id, status FROM users WHERE login= ? AND password= ?", [$_REQUEST['login'], md5(trim($_REQUEST['pass']))]);
if ($user->rowCount() == 0)
return 0;
// http_response::return(401, ["description" => "User not found. Login and password is incorrect"]);
$row = $user->fetch();
if ($row->status != 'active')
return 0;
// http_response::return(401, ["description" => "User status: {$row->status}"]);
return 1;
}
}
Если получили false – авторизация не удалась, возвращаем код и описание ошибки в json, если необходимо.
Если авторизация успешная вызываем $sms_handle->save(), проверяем переданы ли обязательные параметры – телефон и текст сообщения, проверяем в БД статус пользователя, разбираем строку запроса и приводим в нужный нам вид, удаляем пробелы и "+" из номера телефона, а также разделяем их по запятой. Таким образом получаем массив номеров телефонов и текста сообщения, которое нужно на них отправить. Делаем из этого json и сохраняем в таблицу очереди на отправку. Проверка на наличие телефона обязательна. Если попытаться отправлять сообщения без указания номера телефона, возникнет ошибка в Gammu, и шлюз будет занят на несколько секунд. Когда шлюз освободится возникнет аналогичная повторная ситуация, что в свою очередь создаст так называемую «пробку» и последующие сообщения из очереди просто не смогут уйти.
public function save() {
if (empty($_REQUEST['tel'])) http_response::return(400, ["success" => false, "description" => "Phone number is empty"]);
if (empty($_REQUEST['msg'])) http_response::return(400, ["success" => false, "description" => "Message is empty"]);
$msg = $_REQUEST['msg'];
$search = array(' ', '+');
$replace = array('', '');
$phone_array = explode(",", str_replace($search, $replace, $_REQUEST['tel']));
$query = $this->PDO->query("SELECT uuid FROM users WHERE login = ?", [$_REQUEST['login']]);
$user_uuid = $query->fetch();
$data = [
'message' => $msg,
'flash' => @$_REQUEST['flash'],
'replacemessages_id' => @$_REQUEST['replacemessages_id'],
'attempts' => @$_REQUEST['attempts']
];
$data['attempts'] = (@$_REQUEST['attempts']) ?: 1;
foreach ($phone_array as $phone) {
$data['phone'] = $phone;
$this->PDO->query("INSERT INTO sms_queue SET uuid= ?, data= ?, phone= ?", [$user_uuid->uuid, json_encode($data, JSON_UNESCAPED_UNICODE), $phone]);
}
http_response::return(200, ["success" => true, "description" => "Saved to queue"]);
}
Дальше мы используем простой скрипт, который поставим в cron и будем вызывать раз в 5-10 секунд (по вкусу) – send_queue.php
<?php
require_once __DIR__.'/config.php';
$sms_handle = new SMS_data_handle($PDO);
$sms_handle->send();
/*
add follow lines to cron – crontab -e
don't forget to replace <full_path_to> to your really path
* * * * * ( php /<full_path_to>/send_queue.php )
* * * * * ( sleep 10 ; php /<full_path_to>/send_queue.php )
* * * * * ( sleep 20 ; php /<full_path_to>/send_queue.php )
* * * * * ( sleep 30 ; php /<full_path_to>/send_queue.php )
* * * * * ( sleep 40 ; php /<full_path_to>/send_queue.php )
* * * * * ( sleep 50 ; php /<full_path_to>/send_queue.php )
*/
?>
Он будет обращаться к методу класса обработчика сообщений SMS_data_handle->send(). Здесь уже начинается самое интересное.
Мы получаем сообщение за последние 10 минут без тегов статуса. Если нашли такое, ставим на него тег — process и берём в работу.
Извлекаем из тела json uuid пользователя, обращаемся к таблице и получаем список активных шлюзов. Идем в таблицу со счётчиком и проверяем, не превышен ли лимит на отправку. Если мы получили активный шлюз и счётчик не превышен, ставим на него тег — lock, чтобы никакой другой процесс уже не смог параллельно к нему обратиться. Все вызовы происходит внутри метода send(), но логика раскидана по другим методам класса. По указанному выше описанию работы метода эти обращения легко видны.
Далее мы создаем объект класса $send_proc = new Gammu_SMS($param) с параметрами и обращаемся к методу $send_proc->send($attr) с атрибутами
Весь код метода send():
public function send($с = 1) {
$sended_sms = 0;
for ($i = 0; $i < $с; $i++) {
$sms_queue = $this->get_sms_queue(1);
if (!$sms_queue->rowCount()) http_response::return(200, ["description" => "Nothing to do. Sent. count: {$sended_sms}"]);
$sms_count = $sms_queue->rowCount();
$msg_row = $sms_queue->fetch();
$this->PDO->query("UPDATE sms_queue SET status = ? WHERE id = ?", ["process", $msg_row->id]);
$user_gateway = ($this->get_gateway($msg_row->uuid));
if (!$user_gateway) {
$this->PDO->query("UPDATE sms_queue SET status = NULL WHERE id= ?", [$msg_row->id]);
http_response::return(403, ["description" => "Not active gateways or get limit of message count"]);
}
$this->gateway_lock($user_gateway->id);
$param = [
'host' => $user_gateway->host,
'port' => $user_gateway->port,
'login' => 'root',
'password' => $user_gateway->password,
];
$sms_data = json_decode($msg_row->data);
$sms_data->message = $sms_data->message;
$attr = [
'phone' => $sms_data->phone,
'message' => $sms_data->message,
'attempts' => $sms_data->attempts,
'flash' => $sms_data->flash,
'replacemessages_id' => $sms_data->replacemessages_id,
'gateway' => $user_gateway->id,
];
// sleep(5);
$send_proc = new Gammu_SMS($param);
if ($send_proc->send($attr)) {
$this->sms_2archive($msg_row->id, $user_gateway->id);
$this->update_gwcount($user_gateway->gw_phone);
$sended_sms++;
} else {
$this->PDO->query("UPDATE sms_queue SET status = NULL WHERE id= ?", [$msg_row->id]);
}
$this->gateway_release($user_gateway->id);
}
http_response::return(200, ["success" => true, "description" => "Message sent. Count: {$sended_sms}"]);
}
Если объект вернул true, то переносим сообщение в архив и увеличиваем счётчик отправленных сообщений. Иначе снимаем тег proccess и через некоторое время будет повторная попытка отправки по cron.
Особо внимательные заметили, что мы вызываем метод с дефолтным параметром равным одному – send($с = 1). Параметр $c заложен «на перспективу» и позволяет нам, в случае необходимости получать пачку сообщений из базы данных и обрабатывать их отправку в цикле. Для этого в файле, вызываемом в cron нужно в вызове метода указать число сообщений для выборки их БД – $sms_handle->send({число});
Обратим внимание еще на один момент. В файле smsc.php есть возможность отправлять сообщения сразу после того, как оно было добавлено в БД. Для этого нужно раскомментировать следующую строку:
// $sms_handle->send();
Это позволит нам отказаться от cron, но есть один нюанс – желательно использовать этот метод, если вы отправляете сообщения только на один номер и запросы к шлюзу не могут быть чаще чем раз в 30 секунд. Иначе возможны ошибки связанные с наложением запросов и если шлюз занят, то сообщение не отправится.
Теперь наш шлюз работает через API и умеет отправлять сообщения.
Ну и бонусом мы сделаем простую форму для отправки сообщений с сайта. Ее код не нуждается в пояснении, она просто принимает от вас тест и отправляет POST-запрос на указанный нами скрипт. Единственное в блоке отправки ajax нужно заменить url: "/<*.php>" на адрес вашего скрипта smsc.php
Итак подведем итоги проделанной работы. Мы создали аппаратную платформу, научились отправлять сообщения через терминал и расширили возможности системы собственным API для легкого доступа к шлюзу устройств способных отправлять GET/POST-запросы. Хранить историю и балансировать нагрузку между картами и прочее. Все это сильно упрощает работу со шлюзом и позволяет хранить все в одном сервисе.
Внимание, я не претендую на великолепную красоту кода и буду рад любой объективной критике для понимания своих ошибок (в случае наличия) и совершенствования навыков.
Репозиторий данного проекта на Github [2]
Автор: Дмитрий
Источник [3]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/blog-kompanii-ruvds-com/363858
Ссылки в тексте:
[1] Свой личный SMS-шлюз. Часть 1 – цели, задачи, сборка и тестирование: https://habr.com/ru/company/ruvds/blog/554868/
[2] Github: https://github.com/dagababaev/smsgateway_API_via_Gammu
[3] Источник: https://habr.com/ru/post/555422/?utm_source=habrahabr&utm_medium=rss&utm_campaign=555422
Нажмите здесь для печати.