Уведомления через бота Telegram

Telegram боты — это мощный инструмент, благодаря которому можно реализовать множество систем, одной из которых является система уведомлений.
Всю разработку поделим на ряд этапов:
- Формирование задачи
- Проектирование базы данных (БД)
- Верстка (front end)
- Добавление логики (back end)
- Автоматизация (cron)
- Итоги
Формирование задачи
Необходимо создать форму для сбора почтовых ящиков. При отправке которой заполненные данные фиксируются в базе. В конце каждого дня производится подсчет количество e-mail и производится отправка уведомления сотрудникам компании.
Проектирование базы данных (БД)
В роли базы данных будем использовать MySQL в следствии ее популярности.
Для начала нам необходимо создать таблицу, где будут храниться непосредственно собранные e-mail письма:
CREATE TABLE `emails` (
`email_id` int(11) NOT NULL,
`email_value` varchar(255) CHARACTER SET utf8 DEFAULT NULL,
`email_send` tinyint(1) NOT NULL DEFAULT '1',
`email_date` timestamp NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Далее необходимо создать индексы и уникальный первичный ключ, для более быстрого взаимодействия с данными:
ALTER TABLE `emails`
ADD PRIMARY KEY (`email_id`),
ADD UNIQUE KEY `email_value` (`email_value`),
ADD KEY `email_send` (`email_send`),
ADD KEY `email_date` (`email_date`);
ALTER TABLE `emails`
MODIFY `email_id` int(11) NOT NULL AUTO_INCREMENT;
После данных команд будет создана необходимая таблица, в которой:
- «email_id» — автоматически генерироваться уникальный индефикатор записи
- «email_value» — сам электронный почтовый ящик
- «email_send» — заблокирован ли почтовый ящик
- «email_date» — временная метка добавления записи
Верстка (front end)
Не будем тратить время на разработку дизайна и красивую верстку, в данной статье не она является целью. Копируем html5 структуру в файл index.php
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Пример Telegram уведомлений</title>
</head>
<body>
<form method="post">
<fieldset>
<legend>Подпишитесь на новости</legend>
<p><label for="email">E-mail</label><input type="email" id="email" placeholder="info@tarsy.club"></p> </fieldset>
<p><button type="submit">Отправить</button></p>
</fieldset>
</form>
</body>
</html>
Добавление логики (back end)
Данным этапом нам необходимо связать нашу форму и базу данных, что бы при ее отправке сохранялись все данные и выводилось оповещение об успешной или не успешной отправке.
Поэтому после элемента «legend» добавляем сестринский php код с проверкой для вывода оповещения
<?=(isset($message) and $message)?"<p>$message</p>":"";?>
А в самом начале начале файла, перед «<!DOCTYPE html>« добавим php код с подключением файла, где будет лежать вся логика
<?php
include_once "controller.php";
?>
И не забудем создать сам файл controller.php, лежащий на одном уровне с index.php со следующим содержимым:
<?php
//переменная уведомления
$message = null;
//функция проверка валидности
function validEmail($email){
return preg_match( "/^(?:[a-z0-9]+(?:[-_.]?[a-z0-9]+)?@[a-z0-9_.-]+(?:\.?[a-z0-9]+)?\.[a-z]{2,5})$/i", $email );
}
//была ли отправлена форма
if( isset($_POST['email']) ){
//проверяем валидность пришедшего email
if ( validEmail( $_POST['email'] ) ) {//email указан верно
//подключаем файл конфигураций
include_once "config.php";
//подключаем файл для работы с БД
include_once "db.php";
//инициализируем базу данных
DB::getInstance( $db ?? array() );
//добавляем новый элемент в таблицу
$count = DB::insert("INSERT INTO `emails` (`email_value`) VALUES (?)", [
$_POST['email']
]);
//проверяем добавлена ли запись
$message = $count ? "Email адрес успешно добавлен" : "Email адрес не добавлен, повторите попытку позднее.";
}else{
//выводим сообщение об не корректном email
$message = "Email адрес указан не правильно.";
}
}
В коде идет подключение двух файлов:
- config.php — файл всех конфигураций по проекту
- db.php — файл (класс) для работы с БД
Содержимое конфигурационного файла:
<?php
//конфиг БД
$db = [
'hostname' => 'localhost', // расположение БД
'database' => 'tutorial', //имя БД
'username' => 'usr', //имя пользователя
'password' => 'pass', // пароль пользователя
'dbcollat' => 'utf8', // кодировка
];
И содержимое файла для работы с БД:
<?php
interface DBInterface{
//старт файла
public function __construct();
//запрещаем клонирование объекта
public function __clone();
//запрещаем восстановление
public function __wakeup();
//инициализация обьекта
public static function getInstance( array $config = array() );
//запрос с возвратом ответа
public static function select(string $query, array $val = null): array;
//добавление материала
public static function insert(string $query, array $val = null): int;
//обновление материала
public static function update(string $query, array $val = null): int;
//удаление материала
public static function delete(string $query, array $val = null): int;
//удаление материала
public static function error(): string;
//Закрытие соединения
public function __destruct();
}
class DB implements DBInterface{
//сам бьект
protected static $_instance = [
'pdo' => null,
'error' => null,
'config' => null,
];
//старт файла
public function __construct(){}
//запрещаем клонирование объекта
public function __clone() {}
//запрещаем восстановление
public function __wakeup() {}
public static function getInstance( array $config = array() ) {
$error = (!isset(
$config['hostname'],
$config['database'],
$config['username'],
$config['password'],
$config['dbcollat'])
) ? "Error connection to database." : false;
//инициалезируем обьект
if (self::$_instance === null) self::$_instance = new self;
self::$_instance['error'] = $error;
self::$_instance['config'] = $config;
//проверка ошибок
if( !self::$_instance['error'] ){
//подключение к БД
try{
//драйвер подключения
$dbconn = "mysql:host=$config[hostname];dbname=$config[database]";
//процесс подключения
self::$_instance['pdo'] = new PDO( $dbconn, $config['username'], $config['password'] );
//установка параметров
self::$_instance['pdo']->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
//переводим кодировку
self::$_instance['pdo']->exec("SET NAMES '$config[dbcollat]'");
}catch(PDOException $e){ //вывод ошибки подключения
self::$_instance['error'] = $e->getMessage();
}
}
//возвращаем подключение
return self::$_instance;
}
//запрос с возвратом ответа
private static function query(string $query, array $val): PDOStatement {
$stmt = null;
try{
//проверка наличия ошибок
if(self::$_instance['error']){
return null;
}
//проверка передаваемых параметров
if(!$val){
$stmt = self::$_instance['pdo']->query($query);
$stmt->execute();
}else{
$stmt = self::$_instance['pdo']->prepare($query);
$stmt->execute($val);
}
}catch(PDOException $e){ }
//возврат результата
return $stmt;
}
//запрос с возвратом ответа
public static function select(string $query = '', array $val = null): array{
//отправка результата
$query = self::query($query, $val);
//проверка ответа
if( $query === null ){
return array();
}
//возврат результата
return (isset($query->errorInfo()[0]) and $query->errorInfo()[0] == 00000) ?
$query->fetchAll(PDO::FETCH_OBJ) : array();
}
//добавление материала
public static function insert(string $query = '', array $val = null): int {
//отправка результата
$query = self::query($query, $val);
//проверка ответа
if( $query === null ){
return 0;
}
//возврат результата
return self::$_instance['pdo']->lastInsertId();
}
//обновление материала
public static function update(string $query = '', array $val = null): int {
//отправка результата
$query = self::query($query, $val);
//проверка ответа
if( $query === null ){
return 0;
}
//возврат результата
return (isset($query->errorInfo()[0]) and $query->errorInfo()[0] == 00000)?
$query->rowCount() : 0;
}
//удаление материала
public static function delete(string $query = '', array $val = null): int {
//отправка результата
$query = self::query($query, $val);
//проверка ответа
if( $query === null ){
return 0;
}
//возврат результата
return (isset($query->errorInfo()[0]) and $query->errorInfo()[0] == 00000)?
$query->rowCount() : 0;
}
//удаление материала
public static function error(): string {
return self::$_instance['error'] ?? '';
}
//Закрытие соединения
public function __destruct(){
self::$_instance['pdo'] = null;
}
}
Теперь наша база данных получает из формы email подписавшихся, остается только создать сам процесс уведомлений по телеграмму для сотрудников.
Автоматизация (cron)
Автоматизация будет заключаться в том, что по крону будет запускаться каждый день в 09:00 php скрипт, который будет считывать все новый почтовые ящики за последние сутки, подсчитывать их количество и отправлять по телеграмму сотрудникам.
Что бы процесс подсчета и отправки был более функциональный добавим возможность запускать данный скрипт из консоли (терминала) и передавать напрямую параметры для отправки.
А так же в зависимости от региона из-за блокировок популярного месаджера есть необходимость добавить proxy соединениях к Telegram. Поэтому в файл «config.php» добавим следующий массив данных:
//конфиг telegram
$tlgm = [
'token' => '123456:QWER1234QWER1234', // ключ для бота
'mask' => 'C %date% поступило %count% новых подписок.', // маска сообщения в Telegram
'chats' => '123123', // чат Telegram куда прийдет отчет
'proxy' => null, // сервер для отправки через прокси (socks5) - '127.0.0.1:8888'
'auth' => null, // аккаунт для подключения к прокси серверу - 'user:pass'
];
Основная же логика запускаемого файла будет располагаться в файле «cron.php»:
<?php
//подключаем файл обработки передаваемых параметров
include_once "opt.php";
//подключаем файл конфигураций
include_once "config.php";
//проверка данных
if( !isset($tlgm) ){
echo "Конфигурационный файл заполнен не корректно
";
die;
}
//получаем чат telegram для отправки. в параметрах передаем значения по умолчанию
list(
$chats, //чат куда отсылать сообщение
$date, // дата начала проверки
$echo, //необходимо ли выводить системные сообщения
$tlgm, //необходимо ли выводить json ответа telegram
$nosql, //автономная обработка, без подключения к базе
$mess, //маска сообщения
) = my_getopt($tlgm['chats'] ?? array() );
//проверка пришедших параметров
if( empty($chats) ){
echo "Чаты куда паресылать сообщения не найдены
";
die;
}
//проверка пришедших параметров
if( !isset($tlgm['token']) ){
echo "Токен бота для отправки не задан
";
die;
}
$token = $tlgm['token'];
//замена маски
$mess = $mess ? $mess : $tlgm['mask'];
//проверка необходимости подключения
if($nosql) {
//подключам файл работы с БД
include_once "db.php";
//инициализируем базу данных
DB::getInstance($db ?? array());
//получаем количество подписавшихся
$count = DB::select("SELECT COUNT(`email_id`) AS `count` FROM `emails` WHERE `email_date` >= ? ", [
$date
]);
//проверка данных
$count = empty($count) ? 0 : $count[0]->count;
}else{
//подставляем количество
$count = 0;
}
//подставляем дату и количество в маску
$mess = str_replace( ['%date%', '%count%'], [$date, $count], $mess );
//выводим ответ если необходимо
echo $echo ? $mess.'
': '';
//преобразуем данные для отправки в телеграм
$mess = urlencode($mess);
//подключаем файл отправки данных по curl
include_once "curl.php";
//если несколько получателей
$res = [];
if(is_array($chats)){
for ($i=0; isset($chats[$i]); $i++) {
$res[] = my_file_get_contents( "https://api.telegram.org/bot{$token}/sendMessage?chat_id=".$chats[$i]."&parse_mode=html&text=$mess", $tlgm['proxy'] ?? null, $tlgm['auth'] ?? null );
}
}else{
$res[] = my_file_get_contents( "https://api.telegram.org/bot{$token}/sendMessage?chat_id={$chats}&parse_mode=html&text=$mess", $tlgm['proxy'] ?? null, $tlgm['auth'] ?? null );
}
//вывод json от telegram
if( $tlgm ){
echo '['.implode(',', $res).']';
}
В данном файле идет дополнительные подключения:
- opt.php — файл (функция) обработки данных посылаемые через консоль
- config.php — файл всех конфигураций по проекту
- db.php — файл (класс) для работы с БД
- cron.php — файл (функция) для отправки данных методом cURL
содержимое файла «opt.php»:
<?php
//формируем параметры
function my_getopt($defChat = null, $defDid = null){
//параметры
$params = array(
"b" => "bags", // без аргументов
"t" => "tlgm", // без аргументов
"e" => "echo", // без аргументов
"n" => "nsql", // без аргументов
"c::" => "chats::", // Необязательное значение
"m::" => "mess::", // Необязательное значение
"d::" => "date::", // Необязательное значение
"h" => "help", // без аргументов
);
//парсим
$options = getopt( implode('', array_keys($params)), $params);
//проверка режима вывода ошибок (дебаг режим)
if( isset($options['b']) or isset($options['bags']) ){
error_reporting(E_ALL);
ini_set("display_errors", 1);
}else{
error_reporting(0);
ini_set("display_errors", 0);
}
//выводит в консоле сообщение или нет
$echo = ( isset($options['e']) or isset($options['echo']) ) ? true : false;
//выводит в консоле сообщение или нет
$tlgm = ( isset($options['t']) or isset($options['tlgm']) ) ? true : false;
//автономный режим
$nosql = ( isset($options['n']) or isset($options['nsql']) ) ? false : true;
//маска сообщения
$mess = $options['mess'] ?? $options['m'] ?? null;
//маска сообщения
$date = $options['date'] ?? $options['d'] ?? (new DateTime())->modify('-1 day')->format('Y-m-d 00:00:00');
//вывод помощи
if( isset($options['help']) or isset($options['h']) ){
echo "
Исполнение:
\033[1mphp index.php [-h|--help] [-m|--mess=message] [-c|--chats=chats_id] [-d|--date=date] [-e|--echo] [-t|--tlgm] [-n|--nsql] [-b|--bags]\033[0m
Опции:
\033[1m-h --help\033[0m вывод подсказок
\033[1m-m --mess\033[0m маска сообщения, по умолчанию: 'За %date% поступило %count% новых подписок.'
\033[1m-c --chats\033[0m список id чатов в телеграмме через пробел
\033[1m-c --date\033[0m дата начала подсчета данных в фомрате 'Y-m-d H:i:s'
\033[1m-e --echo\033[0m выводить ли в консоле текстовый результат
\033[1m-t --tlgm\033[0m выводить ли ответ телеграм сервера
\033[1m-n --nsql\033[0m автономный режим, отключается от базы и вставляет тестовый результат
\033[1m-b --bags\033[0m режим включения дебага
Пример:
php index.php --chats=\"1234 4321\" --dids=\"1234 4321\" --mess=\"Дополнительное сообщение\"
";
die; //выход из скрипта
}
//собираем идентификаторы чатов;
$TEMP = array();
//получаем данные
$chats = $options['chats'] ?? $options['c'] ?? $defChat ?? null;
//преобразуем в массив данных
$chats = $chats ? explode(' ', $chats) : array();
//проходимся и обрабатываем данные
foreach ($chats as $chat) {
//убираем лишние символы
if( $chat = trim(strip_tags($chat)) and $chat ){
$TEMP[] = $chat;
}
}
//обновляем данные
$chats = $TEMP;
//вывод поисковых значенией
if($echo){
echo "\033[1mChats для рассылки:\033[0m
[ ".implode(', ', $chats)." ]
";
}
//вывод
return [$chats, $date, $echo, $tlgm, $nosql, $mess];
}
содержимое файла «cron.php»:
<?php
//отправка get запроса
function my_file_get_contents(string $url = '', string $proxy = null, string $proxyauth = null): string {
//возвращаемые данные
$data = null;
try{
$ch = curl_init();
curl_setopt_array($ch,[
CURLOPT_URL => $url, //url отправки данных
CURLOPT_ENCODING => "UTF-8", //кодировка данных
CURLOPT_HEADER => false, //не возвращать заголовки
CURLOPT_RETURNTRANSFER => true, //необрабатывать ответ
CURLOPT_AUTOREFERER => true, //автоматическая установка поля Referer
CURLOPT_FOLLOWLOCATION => true, //автопереходы при редиректах
CURLOPT_USERAGENT => 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.0.3705; .NET CLR 1.1.4322)', //передаем клиента
CURLOPT_TIMEOUT => 15, //время ожидание в секундах
CURLOPT_CONNECTTIMEOUT => 5, //время конекта в секундах
]);
//проверка параметров для прокси соединения
if($proxy){
//прокси сервер
curl_setopt($ch, CURLOPT_PROXY, $proxy);
//используемый драйвер
curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5_HOSTNAME);
//необходимо ли авторизироваться
if($proxyauth){
curl_setopt($ch, CURLOPT_PROXYUSERPWD, $proxyauth);
}
}
//получам данные
$data = curl_exec($ch);
//проверка на ошибку
$data = $data ? $data : curl_error($ch);
//закрываем соединение
curl_close($ch);
} catch (\Exception $e) {
$data = null;
}
return $data;
}
Итоги
Проект закончен, теперь для того, что бы подписаться и записать свой email в базу данных необходимо открыть в браузере «index.php» и в форме заполнить необходимое поле, после чего отправить его. В результате чего будет проверенна корректность введенного адреса и записана она в БД.
Для отправки уведомлений на телеграм необходимо добавить в cron одноименный файл «cron.php» на 9 утра, к примеру, и дождаться запуска, в результате чего в telegram чат сотрудникам или канал для публикации будет отправлено сообщение с подсчетом количество новых подписок.
Так же есть возможность отправлять в любое время с любыми параметрами сразу из консоли (терминала) сервера командой, для того что бы узнать какие возможно параметры передавать необходимо вбить «php <dir>/curl.php -h», где <dir> — путь до директории где хранятся файлы.
Для того, что бы скачать и протестировать проект перейдите по ссылке на github.