Добавляем веб-интерфейс к телеграм-боту
medium

Добавляем веб-интерфейс к телеграм-боту

Показываем, как один бэкенд может обслуживать разные фронтенды

Продолжаем показывать, как работают вместе бэкенд и фронтенд. У нас уже вышло про это две статьи — как сделать простой проект телеграм-бота и как запустить с ним полноценный бэкенд. Вот краткое содержание:

  • У нас есть скрипт на Python, который играет роль бэкенда — он записывает некие данные в базу и потом их выводит. 
  • Скрипт получает данные из фронтенда — Телеграма. В Телеграме для этого работает бот, а мостиком между Телеграмом и нашим скриптом служит специальная библиотека Python. 
  • На фронтенде пользователь отчитывается, что он сделал за сегодня. Эти отчёты прилетают на бэкенд, наш скрипт их принимает и записывает в базу данных.
  • По специальной команде скрипт выплёвывает то, что у него сейчас есть в базе данных. Выплёвывает в бота.

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

Что делаем

  1. Веб-страницу, внутри которой будет работать второй фронтенд.
  2. Внутри неё — PHP-скрипт, который будет получать данные из бэкенда.

Почему PHP? Потому что он подходит для этой простой задачи: вывести все данные из базы один раз.

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

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

# подключаем модуль для Телеграма
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;',{'Date': str(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;',{'Date': str(y_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('❌❌❌❌❌ Сработало исключение! ❌❌❌❌❌')

Запускаем скрипт бэкенда на сервере

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

Мы можем запустить скрипт на сервере — то есть на чужом компьютере, который мы арендуем за деньги. Разница в том, что сервер всё время работает и редко перезагружается. 

Для запуска скрипта на сервере нам потребуется собственно арендованный виртуальный сервис. А дальше воспользуемся рецептом из нашей старой статьи про запуск бота:

  1. Запускаем SSH-клиент и входим на наш сервер как администраторы.
  2. В SSH-терминале пишем по очереди такие команды (вместо .thecode можно написать название каталога, которое вам по душе):
    virtualenv .thecode
    source .thecode/bin/activate
  3. Установим Python-модуль для работы с Телеграмом:
    pip install pytelegrambotapi
  4. Когда установка закончится, пишем такую команду (не забывайте поменять путь к файлу на свой):
    python3 osebe/public_html/projects/front/backend.py
  5. Если хотите, чтобы после закрытия SSH-клиента или терминала бот продолжал работать, в последней команде в начале напишите nohup

Проверим работу бота после запуска на сервере:

Бот реагирует на команды и запоминает то, что мы ему пишем, значит, бот на сервере работает как нужно.

По идее нам нужно было отправить бота в защищённую папку, например cgi-bin, чтобы никто не мог получить доступ к базе. Но для простоты проекта мы не стали тратить на это время — сделаем это отдельно и расскажем, как это работает.

Создаём страницу

Наш новый фронт — это веб-страница, поэтому напишем простой код, который создаст обычную страницу с заголовком. Создадим новый файл front.php (не HTML, это важно!) и запишем в него такой код. Это весь HTML-код, который нам понадобится, — всё остальное будем делать на PHP. Если не знаете, что такое PHP, — вот подборка для начала:

А вот код страницы:

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<meta name="viewport" content="width=device-width, initial-scale=1">
	<!-- заголовок страницы -->
	<title>Простой фронтенд к базе данных</title>
</head>
<body>
	<!-- общий заголовок -->
	<h1>Отчёты</h1>
</body>
</html>

Теперь кладём этот файл на сервер в ту же папку, где и python-скрипт, чтобы всё было в одном месте, и открываем страницу в браузере.

Мы не указали ничего кроме заголовка первого уровня, поэтому страница выглядит пусто

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

Сразу после заголовка добавляем PHP-вставку, вся магия будет происходить внутри.

<!-- начало PHP-блока -->
<?php
// конец PHP-блока
?>

Чтобы подключиться к базе данных, используем команду SQLite3() и сразу выведем сообщение об успешном подключении. Если мы его увидим — всё работает, можно двигаться дальше.

// подключаемся к базе данных
$connection = new SQLite3('reports.db');
// если подключились
if($connection){
	// выводим сообщение
	echo "<p>✅ Подключились к базе данных</p>";
}

Выводим отчёты

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

  1. Получить дату в нужном формате.
  2. Выгрузить все сегодняшние отчёты.
  3. Посмотреть, есть ли они вообще, если нет — выдать сообщение, что их нет.
  4. Если есть — вывести даты и отчёты.
  5. То же самое сделать для отчётов за остальные дни.

Запишем это всё на PHP — мы добавили комментарий к каждой строчке, чтобы было понятнее, как это всё работает:

// получаем дату в нужном формате
$date = date('Y-m-d');
// выводим подзаголовок
echo '<h2>За сегодня</h2>';
// получаем сегодняшние отчёты из базы
$results = $connection->query("SELECT * FROM 'reports' WHERE 'date' == $date");
// получаем количество отчётов
$row=$results->fetchArray(SQLITE3_ASSOC);
// если они есть
if ($row != false) {
	// заново получаем отчёты из базы
	$results = $connection->query("SELECT * FROM 'reports' WHERE 'date' == $date");
	// пока есть записи в результатах поиска — выводим их
	while($row=$results->fetchArray(SQLITE3_ASSOC)){
		echo '<strong>' . $row['date'] . '</strong><br>';
		echo 'Ник:		' . $row['name'] . '<br>';
		echo 'Отчёт:	' . $row['text'] . '<br>';
		echo '<br>';
	}
// если отчётов нет
} else {
	echo "<p>За сегодня записей нет</p>";
}

// выводим подзаголовок
echo '<h2>Остальные отчёты</h2>';
// выгружаем остальные отчёты из базы
$results = $connection->query("SELECT * FROM 'reports' WHERE 'date' != $date");
// пока есть записи в результатах поиска — выводим их
while($row=$results->fetchArray(SQLITE3_ASSOC)){
	echo '<strong>' . $row['date'] . '</strong><br>';
	echo 'Ник:		' . $row['name'] . '<br>';
	echo 'Отчёт:	' . $row['text'] . '<br>';
	echo '<br>';
}

Добавляем этот код в PHP-блок после подключения к базе, заливаем файл front.php на сервер и обновляем страницу:

Получается, что у нашего телеграм-бота появился второй фронтенд, который берёт те же данные, что и бот, но отображает их в другом месте и в другом формате.

Что дальше

Технически у нас всё сделано:

  • есть бэкенд на сервере, который принимает отчёты и сохраняет их в базе;
  • есть фронтенд — телеграм-бот со стандартным интерфейсом;
  • есть ещё один фронтенд — веб-страница со всеми отчётами;
  • всё это работает на сервере без нашего участия.

Но внутри много шероховатостей: всё ещё неоптимизированный код, небезопасное расположение базы, нет кнопок выбора дня на странице и так далее. Это можно исправлять в будущем.

Художник:

Алексей Сухов

Корректор:

Ирина Михеева

Вёрстка:

Кирилл Климентьев

Соцсети:

Виталий Вебер

Получите ИТ-профессию
В «Яндекс Практикуме» можно стать разработчиком, тестировщиком, аналитиком и менеджером цифровых продуктов. Первая часть обучения всегда бесплатная, чтобы попробовать и найти то, что вам по душе. Дальше — программы трудоустройства.
Вам может быть интересно
medium
[anycomment]
Exit mobile version