Appearance
Aiogram 3: Message handler и трюки с текстом
https://habr.com/ru/articles/821661/
Из базы остаётся только разобраться с сообщениями и работой с медиа. После этого можно будет переходить к более сложным и серьёзным темам, таким как: оплаты в боте, мидлвари, fsm-состояния, админ-панели и прочее. Но это всё потом, а сегодня мы рассмотрим следующие темы:
- Объекты Message
- Возможности по отправке текстовых сообщений (
reply
,forward
,answer
) - Возможности работы с текстовыми сообщениями (копирование, удаление, замена текста и клавиатур)
- Форматирование текстовых сообщений (HTML)
- Трюки работы с текстовыми сообщениями
Несмотря на кажущуюся простоту, тема достаточно важная и серьёзная.
Типы сообщений (контента)
В Telegram ботах предусмотрены следующие типы сообщений:
- Стикеры (гифки)
- Видео сообщения
- Голосовые сообщения
- Фото сообщения
- Сообщения с документами
- Сообщения с медиа-группами (самое неприятное для разработчика).
Каждый из этих типов сообщений обрабатывается с помощью Message-хендлера:
- При работе через декораторы записывается как
@dp.message
или@router.message
. - При регистрации записывается как
dp.message.register
.
Сегодня будем рассматривать только текстовые сообщения, а в следующий раз закроем тему с медиа-контентом. Многое будет иметь такую же логику и при работе с медиа (за исключением некоторых моментов).
Так как мы работаем с текстовыми сообщениями, наш тип контента будет TEXT
. Для того чтобы отлавливать такие сообщения, нам необходимо использовать магический фильтр F.text
.
После того как наш магический фильтр вычислил текстовое сообщение, у него появляется достаточно много опций, которые можно делать с объектом сообщения (гораздо больше, чем кажется).
Вот полный список возможностей работы с сообщениями в Telegram
ботах на aiogram 3.x
:
- Достать из сообщения данные (данные о пользователе, ID сообщения, ID чата, где оно было отправлено и прочее)
- Ответить на сообщение при помощи объекта
bot
и при помощи самого объектаmessage
- Скопировать или переслать сообщение
- Удалить сообщение
- Имитировать набор ботом текстового сообщения
- Отформатировать сообщение или достать форматирование сообщения от пользователя
- Сообщение можно закрепить (принцип такой же как закреп в личном чате)
- Изменить/удалить клавиатуру из сообщения
И, обратите внимание, что всё это время мы говорим про текстовые сообщения. А теперь давайте к практике.
Какие данные из сообщения наиболее часто используются в практике?
Чаще всего работаю с такими данными из объекта message
в контексте текстовых сообщений:
- Message.message_id (id сообщения)
- Message.date (дата и время отправки сообщения – полезно для логирования)
- Message.text (текст сообщения)
- Message.html_text (забираем текст с htm-тегами)
- Message.from_user (можно взять данные как: username, first_name, last_name, full_name, is_premium и прочее)
- Message.chat (id, type, title, username/channel)
Давайте попрактикуемся, чтобы было более понятно, что к чему.
Закрепим тему со значениями из объекта сообщения. Давайте представим, что у нас есть задача написать хендлер, который будет реагировать на текстовое сообщение, содержащее слово «охотник». Если бот будет видеть, что кто-то написал такое сообщение, он будет выполнять 2 действия:
- Отвечать на сообщение каким-то текстом (цитатой, обычным ответом, ответом через пересылку сообщения)
- Бот сформирует словарь с такими данными:
- Телеграмм айди пользователя
- Полное имя
- Логин
- id сообщения
- Время отправки сообщения
Далее просто выведем этот словарь в консоль.
python
@start_router.message(F.text.lower().contains('охотник'))
async def cmd_start(message: Message, state: FSMContext):
# отправка обычного сообщения
await message.answer('Я думаю, что ты тут про радугу рассказываешь')
# то же действие, но через объект бота
await bot.send_message(chat_id=message.from_user.id, text='Для меня это слишком просто')
# ответ через цитату
msg = await message.reply('Ну вот что за глупости!?')
# ответ через цитату, через объект bot
await bot.send_message(chat_id=message.from_user.id, text='Хотя, это забавно...',
reply_to_message_id=msg.message_id)
await bot.forward_message(chat_id=message.from_user.id, from_chat_id=message.from_user.id, message_id=msg.message_id)
data_task = {'user_id': message.from_user.id, 'full_name': message.from_user.full_name,
'username': message.from_user.username, 'message_id': message.message_id, 'date': get_msc_date(message.date)}
pprint(data_task)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Давайте посмотрим на результат В консоли:
python
{'user_id': 0000000,
'full_name': 'Alexey Yakovenko',
'username': 'yakvenalexx',
'message_id': 337,
'date': datetime.datetime(2024, 6, 13, 19, 53, 1, tzinfo=TzInfo(UTC))}
1
2
3
4
5
2
3
4
5
Обратите внимание на то, что дата отправки сообщения указывается в формате datetime
в часовом поясе UTC
(координированное всемирное время).
При необходимости конвертации в московское время можно использовать такой подход:
python
import pytz
def get_msc_date(utc_time):
# Задаем московский часовой пояс
moscow_tz = pytz.timezone('Europe/Moscow')
# Переводим время в московский часовой пояс
moscow_time = utc_time.astimezone(moscow_tz)
return moscow_time
'date': get_msc_date(message.date)
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
В таком случае это значение будет передано в таком формате:
python
datetime.datetime(2024, 6, 13, 22, 57, 10, tzinfo=<DstTzInfo 'Europe/Moscow' MSK+3:00:00 STD>)
1
В этом коде мы задействовали несколько методов, но каждый из них выполнил одну задачу: отправку сообщений.
Как вы видели, часть методов использовала за основу объект bot
, а другая message
.
python
# отправка обычного сообщения
await message.answer('Я думаю, что ты тут про радугу рассказываешь')
# ответ через цитату
msg = await message.reply('Ну вот что за глупости!?')
1
2
3
4
5
2
3
4
5
Эти два метода достаточно удобны и лаконичны. Основная их особенность в том, что они не требуют обязательного указания id, кому необходимо сделать отправку и на какое сообщение – это всё уже скрыто в самом методе answer
и reply
.
Советую там, где это возможно, использовать именно эти методы.
Такие методы, как bot.send_message
, bot.send_message
с флагом reply_to_message_id
и bot.forward_message
заслуживают больше внимания, как и эта запись:
python
msg = await message.reply('Ну вот что за глупости!?')
1
Когда происходит отправка сообщения при помощи bot
, всегда обязательным параметром будет указание, кому необходимо отправить сообщение. В контексте моего примера:
python
await message.answer('Я думаю, что ты тут про радугу рассказываешь')
await bot.send_message(chat_id=message.from_user.id, text='Для меня это слишком просто')
1
2
3
2
3
Выполнили аналогичные действия с одинаковым результатом, в связи с чем не было смысла выносить это в отдельный объект, но бывают ситуации когда отправку через message.answer
сделать невозможно.
Рассмотрим эту ситуацию на примере рассылки в телеграмм боте из админ-панели.
- Администратор пишет сообщение
- Выбирает отправить всем
- Бот забирает с базы данных айди сообщений
- При помощи
bot.send_message
производит рассылку.
Дела с методом bot.forward_message
и bot.send_message
, когда явно указываем на какое сообщение нужно дать ответ, уже чуть сложнее. Основная трудность тут появляется в обязательном указании message_id
(идентификатора сообщения, на которое нужно ответить/переслать).
Но тут есть один очень большой плюс. Когда вы знаете ID сообщения, у вас появляются практически неограниченные возможности по работе с ним (когда бот является владельцем этого сообщения - иначе нужно прибегнуть к трюкам о которых расскажу далее). К примеру, вы можете постоянно перезаписывать одно и то же сообщение, имитируя набор текста и создавая непрерывную анимацию (наверняка вы с таким сталкивались).
Думаю, это будет проще объяснить на конкретном примере. Я продемонстрирую вам, как делать ботом имитацию набора текста.
Конструкция для имитации набора текста будет выглядеть так:
python
from aiogram.utils.chat_action import ChatActionSender
async with ChatActionSender(bot=bot, chat_id=message.from_user.id, action="typing"):
await asyncio.sleep(2)
1
2
3
4
2
3
4
Пауза нужна для того, чтобы отправка текстового сообщения не прошла мгновенно. Тут есть одна интересная особенность. Конструкция, которую я прописал выше, отвечает только за имитацию набора текста. То есть вам ничего не мешает чередовать имитацию с асинхронными паузами await asyncio.sleep
, создавая полноценную имитацию общения с живым человеком (знаете, когда вам печатают, отвлекаются, а потом снова продолжают печатать).
Сейчас я покажу вам, как можно изменить текст сообщения со временем. Тут важно понимать, что бот может менять только те сообщения, которые сам отправил. Но и тут есть один интересный трюк, которым я с вами поделюсь.
Давайте создадим хендлер, который будет реагировать на команду /test_edit_msg
.
python
@start_router.message(Command('test_edit_msg'))
async def cmd_start(message: Message, state: FSMContext):
# Бот делает отправку сообщения с сохранением объекта msg
msg = await message.answer('Отправляю сообщение')
# Достаем ID сообщения
msg_id = msg.message_id
# Имитируем набор текста 2 секунды и отправляем какое-то сообщение
async with ChatActionSender(bot=bot, chat_id=message.from_user.id, action="typing"):
await asyncio.sleep(2)
await message.answer('Новое сообщение')
# Делаем паузу ещё на 2 секунды
await asyncio.sleep(2)
# Изменяем текст сообщения, ID которого мы сохранили
await bot.edit_message_text(text='<b>Отправляю сообщение!!!</b>', chat_id=message.from_user.id, message_id=msg_id)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
В коде оставлены комментарии. Нужно обратить внимание, на следующую строку:
python
await bot.edit_message_text(text='<b>Отправляю сообщение!!!</b>', chat_id=message.from_user.id, message_id=msg_id)
1
Во-первых, мы задействовали новый метод — edit_message_text
. Он принимает новый текст, чат, в котором нужно изменить сообщение, и, самое главное, ID сообщения. После изменения текста сообщения его ID не меняется. Это говорит о том, что пока сообщение существует, его можно сколько угодно раз заменять. Однако будьте осторожны с этим методом: если пользователь удалит сообщение, бот просто упадет при попытке его изменить.
Без обработки ошибки ловим:
python
aiogram.exceptions.TelegramBadRequest: Telegram server says - Bad Request: message to edit not found
1
Так что будьте внимательны!
Похожую ошибку мы получим, если попытаемся изменить чужое сообщение. Допустим, мы будем менять сообщение с командой /test_edit_msg
:
python
@start_router.message(Command('test_edit_msg'))
async def cmd_start(message: Message, state: FSMContext):
# Бот пытается изменить сообщение, которое не отправлял
await bot.edit_message_text(text='<b>Отправляю сообщение!!!</b>', chat_id=message.from_user.id, message_id=message.message_id)
1
2
3
4
2
3
4
Получим:
python
aiogram.exceptions.TelegramBadRequest: Telegram server says - Bad Request: message can't be edited
1
Тут бот скажет, что сообщение нельзя изменить, и это правда, ведь бот не является автором этого сообщения. А теперь посмотрите на трюк, который можно тут сделать:
- Бот присвоит это сообщение себе (скопирует).
- Удалит сообщение от пользователя.
- Перезапишет уже скопированное сообщение и отправит.
Смотрим:
python
@start_router.message(Command('test_edit_msg'))
async def cmd_start(message: Message, state: FSMContext):
new_msg = await bot.copy_message(
chat_id=message.from_user.id,
from_chat_id=message.from_user.id,
message_id=message.message_id
)
await message.delete()
await bot.edit_message_text(
text='<b>Отправляю сообщение!!!</b>',
chat_id=message.from_user.id,
message_id=new_msg.message_id
)
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
Обратите внимание, что тут мы задействовали новые методы new_msg = await bot.copy_message
и await message.delete()
. У метода await message.delete()
есть аналог из метода bot
. Записывается так:
python
await bot.delete_message(chat_id=message.from_user.id, message_id=message.message_id)
1
Как вы понимаете, выполняет то же действие, но с более громоздкой записью. Иногда бывает очень полезно.
Немного про клавиатуры
Сообщения, в частности текстовые, могут сопровождаться тем или иным типом клавиатур. Посмотрим, как удалить клавиатуру и как заменить клавиатуру в сообщении.
Для начала давайте импортируем метод для удаления клавиатур (работать будет как на текстовые и на инлайн клавиатуры):
python
from aiogram.types import ReplyKeyboardRemove
1
Этот метод нужно использовать, когда по сценарию пользователь переходит от одного состояния к другому. Например, текстовая клавиатура с выбором пола «Мужской» и «Женский». Он делает выбор, а после его ждет новый вопрос, например, «Укажите год рождения».
Если не удалим клавиатуру (не заменим её на другую в рамках сценария), клавиатура так и будет висеть. У новичков бывает такая проблема, когда пользователь выполнил сценарий, а на одном из этапов был выбор города. После этого клавиатура с выбором города преследует его.
Не допускайте такого. Тут всё просто: вместо указания клавиатуры в методе reply_markup
передавайте ReplyKeyboardRemove()
. Это ещё обязательно рассмотрим в теме FSM
.
Иногда клавиатуру нужно заменить в рамках одного конкретного сообщения, например, со временем. Изменить клавиатуру в сообщении (удалить) можно несколькими способами.
Тут используем знакомый нам
edit_message_text
. Переписываем всё сообщение и прямо к нему привязываем клавиатуру.pythonawait bot.edit_message_text(chat_id=message.chat.id, message_id=msg.message_id, text='Пока!', reply_markup=inline_kb())
1Обратите внимание: данный метод ожидает именно инлайн клавиатуру. Передав в этом методе текстовую клавиатуру, вы получите:
pythoninput_value=ReplyKeyboardMarkup(keybo...ню:', selective=None), input_type=ReplyKeyboardMarkup] For further information visit https://errors.pydantic.dev/2.7/v/model_type
1Но заменять инлайн клавиатуры в рамках одного
message_id
можно до бесконечности.Тут мы заменили только клавиатуру, оставив текст без изменений.
python
await bot.edit_message_reply_markup(chat_id=message.chat.id, message_id=msg.message_id, reply_markup=inline_kb())
1
Как же нам добавить текстовую клавиатуру к сообщению?
К сожалению, прямого метода замены нет, но ничего не мешает использовать костыль в виде копирования сообщения, верно?
python
@start_router.message(Command('test_edit_msg'))
async def cmd_start(message: Message, state: FSMContext):
msg = await message.answer('Привет!')
await asyncio.sleep(2)
old_text = msg.text
await msg.delete()
await message.answer(old_text, reply_markup=main_kb(message.from_user.id))
1
2
3
4
5
6
7
2
3
4
5
6
7
Вот такая вышла незамысловатая конструкция. Для того чтобы так не изощряться, стараюсь отдавать преимущество инлайн клавиатурам и вам того советую. Ну а текстовые кнопки учитесь вплетать в сценарии, чтобы они логичным образом появлялись и исчезали (перезаписывались другими текстовыми клавиатурами или удалялись через метод ReplyKeyboardRemove()
).
Форматирование текста
Скажу честно, никогда не использую Markdown форматирование в работе с aiogram 3 и сейчас объясню почему.
- Во-первых, как по мне, синтаксис там неудобный.
- Во-вторых, на старте aiogram 3 вокруг Markdown форматирования крутилось много багов, и те, кто переводили свои проекты на тройку, сильно страдали из-за того, что выбрали Markdown.
Далее будет говорить про форматирование на примере HTML, но можете использовать тот тип, который более приятен.
Для того чтобы к каждому своему сообщению не передавать parse_mode="HTML"
при инициации бота, советую вам использовать такую конструкцию:
python
from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
bot = Bot(token=config('TOKEN'), default=DefaultBotProperties(parse_mode=ParseMode.HTML))
1
2
3
4
2
3
4
В данном случае конструкция DefaultBotProperties(parse_mode=ParseMode.HTML)
установит HTML форматирование по умолчанию к каждому сообщению.
Давайте рассмотрим основные HTML-теги, которые можно использовать в форматировании:
html
<b>Жирный</b>
<i>Курсив</i>
<u>Подчеркнутый</u>
<s>Зачеркнутый</s>
<tg-spoiler>Спойлер (скрытый текст)</tg-spoiler>
<a href="http://www.example.com/">Ссылка в тексте</a>
<code>Код с копированием текста при клике</code>
<pre>Спойлер с копированием текста</pre>
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
Так форматированный текст выглядит в боте.
Используйте их внимательно, так как есть одна очень неприятная штука. Некоторые пользователи решают использовать < и > в своих логинах или в отправляемых боту сообщениях.
Если в коде включен режим HTML форматирования текста, бот ловит ошибку:
python
aiogram.exceptions.TelegramBadRequest: Telegram server says - Bad Request: can't parse entities: Unsupported start tag "<>"
1
Используем html.escape
для решения этой проблемы. Делаем импорт:
python
from html import escape
1
А далее те места, где может встретиться некорректный тег, просто прогоняем через метод escape
.
Ну и последнее, захват отформатированного текста. Сейчас дам пример, чтобы вы сразу поняли (реальная практика).
В админке добавлял возможность создания постов с отформатированным текстом (с тегами HTML). Клиент перед отправкой форматировал текст (просто через стандартные методы Telegram: выделил мышкой, выбрал «Жирное» и так далее). После этих действий отформатированный текст отправлялся боту, а тот захватывал HTML при помощи message.html_text
, а после делал запись в базу данных именно с тегами. Далее, при отображении поста, бот просто брал текст с HTML
тегами и отправлял уже отформатированный текст. Такой себе карманный HTML
редактор в Telegram
.
Вот простой пример:
python
@start_router.message(Command('test_edit_msg'))
async def cmd_start(message: Message, state: FSMContext):
msg = await message.answer('<b>ПРИВЕТ!</b>')
print(msg.html_text)
1
2
3
4
2
3
4
Бот в консоли вывел:
html
<b>ПРИВЕТ!</b>
1
Хотя мы не сохраняли нигде переменной с HTML тегом.