Собираем базу пользователей (подписчиков) Телеграм бота + мультиязычный интерфейс

Сбор и хранение данных о подписчиках Телеграм бота организовать очень просто. Рассмотрим инструменты для ведения такого рода базы, а также настройки мультиязычности интерфейса бота.

Собрать базу подписчиков бота не сложно, потому что при "старте" бота вместе с запросом приходят нужные данные. Нам необходимо только организовать сбор этих данных. Они нам могу пригодиться, например для хранения настроек пользователя, или при формировании рассылки информационных сообщений. Без самостоятельного ведения базы или использования сторонних ресурсов получить список тех, кто начал диалог с вашим ботом невозможно, то есть Телеграм вам такой информации нигде не выведет, по крайней мере на момент написания статьи, я такого функционала в мессенджере не встречал.

Для примера сделаем возможность пользователю выбирать язык интерфейса (мультиязычность), и соответствующую настройку будем хранить вместе с данными пользователя.

* * *

Как организовать сбор данных?

Для начала создадим таблицу users в базе MySQL (используйте кодировку utf8mb4_unicode_ci)

CREATE TABLE IF NOT EXISTS `users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `telegram_id` bigint(20) DEFAULT NULL,
  `first_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `last_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `username` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `lang` varchar(2) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Класс User по работе с таблицей, к нему мы будем обращаться для создания, получения или обновления данных о пользователях.  

<?php
// Подключаем класс для работы с БД
require_once("Db.php");

/**
 * Class User
 * @property int $id
 * @property int $telegram_id
 * @property string $first_name
 * @property string $last_name
 * @property string $username
 * @property string $lang
 * @property User|bool $identity
 */
class User extends Db
{
    private $id;
    private $telegram_id;
    private $first_name;
    private $last_name;
    private $username;
    private $lang;

    private $identity;

    /**
     * User constructor.
     * @param $userData array
     */
    public function __construct($userData)
    {
        // проверяем наличие пользователя в базе
        $this->identity = $this->getUserByTelegramId($userData['telegram_id']);
        if ($this->identity) {
            // если есть то отправляем на обновление данных
            $this->updateUserData($userData);
        } else {
            // если не нашли то отправляем на регистрацию
            $this->identity = $this->insertUserData($userData);
        }
    }

    /** Добавляем пользователя
     * @param $userData array
     * @return bool
     */
    public function insertUserData($userData)
    {
        // готовим запрос
        $insert = $this->connect()
            ->prepare("INSERT INTO users SET " .
                $this->pdoSet(array_keys($userData), $values, $userData));
        return $insert->execute($values)
            ? $this->getUserByTelegramId($userData['telegram_id'])
            : false;
    }

    /** Обновляем данные пользователя
     * @param $userData array
     * @return bool
     */
    public function updateUserData($userData)
    {
        return $this->setParam($userData);
    }

    /** Проверяем наличие данных пользователя
     * @param $telegram_id int
     * @return bool|self
     */
    public function getUserByTelegramId($telegram_id)
    {
        $order = $this->connect()
            ->prepare("SELECT * FROM users WHERE telegram_id = :telegram_id LIMIT 1");
        $order->execute([
            'telegram_id' => (int)$telegram_id
        ]);
        // если пользователь найден то возвращаем объект
        return $order->rowCount() > 0 ? $order->fetch(PDO::FETCH_OBJ) : false;
    }

    /** Получаем id
     * @return int
     */
    public function getId()
    {
        return $this->identity->id;
    }

    /** Получаем настройку lang
     * @return string
     */
    public function getLang()
    {
        return $this->identity->lang;
    }

    /** Меняем настройку lang
     * @param $lang
     * @return bool
     */
    public function setLang($lang)
    {
        $this->identity->lang = $lang;

        return $this->setParam([
            'lang' => $lang,
            'telegram_id' => $this->identity->telegram_id,
        ]);
    }

    /** Получаем first_name
     * @return string
     */
    public function getFirstName()
    {
        return $this->identity->first_name;
    }

    /** Получаем last_name
     * @return string
     */
    public function getLastName()
    {
        return $this->identity->last_name;
    }

    /** Получаем username
     * @return string
     */
    public function getUserName()
    {
        return $this->identity->username;
    }

    /** Получаем полное имя пользователя
     * @return string
     */
    public function getFullName() {
        return trim($this->getFirstName() . " " . $this->getLastName());
    }

    /** Обновляем данные пользователя
     * @param array $userData
     * @param bool $type
     * @return bool
     */
    private function setParam($userData = [], $type = true)
    {
        if (count($userData) > 0) {
            $update = $this->connect()
                ->prepare("UPDATE users SET " .
                    $this->pdoSet(array_keys($userData), $values, $userData, $type)
                    . " WHERE telegram_id = :telegram_id");
            return $update->execute($values);
        } else {
            return false;
        }
    }
}
?>

Добавим новый метод setPdo в класс по организации соединения с СУБД MySQL, сам класс Db вы можете посмотреть в предыдущей статье "Авторизация на сайт через Телеграм без использования официального виджета", также в прикрепленном к статье файле будут все приведенные скрипты.

<?
    /** Готовим данные для запроса
     * @param $allowed
     * @param $values
     * @param array $source
     * @param bool $type
     * @return bool|string
     */
    public function pdoSet($allowed, &$values, $source, $type = false)
    {
        $set = '';
        $values = [];
        foreach ($allowed as $field) {
            if (isset($source[$field])) {
                $values[$field] = $source[$field];
                if ($field == 'telegram_id' && $type) {
                    continue;
                } else {
                    $set .= "`" . str_replace("`", "``", $field) . "`" . "=:" . $field . ", ";
                }
            }
        }
        return substr($set, 0, -2);
    }
