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

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

Часть 2: продолжаем работать с бэкендом для пользы дела

Недавно мы рассказали о том, что такое бэкенд и зачем он нужен в современных проектах. Вот короткая версия:

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

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

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

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

  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;',{'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')

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

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

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

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

Чтобы получить вчерашнюю дату, используют функцию 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;',{'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')

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

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

Результат

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

  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;',{'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('❌❌❌❌❌ Сработало исключение! ❌❌❌❌❌')

Что дальше

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

Текст:

Михаил Полянин

Редактор:

Максим Ильяхов

Художник:

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

Корректор:

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

Вёрстка:

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

Соцсети:

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

Это первый шаг в Python-разработке
Мы сделали первый этап проекта на Python. Дальше будет ещё. Но пока новая статья не вышла, можно бесплатно начать курс для Python-разработчика в Практикуме.
Начать бесплатно
Это первый шаг в Python-разработке Это первый шаг в Python-разработке Это первый шаг в Python-разработке Это первый шаг в Python-разработке
Получите ИТ-профессию
В «Яндекс Практикуме» можно стать разработчиком, тестировщиком, аналитиком и менеджером цифровых продуктов. Первая часть обучения всегда бесплатная, чтобы попробовать и найти то, что вам по душе. Дальше — программы трудоустройства.
Начать карьеру в ИТ
Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию
Еще по теме
medium