Перейти к содержимому

Как сделать базу данных для телеграм бота

  • автор:

Парсинг телеграм бота, с добавлением в базу данных

Есть возможность парсить заявки, которые отправляются из аккаунтов Telegram, и загружать их в базу данных. Один из способов добиться этого — использовать библиотеку python-telegram-bot, которая позволяет создавать ботов Telegram и взаимодействовать с API Telegram.

Во-первых, вам нужно создать бота, поговорив с BotFather и получить токен.

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

Затем вы можете использовать библиотеку sqlite3 для подключения к базе данных SQLite и вставки извлеченных данных в соответствующую таблицу.

Вот пример того, как вы можете обработать сообщение, содержащее приложение, и вставить информацию в базу данных SQLite:

import sqlite3 from telegram.ext import Updater, MessageHandler, Filters def handle_application(bot, update): message = update.message name = message.text conn = sqlite3.connect('applications.db') c = conn.cursor() c.execute("INSERT INTO applications (name) VALUES (?)") 

Если я ошибаюсь то прошу поправить

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

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

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

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

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

Для начала создадим таблицу 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 по работе с таблицей, к нему мы будем обращаться для создания, получения или обновления данных о пользователях.

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 вы можете посмотреть в предыдущей статье «Авторизация на сайт через Телеграм без использования официального виджета», также в прикрепленном к статье файле будут все приведенные скрипты.

 else < $set .= "`" . str_replace("`", "``", $field) . "`" . "=:" . $field . ", "; >> > return substr($set, 0, -2); > ?>

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

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
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("", $val, $text); > > return $text; > > ?>

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

ru.json < "error": "Произошла исключительная ситуация", "helloText": "Привет мой друг. \nТвое имя \n\n/lang - Сменить настройки языка" > en.json < "error": "An exceptional situation has occurred", "helloText": "Hello, my friend. \nYour name is \n\n/lang - Change language" > 

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

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
— это файл, на который необходимо настроить вебхук.

Заключение

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

Создание Чат-Бот-Магазин в Телеграм с нуля. Часть вторая

Создание окружения, рабочие методы. Блок администрирования бот-магазина. Дополнения и изменения в структуре базы данных. Знакомство с PDO.

Продолжение: (Часть 1 и Часть 3, Часть 4) цикла статей по созданию чат-бот-магазина в Телеграм. В этой статье мы перейдем непосредственно к практической части. Забегая вперед сообщу, что исходный код рассматриваемого блока администрирования можно скачать в конце статьи. Созданию окружения для работы в BOT API мы уделим особое внимание, считаю это одним из основных составляющих приложения. Что я имею ввиду под словом «окружение»: это методы запуска приложения, для работы с базой данных, метод-роутер и методы работы с BOT API. * * *

Настройка окружения

Рассмотрим метод запуска приложения init() — это метод, который запускает работу с базой данных и работу роутера.

setPdo(); // получаем данные от АПИ и преобразуем их в ассоциативный массив $rawData = json_decode(file_get_contents('php://input'), true); // направляем данные из бота в метод роутер // для определения дальнейшего выбора действий $this->router($rawData); // в любом случае вернем true для бот апи return true; > ?>

В методе работы с базой данных, создадим объект PDO и добавим его в свойство $this->pdo

host;dbname=$this->db;charset=$this->charset"; // дополнительные опции $opt = [ // способ обработки ошибок - режим исключений PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // тип получаемого результата по-умолчанию - ассоциативный массив PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // отключаем эмуляцию подготовленных запросов PDO::ATTR_EMULATE_PREPARES => false, // определяем кодировку запросов PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci" ]; // записываем объект PDO в свойство $this->pdo $this->pdo = new PDO($dsn, $this->user, $this->pass, $opt); > ?>