?>

Вынесем все необходимые в рамках этой статьи методы взаимодействия с Telegram Bot API в отдельный класс Bot, не забудьте заменить ___TOKEN__ВАШЕГО__БОТА___ на токен от своего бота.

Класс при необходимости можно расширить, а методы дополнить, но это на ваше усмотрение. 

<?php
/**
 * Class Bot
 * @property string $token
 * @property array $data
 */
class Bot
{
    // токен бота
    public $token = "___TOKEN__ВАШЕГО__БОТА___";
    // для хранения массива данных от Телеграм
    public $data;

    /**
     * Bot constructor.
     */
    public function __construct()
    {
        // записываем в свойство данные
        $this->data = json_decode(file_get_contents('php://input'), true);
    }

    /** Получаем id
     * @param $data
     * @return mixed
     */
    public function getChatId()
    {
        if ($this->getType() == "callback_query") {
            return $this->data['callback_query']['message']['chat']['id'];
        }
        return $this->data['message']['chat']['id'];
    }

    /** Получаем first_name
     * @param $data
     * @return mixed
     */
    public function getChatFirstName()
    {
        if ($this->getType() == "callback_query") {
            return $this->data['callback_query']['message']['chat']['first_name'];
        }
        return $this->data['message']['chat']['first_name'];
    }

    /** Получаем last_name
     * @param $data
     * @return mixed
     */
    public function getChatLastName()
    {
        if ($this->getType() == "callback_query") {
            return $this->data['callback_query']['message']['chat']['last_name'];
        }
        return $this->data['message']['chat']['last_name'];
    }

    /** Получаем username
     * @param $data
     * @return mixed
     */
    public function getChatUserName()
    {
        if ($this->getType() == "callback_query") {
            return $this->data['callback_query']['message']['chat']['username'];
        }
        return $this->data['message']['chat']['username'];
    }

    /** Получаем id сообщения
     * @param $data
     * @return mixed
     */
    public function getMessageId()
    {
        if ($this->getType() == "callback_query") {
            return $this->data['callback_query']['message']['message_id'];
        }
        return $this->data['message']['message_id'];
    }

