Три способа ответить
До сих пор бот отвечал одним способом — message.answer: текст уходит в тот же чат, откуда пришло сообщение. Это самый частый ответ, но не единственный.
message.reply отправляет ответ с цитатой — над ним видно, на какое сообщение отвечают. В личке это почти не нужно, а в группе очень полезно: когда боту пишут пятеро, цитата показывает, кому именно он ответил. Правило: в группах — reply, в личке — answer.
Третий способ — bot.send_message(chat_id, text). Он отправляет текст в любой чат по номеру. Нужен он там, где входящего сообщения нет: бот напоминает о записи за час, сообщает владельцу о новой заявке, рассылает новость. Объект bot доступен в обработчике: достаточно добавить параметр с этим именем, и библиотека сама его подставит.
OWNER_ID = 58201937
@dp.message(Command("zapis"))
async def zapis(message: Message, bot: Bot):
await message.answer("Вы записаны на завтра, 15:00.")
await bot.send_message(OWNER_ID, f"Новая запись: {message.from_user.first_name}")
Здесь человек получает подтверждение в свой чат, а владелец — уведомление в свой. Помните правило первого урока: send_message сработает, только если владелец когда-то сам запустил бота. Иначе Телеграм откажет — бот не может писать первым.
Каждый способ возвращает отправленное сообщение. Его можно сохранить в переменную и потом изменить — например, заменить «Ищу…» на результат методом edit_text — или удалить методом delete. Так бот не засыпает чат промежуточными сообщениями.
Оформление и escape
Текст без оформления читается тяжело. Телеграм умеет жирный, курсив, моноширинный шрифт и ссылки — для этого у сообщения задают parse_mode. Самый надёжный режим — HTML: оформление пишется знакомыми тегами.
await message.answer(
"<b>Запись подтверждена</b>\n"
"Дата: <i>завтра, 15:00</i>\n"
"Адрес: <a href='https://example.com/map'>на карте</a>",
parse_mode="HTML",
)
Работают теги <b> (жирный), <i> (курсив), <u> (подчёркнутый), <s> (зачёркнутый), <code> (моноширинный — удобно для номеров заказов, которые копируют) и <a href> (ссылка). Абзацы, списки и заголовки, как на сайте, не работают: перенос строки — это просто «\n».
Чтобы не писать parse_mode в каждом ответе, его задают боту сразу для всех сообщений:
from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
bot = Bot(token=TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
Теперь главное. Если в текст попадает то, что прислал человек — его имя, адрес, комментарий, — его нужно экранировать. Представьте, что человек назвался «Аня <3». Знак «<» Телеграм примет за начало тега, не найдёт его конца и откажется отправлять сообщение с ошибкой разбора. Бот, который падает на имени пользователя, выглядит нелепо, а иногда так ломают бота и нарочно.
from html import escape
name = escape(message.from_user.first_name)
await message.answer(f"Здравствуйте, <b>{name}</b>!")
escape превращает опасные знаки в безопасные записи, и в сообщении они отобразятся как есть. Правило простое: всё чужое, что вставляется в текст с оформлением, проходит через escape. Своё — заголовки и подписи, которые вы пишете сами, — экранировать не нужно.
Длина и частота
У сообщения есть предел длины — 4096 знаков. Текст длиннее Телеграм не примет, и бот получит ошибку. Обычно это всплывает внезапно: список заказов рос-рос и однажды не влез.
| что | предел |
|---|---|
| текст сообщения | 4096 знаков |
| подпись под фото или файлом | 1024 знака |
| текст команды | 32 знака |
Длинный текст режут на части. Резать посередине слова или строки некрасиво, поэтому режут по переносам строк: набирают строки, пока следующая не переполнит часть, и начинают новую. Но лучше всего вовсе не доводить до четырёх тысяч: человек не прочитает сообщение на три экрана. Список из двухсот заказов показывают страницами по десять, а не простынёй.
Обратная ошибка — много коротких сообщений подряд. Бот, который на один вопрос отвечает пятью сообщениями, раздражает, а в группах ещё и упирается в ограничения Телеграма на частоту (об этом девятый урок). Если ответ естественно делится на пункты, это один ответ со списком, а не пять ответов.
Когда ответ готовится долго — бот ищет в базе, обращается к сайту, — человек видит тишину и начинает писать снова. Показать, что бот работает, можно двумя способами. Первый — статус «печатает…» вверху чата:
await bot.send_chat_action(message.chat.id, "typing")
Статус держится около пяти секунд. Второй способ — сразу отправить короткое «Ищу…», а потом заменить его результатом через edit_text. Для ответов дольше нескольких секунд второй способ лучше: человек видит, что запрос принят, и не отправляет его повторно.
Фото и файлы
Картинка часто объясняет лучше текста: меню, схема проезда, карточка товара. Фото отправляют методом answer_photo. Взять его можно из файла на диске, по ссылке из интернета или по номеру файла в Телеграме.
from aiogram.types import FSInputFile
@dp.message(Command("menu"))
async def menu(message: Message):
photo = FSInputFile("menu.jpg")
await message.answer_photo(photo, caption="Меню на сегодня")
Когда фото отправлено один раз, Телеграм хранит его у себя и возвращает file_id — номер файла. Если отправлять то же фото снова по file_id, его не нужно загружать заново: отправка мгновенная, трафик не тратится. Для меню, которое бот показывает сто раз в день, это заметная экономия. Номер файла привязан к боту: другой бот по нему фото не отправит.
sent = await message.answer_photo(FSInputFile("menu.jpg"))
menu_id = sent.photo[-1].file_id # сохранить и дальше слать по нему
await message.answer_photo(menu_id)
Фото, которое прислал человек, приходит в message.photo — и это не одно фото, а список размеров. Телеграм сам делает уменьшенные копии, от маленькой к большой.
Для предпросмотра хватит маленькой, а если фото нужно сохранить или обработать — берут последнюю, photo[-1], самую крупную. Скачать её можно методом bot.download. Подпись, которую человек добавил к фото, лежит не в text, а в caption — ещё одна причина, почему у фото text пустой.
У файлов свои пределы: отправлять бот может файлы до 50 МБ, а скачивать присланные — до 20 МБ. Для большинства задач этого хватает; видео и большие архивы через обычного бота не передают.