Наш роутер определяет какой тип данных нам пришел от BOT API и направляет их по заданному нами направлению. В нем мы будем использовать дополнительно еще несколько инструментов для определения статуса пользователя и в случае если это администратор бота, то проверим на цель данных. Для работы нашего приложения в блоке администрирования бот-магазина, мы будем обрабатывать 2 типа данных — это message и callback_query , остальные нам пока не нужны, поэтому будем выводить предупреждение. В тип message приходят объекты, отправленные пользователем: текст, картинки, видео, документы, аудиофайл, видеофайл и другие. Объект callback_query приходит только при нажатии inline кнопки с опцией callback_data . Мы составим условия таким образом, что принимать message будем только текст и картинки, а callback_query будем перенаправлять в необходимый метод и в нем уже обрабатывать запрос на действие. При проверке на текстовое сообщение мы зададим несколько шаблонов в условиях и при их выполнении направим в нужные нам методы. Это стандартная команда для старта бота /start , для страницы админа /admin и еще несколько команд для отображения экранов для просмотра категорий, контактов, добавления категории. Все остальные текстовые сообщения, которые пришли в бот, мы будем определять по действию, которое было записано во временную таблицу в бд и при условии, что пользователь является админом. Действия мы записываем в бд по мере вызова методов добавления категорий или товара в категории. При запросе на добавление категории мы запишем действие в бд как addcategory , и следующее текстовое сообщение мы будем воспринимать как название категории. В случае если будет отправлен другой контент — картинка например, то по условиям будет выведена ошибка в отправленных данных, потому что картинку мы ждем только на 5 шаге добавления товара. Обратите внимание как обрабатывается объект callback_query . Мы смотрим какое значение data приходит в объекте, это всегда (в нашем приложении) строка, состоящая из значений разделенные знаком нижнего подчеркивания и первым значением идет название метода в который нужно передать данные, далее это необходимые параметры в основном это идентификаторы.

getChatId($data); $text = $this->getText($data); // если пришли данные message if (array_key_exists("message", $data)) < // дастаем действие админа из базы $action = $this->getAdminAction(); // текстовые данные if (array_key_exists("text", $data['message'])) < // если это пришел старт бота if ($text == "/start") < $this->startBot($chat_id); > elseif ($text == "/admin" && $this->isAdmin($chat_id)) < // выводим страницу только админу $this->adminPage(); > elseif ($text == "/admincategory" && $this->isAdmin($chat_id)) < // Страница админ категорий $this->adminCategory(); > elseif ($text == "/addcategory" && $this->isAdmin($chat_id)) < // отправляем на добавление категории $this->addCategory(); > elseif ($text == "/admincontact" && $this->isAdmin($chat_id)) < // просмотр контактов $this->adminContact(); > else < // смотрим куда отправить данные if ($action == "addcategory" && $this->isAdmin($chat_id)) < // если ждем данные для добавления категории $this->adderCategory($text); > elseif (preg_match("~^addproduct_1_~", $action) && $this->isAdmin($chat_id)) < // если ждем данные для добавления товара step_1 - название $param = explode("_", $action); // отправляем на добавление описания $this->addProductName($param['2'], $text); > elseif (preg_match("~^addproduct_2_~", $action) && $this->isAdmin($chat_id)) < // если ждем данные для добавления товара step_2 - описание $param = explode("_", $action); // отправляем на добавление описания $this->addProductDescription($param['2'], $param['3'], $text); > elseif (preg_match("~^addproduct_3_~", $action) && $this->isAdmin($chat_id)) < // если ждем данные для добавления товара step_3 - единица измерения $param = explode("_", $action); // отправляем на добавление описания $this->addProductPrice($param['2'], $param['3'], $text); > elseif (preg_match("~^addproduct_4_~", $action) && $this->isAdmin($chat_id)) < // если ждем данные для добавления товара step_4 - цена $param = explode("_", $action); // отправляем на добавление описания $this->addProductUnit($param['2'], $param['3'], $text); > elseif (preg_match("~^addcontact_~", $action) && $this->isAdmin($chat_id)) < // если ждем данные для для редактирования контактов $param = explode("_", $action); // отправляем данные на редактирование контактов $this->rederContact($param[1], $text); > else < $this->sendMessage($chat_id, "Нам пока не нужны эти данные. Спасибо."); > > > elseif (array_key_exists("photo", $data['message'])) < // если пришли картинки if (preg_match("~^addproduct_5_~", $action) && $this->isAdmin($chat_id)) < // если ждем данные для добавления товара step_5 - картинка $param = explode("_", $action); // берем данные картинки $file_id = end($data['message']['photo'])['file_id']; // отправляем на добавление описания $this->addProductPhoto($param['2'], $param['3'], $file_id); > else < $this->sendMessage($chat_id, "Нам пока не нужны эти данные. Спасибо."); > > else < $this->sendMessage($chat_id, "Нам пока не нужны эти данные. Спасибо."); > > // если пришел запрос на функцию обратного вызова elseif (array_key_exists("callback_query", $data)) < // смотрим какая функция вызывается $func_param = explode("_", $text); // определяем функцию в переменную $func = $func_param[0]; // вызываем функцию передаем ей весь объект $this->$func($data['callback_query']); > // Здесь пришли пока не нужные нам форматы else < // вернем текст с ошибкой $this->sendMessage($chat_id, "Нам пока не нужны эти данные. Спасибо."); > > ?>