    /** Получим значение текст
     * @return mixed
     */
    public function getText()
    {
        if ($this->getType() == "callback_query") {
            return $this->data['callback_query']['data'];
        }
        return $this->data['message']['text'];
    }

    /** Узнаем какой тип данных пришел
     * @param $data
     * @return bool|string
     */
    public function getType()
    {
        if (isset($this->data['callback_query'])) {
            return "callback_query";
        } elseif (isset($this->data['message']['text'])) {
            return "message";
        } else {
            return false;
        }
    }

    /** Уведомление в клиенте
     * @param $cbq_id
     * @param $text
     * @param bool $type
     */
    public function notice($text = "")
    {
        $data = [
            'callback_query_id' => $this->data['callback_query']['id'],
        ];
        if (!empty($text)) {
            $data['text'] = $text;
        }
        $this->botApiQuery("answerCallbackQuery", $data);
    }

    /** Удаляем сообщение
     * @param $chat_id
     * @param $message_id
     * @return mixed
     */
    public function deleteMessage()
    {
        return  $this->botApiQuery("deleteMessage", [
                "chat_id" => $this->getChatId(),
                "message_id" => $this->getMessageId()
            ]
        );
    }

    /** Кнопка inline
     * @param $text
     * @param string $callback_data
     * @param string $url
     * @return array
     */
    public function buildInlineKeyboardButton($text, $callback_data = '', $url = '')
    {
        // рисуем кнопке текст
        $replyMarkup = [
            'text' => $text,
        ];
        // пишем одно из обязательных дополнений кнопке
        if ($url != '') {
            $replyMarkup['url'] = $url;
        } elseif ($callback_data != '') {
            $replyMarkup['callback_data'] = $callback_data;
        }
        // возвращаем кнопку
        return $replyMarkup;
    }

    /** Набор кнопок inline
     * @param array $options
     * @return string
     */
    public function buildInlineKeyBoard(array $options)
    {
        // собираем кнопки
        $replyMarkup = [
            'inline_keyboard' => $options,
        ];
        // преобразуем в JSON объект
        $encodedMarkup = json_encode($replyMarkup, true);
        // возвращаем клавиатуру
        return $encodedMarkup;
    }

    /** Отправляем текстовое сообщение с inline кнопками
     * @param $user_id
     * @param $text
     * @param null $buttons
     * @return mixed
     */
    public function sendMessage($user_id, $text, $buttons = NULL)
    {
        // готовим массив данных
        $data_send = [
            'chat_id' => $user_id,
            'text' => $text,
            'parse_mode' => 'html',
        ];

        // если переданны кнопки то добавляем их к сообщению
        if (!is_null($buttons) && is_array($buttons)) {
            $data_send['reply_markup'] = $this->buildInlineKeyBoard($buttons);
        }
        // отправляем текстовое сообщение
        return $this->botApiQuery("sendMessage", $data_send);
    }

    /** Запросы в Бот АПИ
     * @param $method
     * @param array $fields
     * @return mixed
     */
    public function botApiQuery($method, $fields = array())
    {
        $ch = curl_init('https://api.telegram.org/bot' . $this->token . '/' . $method);
        curl_setopt_array($ch, array(
            CURLOPT_POST => count($fields),
            CURLOPT_POSTFIELDS => http_build_query($fields),
            CURLOPT_SSL_VERIFYPEER => 0,
            CURLOPT_RETURNTRANSFER => 1,
            CURLOPT_TIMEOUT => 10
        ));
        $r = json_decode(curl_exec($ch), true);
        curl_close($ch);
        return $r;
    }
}
?>

* * *

Добавляем боту мультиязычность

В качестве примера применения базы пользователей сделаем боту мультиязычный интерфейс, для этого необходимо при старте пользователю выводить возможность выбора языка в случае, если ранее им язык не был выбран. Мультиязычность позволит вам как создателю бота расширить круг пользователей, повысить лояльность и соответственно получить больше клиентов.

