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

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

Telegram боты — это мощный инструмент, благодаря которому можно реализовать множество систем, одной из которых является система уведомлений.

Всю разработку поделим на ряд этапов:

 

Формирование задачи

Необходимо создать форму для сбора почтовых ящиков. При отправке которой заполненные данные фиксируются в базе. В конце каждого дня производится подсчет количество 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.

Опубликовано:
Пожертвование
На развите проекта, продление хостинга и на корм бобику - Дикуше :)