Для работы с действиями администратора есть несколько методов для выборки, для записи и для очистки.

pdo->query("DELETE FROM bot_shop_action_admin"); > /** Получаем действие админа из таблицы * @return bool */ private function getAdminAction() < // достаем из базы $last = $this->pdo->query("SELECT name FROM bot_shop_action_admin ORDER BY id DESC LIMIT 1"); // преобразуем строку в массив $lastAction = $last->fetch(); // если есть значение то возвращаем его иначе false return isset($lastAction['name']) ? $lastAction['name'] : false; > /** Записываем действие админа * @param $action * @return mixed */ private function setActionAdmin($action) < // отменяем все действия админа if ($this->adminActionCancel()) < // готовим запрос $insertSql = $this->pdo->prepare("INSERT INTO bot_shop_action_admin SET name = :name"); // возвращаем результат return $insertSql->execute(['name' => $action]); > else < // выводим ошибку $this->sendMessage($this->admin, "Ошибка отмены предыдущих действий."); > > ?>

Прокачиваем телеграм-бота: пишем полноценный бэкенд

Сегодня мы продолжим этот проект и добавим возможности нашему бэкенду для телеграм-бота. Если хотите разобраться в теме телеграм-ботов с самого начала, почитайте эти статьи:

Логика проекта

Мы сделаем простого бота для приёма отчётов — что сделано за день или над чем идёт работа:

  1. Сообщение может написать боту кто угодно. Бот запомнит, кто ему написал.
  2. Бот будет работать только с текстовыми сообщениями. Стикеры и файлы не подойдут.
  3. Чтобы никто не спамил, бот обрезает сообщение до 500 символов и записывает их в свою базу данных.
  4. База данных будет настоящая — SQLite, с которой Python работает без дополнительных настроек.
  5. У бота будут две команды — посмотреть все отчёты за вчера и за сегодня. По этим командам бот присылает отчёты.
  6. Другие команды бот игнорирует, удалить отчёт нельзя, бот помнит всё.

Это простая, но рабочая логика бэкенда — с ним бот уже может приносить пользу и быть рабочим инструментом.

Что понадобится

Для скрипта нам понадобится Python — половина современного бэкенда пишется именно на нём.

Также мы будем использовать базу данных SQLite — она простая, быстрая и работает практически везде. Прочитайте о ней: SQLite — самая простая база данных, которая работает везде

Код обработки команд для бота мы возьмём из наших прошлых статей про Телеграм и адаптируем под новый проект:

Исходный код прошлого проекта

# подключаем модуль для Телеграма import telebot # указываем токен для доступа к боту bot = telebot.TeleBot(123456789:AAAAA12345678abcdef12345678abcdef') # приветственный текст start_txt = 'Привет! Это журнал «Код». \n\nТеперь у бота появился бэкенд.' # обрабатываем старт бота @bot.message_handler(commands=['start']) def start(message): # выводим приветственное сообщение bot.send_message(message.from_user.id, start_txt, parse_mode='Markdown') # запускаем бота if __name__ == '__main__': while True: # в бесконечном цикле постоянно опрашиваем бота — есть ли новые сообщения try: bot.polling(none_stop=True, interval=0) # если возникла ошибка — сообщаем про исключение и продолжаем работу except Exception as e: print('❌❌❌❌❌ Сработало исключение! ❌❌❌❌❌')

Подключаем базу данных