Класс Lang работает с файлами, которые содержат текстовые данные на разных языках в формате JSON. На сервере сделаем для них отдельную директорию lang и поместим ее в корень.

Структура приложения по итогу будет выглядеть так:

Метод getParam() принимает один обязательный параметр и один необязательный. Обязательный параметр - это ключ в массиве данных языкового файла, по нему мы будем получать значение контента. В качестве не обязательного параметра можно передать массив (ключ - значение) для подстановки необходимых нам динамических данных, например имя пользователя: 

$this->lang->getParam("helloText", [ 'name' => $this->user->getFullName() ]);

Lang.php

<?php
/**
 * Class Lang
 * @property string $lang
 * @property array $data
 * @property string $dir
 */
class Lang
{
    private $lang;
    private $data;
    private $dir;

    /**
     * Lang constructor.
     * @param $lang
     */
    public function __construct($lang)
    {
        $this->lang = $lang;
        $this->dir = __DIR__ . '/../lang/';
        $this->getData();
    }

    /**
     * Получаем данные из файла и записываем в свойство
     */
    private function getData()
    {
        $this->data = json_decode(file_get_contents( $this->dir . $this->lang . '.json'), true);
    }

    /** Получаем текст по запросу
     * @param $param
     * @param array $data
     * @return mixed
     */
    public function getParam($param, $data = [])
    {
        $text = $this->data[$param];
        if (count($data) > 0) {
            foreach ($data as $key => $val) {
                $text = str_replace("{" . $key . "}", $val, $text);
            }
        }
        return $text;
    }
}
?>


Файлы с контентом ru.json и en.json

ru.json
{
  "error": "Произошла исключительная ситуация",
  "helloText": "Привет мой друг!!!\nТвое имя <b>{name}</b>\n\n/lang - Сменить настройки языка"
}

en.json
{
  "error": "An exceptional situation has occurred",
  "helloText": "Hello, my friend!!!\nYour name is <b>{name}</b>\n\n/lang - Change language"
}

Переходим к самому интересному, это класс WebHook. После того как пришел запрос от пользователя мы создаем необходимые объекты и отправляем на проверку языковой настройки. В случае если у пользователя еще не выбран язык (это возможно при первом старте бота) то ему выводится предложение со списком языковых настроек. Если у него язык уже выбран, то все данные передаются в роутер, и там в зависимости от команды выполняются необходимые действия. В нашем варианте выводится приветствие.

Еще есть команда на смену языка /langпри ее выполнении пользователю предлагается список на смену языковой настройки. 

<?php
// Подключаем класс для работы с БД
require_once("Db.php");
// подключаем класс Lang
require_once("Lang.php");
// подключаем класс User
require_once("User.php");
// подключаем класс Bot
require_once("Bot.php");

/**
 * Class WebHook
 * @property User $user
 * @property Bot $bot
 * @property Lang $lang
 */
class WebHook extends Db
{
    private $user;
    private $bot;
    private $lang;

    /**
     * Инициализируем работу класса
     */
    public function __construct()
    {
        // создаем объект бота
        $this->bot = new Bot();
        //  создаем объект пользователя
        $this->user = new User($this->prepareUserData());
        // отправляем на проверку настройки языка
        $this->userLang();
    }

    /** Роутер
     * @param $data array
     */
    private function router()
    {
        // Если это объект Message
        if ($this->bot->getType() == "message") {
            // проверяем на наличие объекта Text
            if (array_key_exists("text", $this->bot->data['message'])) {
                if ($this->bot->getText() == "/start") {
                    // Выводим приветствие
                    $this->startBot();
                } elseif ($this->bot->getText() == "/lang") {
                    // Смена настройки Lang
                    $this->changeLang();
                } else {
                    // выводим ошибку
                    $this->showError();
                }
            } else {
                // выводим ошибку
                $this->showError();
            }
        // Если это объект callback_query
        } elseif ($this->bot->getType() == "callback_query") {
            $func_param = explode("_", $this->bot->getText());
            $func = $func_param[0];
            $this->$func();
        }
        // Другие объекты не рассматриваем
        else {
            // выводим ошибку
            $this->showError();
        }
    }

