Недавно мы рассказали о том, что такое бэкенд и зачем он нужен в современных проектах. Вот короткая версия:
- Есть фронтенд, который отвечает за внешнюю и визуальную часть. Обычно под фронтендом мы понимаем сайт. Но также им может быть приложение или чат-бот.
- Есть бэкенд, который отвечает за логику работы и хранение данных. Обычно бэкенд — это программа на сервере, которая обрабатывает запросы с фронтенда. Но также эта программа может работать на нашем компьютере.
- Если мы изменим что-то в бэкенде, то сервис станет работать немного иначе, при этом интерфейс останется тем же самым.
- Наоборот тоже работает: если мы изменим фронт, а бэкенд оставим как есть, то сервис будет работать точно так же, как и раньше, только в новом дизайне.
Сегодня мы продолжим этот проект и добавим возможности нашему бэкенду для телеграм-бота. Если хотите разобраться в теме телеграм-ботов с самого начала, почитайте эти статьи:
Логика проекта
Мы сделаем простого бота для приёма отчётов — что сделано за день или над чем идёт работа:
- Сообщение может написать боту кто угодно. Бот запомнит, кто ему написал.
- Бот будет работать только с текстовыми сообщениями. Стикеры и файлы не подойдут.
- Чтобы никто не спамил, бот обрезает сообщение до 500 символов и записывает их в свою базу данных.
- База данных будет настоящая — SQLite, с которой Python работает без дополнительных настроек.
- У бота будут две команды — посмотреть все отчёты за вчера и за сегодня. По этим командам бот присылает отчёты.
- Другие команды бот игнорирует, удалить отчёт нельзя, бот помнит всё.
Это простая, но рабочая логика бэкенда — с ним бот уже может приносить пользу и быть рабочим инструментом.
Что понадобится
Для скрипта нам понадобится 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 доступной для всех. Когда бот её получит, он отправит в ответ все отчёты за сегодня.
Логика поиска и выводов отчёта будет такая:
- Подключаемся к базе.
- Получаем текущую дату и ищем в базе все записи с этой датой.
- Перебираем результаты и формируем из них сообщение с отчётами.
- Отправляем общий отчёт пользователю.
При этом нам ещё нужно предусмотреть ситуацию, когда отчётов сегодня не было. Если этого не сделать, скрипт попытается отправить пустое сообщение и выдаст ошибку. В этом случае нам нужно дополнительно проверить, пустая у нас строка с отчётом или нет. Если пустая — записать в неё текст о том, что за сегодня отчётов не было.
# обрабатываем команду /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')
Так как мы только начали работать с ботом, вчерашних сообщений в нём ещё нет, поэтому в ответ на эту команду бот выдаст заготовленный текст:
Результат
Смотрите, что мы сегодня сделали:
- Подключили бэкенд в виде скрипта к фронтенду — телеграм-боту.
- После этого бот смог обрабатывать наши сообщения и команды — запоминать отчёты и отправлять их, когда нужно.
- Мы не трогали фронт, зато все наши изменения в бэкенде приводили к тому, что бот получал новые функции одну за другой.
Так и работает бэкенд: он почти не влияет на интерфейс, зато от него может полностью зависеть логика проекта. Если хотите делать крутой бэкенд и разбираться в этом — приходите в Практикум на курсы бэкенда, там научат всему, что потребуется.
Что нужно доделать в следующий раз
Сейчас бот работает и справляется с задачами, но есть пара моментов, которую нужно будет потом исправить:
- Если отчётов за день станет слишком много, возможно, бот не сможет отправить их в одном сообщении или не сможет отправить в принципе. Нужно добавить проверку на максимальное количество символов, и, если что, — делить одно сообщение на несколько.
- Код вывода сообщений за вчера и за сегодня — одинаковый за исключением нескольких строк. Нужно вынести общую часть в отдельную функцию, чтобы не плодить спагетти-код.
Попробуйте сделать это сами или дождитесь следующей статьи с продолжением, где мы исправим обе эти проблемы.
# подключаем модуль для Телеграма
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('❌❌❌❌❌ Сработало исключение! ❌❌❌❌❌')
Что дальше
Теперь у нас есть полноценный бэкенд для телеграм-бота, который умеет принимать и отправлять отчёты. Но сила бэкенда в том, что его можно обернуть в любой интерфейс, в том числе и в веб-страницу. В следующий раз сделаем именно это: сделаем сайт, на котором будут отображаться все отчёты, которые прислали этому телеграм-боту. Получается, что бэкенд будет один, а фронтенда — два. Магия.