Мы будем хранить все отчёты пользователей в базе данных — это позволит нам хранить почти неограниченное количество отчётов и быстро находить среди них нужные. Для этого сначала подключим библиотеку sqlite3:

# модуль для работы с базой данных
import sqlite3 as sl

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

# подключаемся к файлу с базой данных
con = sl.connect(‘reports.db’)

А вот тут есть тонкий момент: если это наш первый запуск программы, то после подключения скрипт получит пустую базу, без таблиц и внутренней структуры. Если мы попробуем туда что-то записать, то получим ошибку. Чтобы такого не было, сразу сделаем проверку, есть ли в базе нужная нам таблица. Если есть — всё хорошо, продолжаем работу, а если нет — просто создадим её и тоже продолжим работу.

Обратите внимание на структуру таблицы: чтобы каждая запись имела свой уникальный ключ, мы используем для этого комбинацию даты и времени. Дело в том, что время в Python можно получить с точностью до миллисекунды — этого достаточно для того, чтобы у каждого сообщения было своё уникальное время.

Ещё нам нужно хранить дату, чтобы выбирать по ней отчёты за вчера и за сегодня — сделаем это в поле date. В остальные поля будем записывать id и ник пользователя в Телеграме и сам текст сообщения.

# открываем файл with con: # получаем количество таблиц с нужным нам именем data = con.execute("select count(*) from sqlite_master where type='table' and name='reports'") for row in data: # если таких таблиц нет if row[0] == 0: # создаём таблицу для отчётов with con: con.execute(""" CREATE TABLE reports ( datetime VARCHAR(40) PRIMARY KEY, date VARCHAR(20), id VARCHAR(200), name VARCHAR(200), text VARCHAR(500) ); """)

Запоминаем отчёт пользователя

Когда база готова, мы можем принимать и сохранять все сообщения пользователей. Так как нам нужно каждой записи в таблице дать свой уникальный идентификатор, используем для этого комбинацию даты и времени. Для этого сначала подключим стандартные библиотеки:

# модуль работы со временем
from datetime import datetime, timezone, timedelta

Так как мы считаем каждое сообщение пользователя отчётом о работе, мы будем обрабатывать и запоминать каждое текстовое сообщение в чате. При этом мы принудительно обрежем сообщение и оставим только первые 500 символов, чтобы ограничить возможный спам. Как только скрипт получит любой текст, он занесёт его в базу и отправит пользователю сообщение с подтверждением.

# обрабатываем входящий отчёт пользователя @bot.message_handler(content_types=['text']) def func(message): # подключаемся к базе con = sl.connect('reports.db') # подготавливаем запрос sql = 'INSERT INTO reports (datetime, date, id, name, text) values(?, ?, ?, ?, ?)' # получаем дату и время now = datetime.now(timezone.utc) # и просто дату date = now.date() # формируем данные для запроса data = [ (str(now), str(date), str(message.from_user.id), str(message.from_user.username), str(message.text[:500])) ] # добавляем с помощью запроса данные with con: con.executemany(sql, data) # отправляем пользователю сообщение о том, что отчёт принят bot.send_message(message.from_user.id, 'Принято, спасибо!', parse_mode='Markdown')

Проверим, как работает код: отправим боту любое сообщение и посмотрим, получим ли мы что-то в ответ:

Прокачиваем телеграм-бота: пишем полноценный бэкенд

Выводим отчёты за сегодня

В реальном проекте мы бы отдельно прописали ID пользователя, который может запрашивать отчёты, чтобы этой командой мог пользоваться только один человек, например руководитель отдела. Сейчас для наглядности мы сделаем проще: сделаем команду /now доступной для всех. Когда бот её получит, он отправит в ответ все отчёты за сегодня.

Логика поиска и выводов отчёта будет такая:

  1. Подключаемся к базе.
  2. Получаем текущую дату и ищем в базе все записи с этой датой.
  3. Перебираем результаты и формируем из них сообщение с отчётами.
  4. Отправляем общий отчёт пользователю.

При этом нам ещё нужно предусмотреть ситуацию, когда отчётов сегодня не было. Если этого не сделать, скрипт попытается отправить пустое сообщение и выдаст ошибку. В этом случае нам нужно дополнительно проверить, пустая у нас строка с отчётом или нет. Если пустая — записать в неё текст о том, что за сегодня отчётов не было.