    /**
     * Первый экран
     */
    private function startBot()
    {
        // выводим приветствие
        $this->bot->sendMessage($this->bot->getChatId(), $this->lang->getParam("helloText",[
            'name' => $this->user->getFullName()
        ]));
    }

    /** Готовим данные для пользователя
     * @return array
     */
    private function prepareUserData()
    {
        return [
            'telegram_id' => $this->bot->getChatId(),
            'first_name' => $this->bot->getChatFirstName(),
            'last_name' => $this->bot->getChatLastName(),
            'username' => $this->bot->getChatUserName(),
        ];
    }

    /**
     *  Выводим ошибку
     */
    private function showError() {
        $this->bot->sendMessage($this->bot->getChatId(), $this->lang->getParam("error"));
    }

    /**
     *  Проверяем выбран ли язык у пользователя
     */
    private function userLang()
    {
        if (is_null($this->user->getLang())) {
            // получаем текст
            $text = $this->bot->getText();
            // возможно это запрос на 1-ю смену языка, т.е. язык не установлен
            // просто при инлайн запросе все равно в index запрос сюда идет в userLang
            // просто перенаправляем в setUserLangInline_ а то цикл замкнутый идет
            if (preg_match("~^setUserLangInline_~", $text)) {
                // отправляем на смену
                $this->setUserLangInline();
            } else {
                // делаем запрос на смену язык
                $this->changeLang();
            }
        } else {
            // получаем языковые настройки
            $this->lang = new Lang($this->user->getLang());
            // передаем в роутер
            $this->router();
        }
    }

    /**
     * Запрос на установку языка
     */
    private function changeLang()
    {
        // готовим кнопки
        $buttons[] = [
            ['text' => '?? Русский', 'callback_data' => 'setUserLangInline_ru'],
            ['text' => '?? English', 'callback_data' => 'setUserLangInline_en'],
        ];

        // отправляем сообщение
        $this->bot->sendMessage($this->bot->getChatId(), 'Selected language', $buttons);
    }

    /**
     * Обработка команды inline по установке языка
     */
    private function setUserLangInline()
    {
        // 1 - lang
        $param = explode("_", $this->bot->getText());
        // устанавливаем язык пользовтаелю
        $setLang = $this->user->setLang($param[1]);
        // проверяем
        if ($setLang) {
            // глушим уведомление
            $this->bot->notice();
            // удаляем сообщение
            $this->bot->deleteMessage();
            // Определяем язык
            $this->lang = new Lang($this->user->getLang());
            // перенаправляем на старт бота
            $this->startBot();
        } else {
            // выводим сообщение об ошибке popup
            $this->bot->notice("Failed to change language, try again");
        }
    }
}
?>


Index.php
- это файл, на который необходимо настроить вебхук. 

<?php
    header("HTTP/1.1 200 OK");
    // определим кодировку UTF-8
    header('Content-type: text/html; charset=utf-8');
    // подключаем класс User
    require_once("models/WebHook.php");
    // запускаем вебхук
    new WebHook();
?>

* * *

Заключение

После успешной настройки вышеописанного приложения у вас будет основа для разработки мультиязычного приложения на базе Телеграм. Исходный код можно скачать и использовать на свое усмотрение.

sobiraem-bazu.zip
9.4
2 комментария
Авторизуйтесь через Telegram, чтобы оставить комментарий.
Откройте по ссылке или QR бот @iMakeBot, нажмите кнопку Старт/Start.
Следуйте инструкциям бота.

  • Jay Gatsby [2 года назад]

    а можно сделать вместо MySQL базы, базу SQLite ?

  • iMakeBots [2 года назад → Jay Gatsby]

    Конечно, можно - вы можете переписать весь код бота под ваши нужды