Итак, сегодня мы попробуем несколько увеличить функционал нашего простейшего чат-бота, а именно: прикрутим к нему базу данных MySQL, в которой будем хранить список подключавшихся к боту пользователей и информацию о них, а так же добавим возможность этих пользователей забанивать и разбанивать.
Первым делом создаём базу данных (пусть она называется, скажем, MYBASE) и дописываем в наш файл config.php данные для доступа к этой БД:
$dbhost = 'MYHOST'; // адрес сервера БД $dbname = 'MYBASE'; // имя БД $dbuser = 'username'; // имя пользователя для доступа к БД $dbpwd = 'password'; // пароль для доступа к БД |
Теперь нужно определиться со структурой нашей БД. Пусть в ней будет всего одна таблица, назовём её, скажем, MYTABLE, которая будет содержать следующие поля:
- user_id — уникальный идентификатор пользователя в Telegram, varchar(32)
- first_name — имя пользователя в Telegram, varchar(1024)
- last_name — фамилия пользователя в Telegram, varchar(1024)
- nick_name — ник пользователя в Telegram, varchar(1024)
- registered — время первого подключения юзера (timestamp)
- lastvisit — время последнего общения юзера с ботом (timestamp)
- status — статус пользователя (забанен/не забанен), uint(8)
Поле user_id нужно пометить как уникальное, для поля status установить значение по умолчанию 1 (у незабаненых юзеров будет status=1, а у забаненых status=0), для полей registered и lastvisit — установить значение по умолчанию «CURRENT_TIMESTAMP», а также дополнительно установить опцию «on update CURRENT_TIMESTAMP» для поля lastvisit (чтобы каждый раз при обновлении записи значение этого поля автоматически обновлялось).
Два небольших замечания:
- Обычно все любят заводить в таблице первичный ключ id с автоинкрементом, однако нам он не нужен, поскольку user_id и так будут уникальные (это обеспечивает сам Telegram), без всякого автоинкремента, а использовать значение последнего элемента этого поля для подсчёта количества пользователей в таблице — вообще дурь, так как при удалении или обновлении записей в таблице всё сдвинется в любом случае (при обновлении записей в таблицах (запросом UPDATE) автоинкремент выполняется даже если поля реально не обновлялись).
- Почему-то во всех экспериментах с ботами Telegram, в любом апдейте и с любыми ботами, я наблюдал одну и ту же картину, — chat_id и user_id у одного и того же пользователя всегда совпадают. Причём, я считаю, что получаемое в полях chat_id и user_id значение — это именно user_id, поскольку он одинаковый при общении юзера с любым другим ботом, а значения chat_id должны в этом случае быть разными по определению, — разные боты — разные чаты.
Последнее замечание не очень похоже на ошибку (как если бы нам просто отдавали не то поле), поскольку отправка сообщений методом sendMessage при использовании этого идентификатора (user_id) работает нормально, хотя в описании сказано, что здесь должен быть chat_id. Техподдержка Telegram мой вопрос на эту тему проигнорировала. В общем, либо нам чего-то не договаривают, либо я что-то, где-то не дочитал.
Мой товарищ, Virtual, вообще считает, что так и должно быть, а chat_id юзеров «A» и «B» Telegram получает сложением двух уникальных строк: user_id юзера «A» и user_id юзера «B». Полученный таким сложением идентификатор был бы уникальным (поскольку состоял бы из двух уникальных частей) и однозначно идентифицировал бы кто с кем чатится. Это было бы логично, хотя в доке, в любом случае, написано по-другому.
Ладно, хватит лирических отступлений, идём дальше (но если кто разберётся в чём тут подвох — отпишитесь в коментах или на форуме).
Нам нужно добавлять в БД данные о пользователях и читать данные о пользователях из БД.
Вместо того, чтобы для каждого юзера выяснять, есть ли он в БД, и далее добавлять его данные в базу, если его там нет (это очень долгий путь), просто будем каждый раз пытаться добавить его в базу (если он там уже есть — запрос просто не выполнится). Одновременно с попыткой добавления будем пытаться обновить данные о юзере, записанные в БД, и пытаться прочитать его статус (забанен или нет).
Всё это мы будем делать в одном мультизапросе, а чтобы его выполнение не прекращалось из-за невозможности выполнить какой-то из подзапросов, — воспользуемся директивой IGNORE.
Для реализации всех вышеперечисленных идей дописываем в файле bot.php, перед строкой
// начинаем распарсивать полученное сообщение |
следующий код:
$user_id = $input_array['message']['from']['id']; // выделяем идентификатор юзера $fname = $input_array['message']['chat']['first_name']; // выделяем имя собеседника $lname = $input_array['message']['chat']['last_name']; // выделяем фамилию собеседника $uname = $input_array['message']['chat']['username']; // выделяем ник собеседника if(!$user_id){ // если в сообщении нет иденификатора юзера exit(); // завершаем работу скрипта } // пытаемся подключиться к БД $mysqli = new mysqli($dbhost, $dbuser, $dbpwd, $dbname); if ($mysqli->connect_errno) { // если подключиться не получилось - сообщаем админу sendMessage($admin_chat_id,'Не удалось подключиться к БД ('.$mysqli->connect_errno.': '.$mysqli->connect_error.') для пользователя с user_id: '.$user_id); exit(); } $sel_number = 4; // номер интересующего нас запроса (нам интересен ответ на запрос SELECT status) $sql = ""; // здесь формируем строку мультизапроса // запрос 1 - выставляем кодировку $sql .= 'SET NAMES utf8;'; // запрос 2 - добавляем юзера в базу $sql .= 'INSERT IGNORE INTO MYTABLE (user_id, first_name) VALUES("'.$user_id.'", "'.$fname.'");'; // запрос 3 - обновляем данные юзера в базе $sql .= 'UPDATE IGNORE MYTABLE SET first_name="'.$fname.'", last_name="'.$lname.'", nick_name="'.$uname.'" WHERE user_id="'.$user_id.'";'; // запрос 4 - запрос на поиск юзера в базе $sql .= 'SELECT status FROM MYTABLE WHERE user_id="'.$user_id.'";'; if (!$mysqli->multi_query($sql)) { // пытаемся выполнить мультизапрос sendMessage($admin_chat_id,'Не удалось выполнить мультизапрос ('.$mysqli->errno.': '.$mysqli->error.') для пользователя с user_id: '.$user_id); exit(); // подключение отвалится само при завершении скрипта } $status = NULL; // вначале статус юзера нам неизвестен $counter = 0; // инициализируем счётчик обрабатываемых результатов do { $counter += 1; // увеличиваем счётчик $res = $mysqli->store_result(); // получаем результат i-того запроса if ($res){ // если результат запроса не нулевой if($counter == $sel_number){ // если это запрос select status $row = $res->fetch_row(); // получаем первую строку ответа // (она у нас всего одна должна быть, мы же ищем по user_id, а он уникальный) if(isset($row[0])){ // если нулевой элемент этой строки существует // (значение status, поскольку мы запрашивали только его), $status = $row[0]; // то статус юзера равен прочитанному } } $res->free(); // если результат был не пустой, то освобождаем его } } while ($mysqli->more_results() && $mysqli->next_result()); //перебираем все результаты мультизапроса $mysqli->close(); // закрываем подключение к базе // проверяем что у нас получилось if(!isset($status)){ // если статус юзера не определён sendMessage($admin_chat_id,'Статус юзера с user_id = '.$user_id.' остался неопределёным.'); exit(); } elseif(!$status){ // если статус юзера равен нулю (юзер забанен) sendMessage($chat_id,'Для Вас заблокирована возможность посылать сообщения роботу @BOTNAME'); exit(); } |
Теперь нам нужно добавить две новые команды: /ban и /unban. Это делается следующим образом:
if(($chat_id == $admin_chat_id) && ($command === '/send') && ($user_text_length!=0)){ |
на вот такую:
if(($chat_id == $admin_chat_id) && (($command === '/send') || ($command === '/ban') || ($command === '/unban')) && ($user_text_length!=0)){ |
/send <i>chat_id</i> <i>message</i> - послать <i>message</i> в указанный чат'); |
на вот такие:
/send <i>chat_id</i> <i>message</i> - послать <i>message</i> в указанный чат /ban <i>user_id</i> - забанить пользователя с указанным user_id /unban <i>user_id</i> - разбанить пользователя с указанным user_id'); |
default: |
дописываем следующий код:
case('/ban'): if($chat_id == $admin_chat_id){ // если это команда от админа if($user_chat_id != $admin_chat_id){ // если админ не пытается забанить сам себя // пытаемся подключиться к БД $mysqli = new mysqli($dbhost, $dbuser, $dbpwd, $dbname); if ($telegram_mysqli->connect_errno) { sendMessage($admin_chat_id,'Не удалось подключиться к БД ('.$mysqli->connect_errno.': '.$mysqli->connect_error.') для пользователя с user_id: '.$user_id); exit(); } // формируем запрос на обновление статуса указанного юзера в базе (меняем его на 0) $sql = 'UPDATE IGNORE MYTABLE SET status="0" WHERE user_id="'.$user_chat_id.'";'; if (!$mysqli->multi_query($sql)) { // пытаемся выполнить мультизапрос sendMessage($admin_chat_id,'Не удалось добавить в бан пользователя c user_id = '.$user_chat_id); exit(); // подключение отвалится само при завершении скрипта } $mysqli->close(); // закрываем подключение к базе sendMessage($admin_chat_id,'Запрос на добавление в бан пользователя c user_id = '.$user_chat_id.' выполнен'); } else{ // если всё же админ пытается забанить сам себя sendMessage($admin_chat_id,'Никто не имеет права банить админа, даже сам админ!'); } } else{ sendMessage($chat_id,'неизвестная команда'); // если команда не от админа, то её как бы нет } break; case('/unban'): if($chat_id == $admin_chat_id){ // если это команда от админа // пытаемся подключиться к БД $mysqli = new mysqli($dbhost, $dbuser, $dbpwd, $dbname); if ($mysqli->connect_errno) { sendMessage($admin_chat_id,'Не удалось подключиться к БД ('.$mysqli->connect_errno.': '.$mysqli->connect_error.') для пользователя с user_id: '.$user_id); exit(); } // формируем запрос на обновление статуса юзера в базе (меняем его на 1) $sql = 'UPDATE IGNORE MYTABLE SET status="1" WHERE user_id="'.$user_chat_id.'";'; if (!$mysqli->multi_query($sql)) { // пытаемся выполнить мультизапрос sendMessage($admin_chat_id,'Не удалось выполнить отмену бана пользователя c user_id = '.$user_chat_id); exit(); } $mysqli->close(); // закрываем подключение к базе sendMessage($admin_chat_id,'Запрос на отмену бана пользователя c user_id = '.$user_chat_id.' выполнен'); } else{ sendMessage($chat_id,'неизвестная команда'); // если команда не от админа, то её как бы нет } break; |
Загружаем к себе на сервер (в папку MYBOT) исправленные файлы bot.php и config.php и смотрим, что получилось. На этом этапе у нас уже всё должно работать. Остаётся, как обычно, хоть чуть-чуть озаботится вопросами безопасности.
Поскольку мы добавили код, который позволяет вводить что-то, что в дальнейшем участвует в формировании SQL-запросов, то защищаться нам придётся от возможных SQL-инъекций. Соответственно, нам нужно добавить какую-то обработку вводимой информации. Самое простое, что мы можем в данном случае сделать, — это заменять спецсимволы на их html-коды (например, > на > и так далее). Открываем файл bot.php и дописываем после строки
$uname = $input_array['message']['chat']['username']; // выделяем ник собеседника |
следующий код:
// обрабатываем принятое сообщение для защиты и удобства $message = trim($message); // удаляем пробелы $message = htmlspecialchars($message, ENT_QUOTES); // преобразуем спецсимволы (&, ", ', <, >) в html-сущности |
Снова перезаливаем исправленный файл bot.php на сервак и наслаждаемся результатом, — у нас есть организованный через бота Telegram-чат с регистрацией юзеров и возможностью из забанить или разбанить.
Что ещё можно добавить? Да много чего. Например:
- возможность существования нескольких админов, которым бот будет присылать сообщения от юзеров
- возможность добавления/удаления админов
- возможность админам включать режим «не беспокоить», для временной приостановки получения сообщений
- возможность сохранения переписки
Однако, всё это вы можете сделать самостоятельно, а я на этом закончу (и так уже слишком много букв). Лучше начну писать про long polling.
- Часть 1. Что такое Telegram боты и как они работают
- Часть 2. Регистрация аккаунтов Telegram ботов в картинках
- Часть 3. Пишем простого чат-бота для Telegram на чистом php (webhook)
- Часть 4. Прикручиваем MySQL к чат-боту для Telegram на php (webhook)
- Часть 5. Пишем Telegram бота на php для работы через longpolling
- Часть 6. Делаем пользовательскую клавиатуру для бота
- Дополнение. Как отправлять правильные https-запросы через curl и при чём здесь SSL-cертификаты