# обрабатываем команду /now @bot.message_handler(commands=['now']) def start(message): # подключаемся к базе con = sl.connect('reports.db') # получаем сегодняшнюю дату now = datetime.now(timezone.utc) date = now.date() # пустая строка для будущих отчётов s = '' # работаем с базой with con: # выполняем запрос к базе data = con.execute('SELECT * FROM reports WHERE date = :Date;',) # перебираем все результаты for row in data: # формируем строку в общем отчёте s = s + '*' + row[3] + '*' + ' → ' + row[4] + '\n\n' # если отчётов не было за сегодня if s == '': # формируем новое сообщение s = 'За сегодня ещё нет записей' # отправляем общий отчёт обратно в телеграм bot.send_message(message.from_user.id, s, parse_mode='Markdown')

Перезапускаем скрипт и просим бота показать нам сегодняшние отчёты:

Прокачиваем телеграм-бота: пишем полноценный бэкенд

Выводим вчерашние отчёты

Здесь всё то же самое, кроме даты и текста при отсутствии отчётов. С текстом всё просто, а со вчерашней — не совсем. Дело в том, что если штатными средствами получить текущую дату и просто отнять от неё единицу, то скрипт выдаст ошибку. Всё из-за того, что дата — это не целое число, а отдельный объект, из которого нельзя просто вычитать числа.

Чтобы получить вчерашнюю дату, используют функцию timedelta() — она считает разницу между текущей датой и указанным в ней количеством дней и эту разницу хранит как раз в виде объекта, который можно вычитать из даты.

# обрабатываем команду /yesterday @bot.message_handler(commands=['yesterday']) def start(message): # подключаемся к базе con = sl.connect('reports.db') # получаем вчерашнюю дату yesterday = datetime.today() - timedelta(days=1) y_date = yesterday.date() # пустая строка для будущих отчётов s = '' # работаем с базой with con: # выполняем запрос data = con.execute('SELECT * FROM reports WHERE date = :Date;',) # смотрим на результат for row in data: # если результат пустой — ничего не делаем if row[0] == 0: pass # если вчера были какие-то отчёты else: # добавляем их в общий список отчётов s = s + '*' + row[3] + '*' + ' → ' + row[4] + '\n\n' # если отчётов не было за вчера if s == '': # формируем новое сообщение s = 'За вчерашний день нет записей' # отправляем пользователю это новое сообщение bot.send_message(message.from_user.id, s, parse_mode='Markdown')

Так как мы только начали работать с ботом, вчерашних сообщений в нём ещё нет, поэтому в ответ на эту команду бот выдаст заготовленный текст:

Прокачиваем телеграм-бота: пишем полноценный бэкенд

Результат

Смотрите, что мы сегодня сделали:

  1. Подключили бэкенд в виде скрипта к фронтенду — телеграм-боту.
  2. После этого бот смог обрабатывать наши сообщения и команды — запоминать отчёты и отправлять их, когда нужно.
  3. Мы не трогали фронт, зато все наши изменения в бэкенде приводили к тому, что бот получал новые функции одну за другой.

Так и работает бэкенд: он почти не влияет на интерфейс, зато от него может полностью зависеть логика проекта. Если хотите делать крутой бэкенд и разбираться в этом — приходите в Практикум на курсы бэкенда, там научат всему, что потребуется.

Что нужно доделать в следующий раз

Сейчас бот работает и справляется с задачами, но есть пара моментов, которую нужно будет потом исправить:

  1. Если отчётов за день станет слишком много, возможно, бот не сможет отправить их в одном сообщении или не сможет отправить в принципе. Нужно добавить проверку на максимальное количество символов, и, если что, — делить одно сообщение на несколько.
  2. Код вывода сообщений за вчера и за сегодня — одинаковый за исключением нескольких строк. Нужно вынести общую часть в отдельную функцию, чтобы не плодить спагетти-код.

Попробуйте сделать это сами или дождитесь следующей статьи с продолжением, где мы исправим обе эти проблемы.

Готовый код

# подключаем модуль для Телеграма import telebot # модуль работы со временем from datetime import datetime, timezone, timedelta # модуль для работы с базой данных import sqlite3 as sl # указываем токен для доступа к боту bot = telebot.TeleBot('6285051364:AAGG2iZ77NIeWj0YjqIs791qxPhvcS3Yu0s') # приветственный текст start_txt = 'Привет! Это журнал «Код». \n\nТеперь у бота появился бэкенд.' # подключаемся к файлу с базой данных con = sl.connect('reports.db') # открываем файл with con: # получаем количество таблиц с нужным нам именем data = con.execute("select count(*) from sqlite_master where type='table' and name='reports'") for row in data: # если таких таблиц нет if row[0] == 0: # создаём таблицу для отчётов with con: con.execute(""" CREATE TABLE reports ( datetime VARCHAR(40) PRIMARY KEY, date VARCHAR(20), id VARCHAR(200), name VARCHAR(200), text VARCHAR(500) ); """) # обрабатываем старт бота @bot.message_handler(commands=['start']) def start(message): # выводим приветственное сообщение bot.send_message(message.from_user.id, start_txt, parse_mode='Markdown') # обрабатываем команду /now @bot.message_handler(commands=['now']) def start(message): # подключаемся к базе con = sl.connect('reports.db') # получаем сегодняшнюю дату now = datetime.now(timezone.utc) date = now.date() # пустая строка для будущих отчётов s = '' # работаем с базой with con: # выполняем запрос к базе data = con.execute('SELECT * FROM reports WHERE date = :Date;',) # перебираем все результаты for row in data: # формируем строку в общем отчёте s = s + '*' + row[3] + '*' + ' → ' + row[4] + '\n\n' # если отчётов не было за сегодня if s == '': # формируем новое сообщение s = 'За сегодня ещё нет записей' # отправляем общий отчёт обратно в телеграм bot.send_message(message.from_user.id, s, parse_mode='Markdown') # обрабатываем команду /yesterday @bot.message_handler(commands=['yesterday']) def start(message): # подключаемся к базе con = sl.connect('reports.db') # получаем вчерашнюю дату yesterday = datetime.today() - timedelta(days=1) y_date = yesterday.date() # пустая строка для будущих отчётов s = '' # работаем с базой with con: # выполняем запрос data = con.execute('SELECT * FROM reports WHERE date = :Date;',) # смотрим на результат for row in data: # если результат пустой — ничего не делаем if row[0] == 0: pass # если вчера были какие-то отчёты else: # добавляем их в общий список отчётов s = s + '*' + row[3] + '*' + ' → ' + row[4] + '\n\n' # если отчётов не было за вчера if s == '': # формируем новое сообщение s = 'За вчерашний день нет записей' # отправляем пользователю это новое сообщение bot.send_message(message.from_user.id, s, parse_mode='Markdown') # обрабатываем входящий отчёт пользователя @bot.message_handler(content_types=['text']) def func(message): # подключаемся к базе con = sl.connect('reports.db') # подготавливаем запрос sql = 'INSERT INTO reports (datetime, date, id, name, text) values(?, ?, ?, ?, ?)' # получаем дату и время now = datetime.now(timezone.utc) # и просто дату date = now.date() # формируем данные для запроса data = [ (str(now), str(date), str(message.from_user.id), str(message.from_user.username), str(message.text[:500])) ] # добавляем с помощью запроса данные with con: con.executemany(sql, data) # отправляем пользователю сообщение о том, что отчёт принят bot.send_message(message.from_user.id, 'Принято, спасибо!', parse_mode='Markdown') # запускаем бота if __name__ == '__main__': while True: # в бесконечном цикле постоянно опрашиваем бота — есть ли новые сообщения try: bot.polling(none_stop=True, interval=0) # если возникла ошибка — сообщаем про исключение и продолжаем работу except Exception as e: print('❌❌❌❌❌ Сработало исключение! ❌❌❌❌❌')

Что дальше

Теперь у нас есть полноценный бэкенд для телеграм-бота, который умеет принимать и отправлять отчёты. Но сила бэкенда в том, что его можно обернуть в любой интерфейс, в том числе и в веб-страницу. В следующий раз сделаем именно это: сделаем сайт, на котором будут отображаться все отчёты, которые прислали этому телеграм-боту. Получается, что бэкенд будет один, а фронтенда — два. Магия.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *