В aiogram 3 бот отправляет сообщение тремя способами: message.answer() пишет в тот же чат, message.reply() пишет в тот же чат ответом с цитатой, bot.send_message(chat_id, text) пишет в любой чат по его номеру. Редактируют сообщение через message.edit_text() или bot.edit_message_text(), удаляют через message.delete(). Ниже каждый метод с примером и три готовых бота, которые можно запустить.
Примеры проверены на aiogram 3.x (версия 3.31.0) и Python 3.14.
Если у вас старый код с executor.start_polling и @dp.message_handler, это aiogram 2. В третьей версии он не запустится: обработчики живут в роутере, а запуск идёт через asyncio.run(). Все примеры ниже написаны под третью версию.
Объект Message: что приходит боту
Каждое входящее сообщение aiogram превращает в объект Message из модуля aiogram.types. Через него бот узнаёт, кто написал, куда и что. У этого же объекта есть методы, чтобы ответить, отредактировать или удалить сообщение.
Поля, которые нужны чаще всего:
message_id— номер сообщения внутри чата. Нужен, чтобы потом отредактировать или удалить именно его.from_user— отправитель, объектUser(имя, ник, id).chat— чат, где появилось сообщение, объектChat. Егоchat.idпередают вsend_message.date— дата и время отправки.text— текст. Для фото, стикера или документа здесьNone, подпись к фото лежит вcaption.entities— разметка внутри текста: упоминания, ссылки, хэштеги, команды.reply_to_message— сообщение, на которое ответил пользователь.forward_origin— откуда переслано сообщение. В старых версиях API для этого были поляforward_fromиforward_from_chat.reply_markup— клавиатура, прикреплённая к сообщению.
Методы: answer(), reply(), edit_text(), delete(), а также answer_photo(), answer_document() и другие для медиа. Подробнее про Message, User и Chat — в статье про модуль aiogram.types.
Как отправить сообщение
bot.send_message: в любой чат по номеру
Метод принимает номер чата (chat_id) и текст. Подходит, когда бот пишет сам, без входящего сообщения: напоминание, рассылка, уведомление администратору.
await bot.send_message(chat_id, "Привет, мир!")
Писать первым бот может только тому, кто уже нажал «Старт» в диалоге с ним. Иначе Telegram вернёт ошибку.
message.answer: в тот же чат
Короткая запись для частого случая. Номер чата aiogram берёт из входящего сообщения сам.
await message.answer("Спасибо за ваше сообщение!")
message.reply: ответ с цитатой
Тоже пишет в тот же чат, но новое сообщение привязано к исходному. Над ответом видна цитата, по нажатию на неё Telegram прокручивает к оригиналу. В группах это помогает понять, кому бот отвечает.
await message.reply("Спасибо за ваше сообщение!")
Ответ на конкретное сообщение через send_message
Если нужно ответить на сообщение с известным номером, передайте его в reply_parameters. Старый параметр reply_to_message_id в aiogram 3 пока принимается, но в документации Bot API его заменил reply_parameters.
from aiogram.types import ReplyParameters
await bot.send_message(
chat_id,
"Ответ на ваше сообщение",
reply_parameters=ReplyParameters(message_id=message_id),
)
Разница между send_message, answer и reply
send_message отправляет в чат, который вы укажете. answer отправляет в тот чат, откуда пришло сообщение, без цитаты. reply отправляет туда же, но с цитатой исходного сообщения. На скриншоте сверху ответ через answer, снизу через reply:
Разметка текста: жирный, курсив, ссылки
Чтобы теги <b> и <i> в тексте превращались в оформление, боту задают режим разметки (parse mode). В aiogram 3 это делают один раз при создании бота через DefaultBotProperties:
from aiogram import Bot
from aiogram.client.default import DefaultBotProperties
from aiogram.enums import ParseMode
bot = Bot(token=token, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
Раньше режим передавали прямо в Bot(parse_mode=...). В aiogram 3.7 и новее так писать нельзя: бот не создастся.
Готовый бот: /start, /help и эхо
Бот здоровается на /start, показывает справку на /help и повторяет любой другой текст. Токен (ключ бота от BotFather) лежит в переменной окружения BOT_TOKEN, а не в коде: так он не попадёт в репозиторий или на скриншот.
import asyncio
import os
from aiogram import Bot, Dispatcher, F, Router
from aiogram.filters import Command, CommandStart
from aiogram.types import Message
router = Router()
@router.message(CommandStart())
async def handle_start(message: Message):
await message.reply("Привет! Я бот. Как дела?")
@router.message(Command("help"))
async def handle_help(message: Message):
await message.answer(
"Что я умею:\n"
"/start — поздороваться\n"
"/help — показать эту справку\n"
"Любой другой текст я повторю."
)
@router.message(F.text)
async def handle_text(message: Message):
await message.reply(f"Вы отправили сообщение: {message.text}")
async def main():
bot = Bot(token=os.environ["BOT_TOKEN"])
dp = Dispatcher()
dp.include_router(router)
await bot.delete_webhook(drop_pending_updates=True)
await dp.start_polling(bot)
if __name__ == "__main__":
asyncio.run(main())
Что здесь от aiogram 3. Router (роутер) — набор обработчиков, его подключают к диспетчеру через dp.include_router(). Декоратор @router.message(...) говорит «эта функция обрабатывает сообщения». В скобках фильтр: CommandStart() ловит /start, Command("help") ловит /help, F.text ловит любое сообщение с текстом. Порядок важен: aiogram проверяет обработчики сверху вниз, поэтому эхо стоит последним. delete_webhook(drop_pending_updates=True) выбрасывает сообщения, которые пришли, пока бот был выключен.
Запуск в Windows: set BOT_TOKEN=ваш_токен, затем python bot.py. В Linux и macOS: export BOT_TOKEN=ваш_токен.
Как отредактировать сообщение
Бот может менять только свои сообщения. Чужие, в том числе сообщения пользователя, он не отредактирует. Обычно редактируют то, что бот отправил чуть раньше: answer() возвращает объект отправленного сообщения, у него и вызывают edit_text().
sent = await message.answer("Считаю...")
await sent.edit_text("Готово: 42")
Если объекта под рукой нет, но известны номер чата и номер сообщения, подойдёт метод бота:
await bot.edit_message_text("Новый текст", chat_id=chat_id, message_id=message_id)
edit_text меняет только текст. У фото и документа меняют подпись через edit_caption(), а кнопки под сообщением — через edit_reply_markup().
Ошибка «message is not modified»
Если отправить на редактирование тот же текст и те же кнопки, Telegram ответит ошибкой TelegramBadRequest: message is not modified. Так бывает, когда пользователь дважды нажал одну кнопку. Ошибку ловят и пропускают:
from aiogram.exceptions import TelegramBadRequest
try:
await sent.edit_text("Готово: 42")
except TelegramBadRequest as e:
if "message is not modified" not in str(e):
raise
Готовый бот: счётчик на кнопках
Самый частый случай редактирования — нажатие встроенной кнопки (inline-кнопки, она висит под сообщением). Telegram присылает боту не сообщение, а событие CallbackQuery (запрос от кнопки). Сообщение с кнопкой лежит в callback.message, его и редактируют.
import asyncio
import os
from aiogram import Bot, Dispatcher, F, Router
from aiogram.exceptions import TelegramBadRequest
from aiogram.filters import Command
from aiogram.types import CallbackQuery, InlineKeyboardMarkup, Message
from aiogram.utils.keyboard import InlineKeyboardBuilder
router = Router()
def counter_keyboard() -> InlineKeyboardMarkup:
builder = InlineKeyboardBuilder()
builder.button(text="-1", callback_data="minus")
builder.button(text="+1", callback_data="plus")
builder.button(text="Сброс", callback_data="reset")
builder.adjust(2, 1)
return builder.as_markup()
@router.message(Command("counter"))
async def show_counter(message: Message):
await message.answer("Счёт: 0", reply_markup=counter_keyboard())
@router.callback_query(F.data.in_({"minus", "plus", "reset"}))
async def change_counter(callback: CallbackQuery):
value = int(callback.message.text.split(": ")[1])
if callback.data == "plus":
value += 1
elif callback.data == "minus":
value -= 1
else:
value = 0
try:
await callback.message.edit_text(f"Счёт: {value}", reply_markup=counter_keyboard())
except TelegramBadRequest as e:
if "message is not modified" not in str(e):
raise
await callback.answer()
async def main():
bot = Bot(token=os.environ["BOT_TOKEN"])
dp = Dispatcher()
dp.include_router(router)
await dp.start_polling(bot)
if __name__ == "__main__":
asyncio.run(main())
InlineKeyboardBuilder собирает клавиатуру: button() добавляет кнопку, adjust(2, 1) раскладывает их в ряды по две и по одной. callback_data — строка, которую Telegram вернёт боту при нажатии. Фильтр F.data.in_({...}) пропускает только наши три значения. Сброс при нуле как раз даёт «message is not modified», поэтому ошибка поймана. В конце обязателен callback.answer(): без него у пользователя на кнопке будут крутиться часики.
Как удалить сообщение
await message.delete()
# или, если известны номера:
await bot.delete_message(chat_id, message_id)
Ограничения Telegram такие. Свои сообщения бот удаляет в любом чате. Чужие — только в группе, где он администратор с правом удалять сообщения. В личном чате бот может удалить и сообщение пользователя. Сообщения старше 48 часов бот удалить не может (кроме своих исходящих в личке и в каналах).
Как отправить фото, файл, аудио и видео
У каждого типа свой метод: send_photo, send_audio, send_document, send_video, send_voice. Файл с диска передают через FSInputFile (FS — файловая система), ссылку из интернета — просто строкой.
from aiogram.types import FSInputFile
await bot.send_photo(chat_id, FSInputFile("photo.jpg"), caption="Красивая фотография")
await bot.send_audio(chat_id, FSInputFile("audio.mp3"), caption="Музыкальный трек")
await bot.send_document(chat_id, FSInputFile("document.pdf"), caption="PDF-документ")
await bot.send_video(chat_id, FSInputFile("video.mp4"), caption="Видеоролик")
await bot.send_voice(chat_id, FSInputFile("voice.ogg"), caption="Голосовое сообщение")
await bot.send_photo(chat_id, "https://example.com/cat.jpg")
Старый приём open('photo.jpg', 'rb') в aiogram 3 не работает. Голосовое сообщение Telegram покажет как голосовое, только если файл в формате OGG с кодеком OPUS. Иначе он придёт как обычный файл.
Внутри обработчика удобнее короткие методы сообщения: message.answer_photo(), answer_document(), answer_video() и так далее. Номер чата они подставят сами.
Готовый бот: семь команд и разный контент
import asyncio
import os
from aiogram import Bot, Dispatcher, Router
from aiogram.filters import Command, CommandStart
from aiogram.types import FSInputFile, Message
router = Router()
@router.message(CommandStart())
async def handle_start(message: Message):
await message.answer("Команды: /text /photo /video /audio /document /voice")
@router.message(Command("text"))
async def handle_text(message: Message):
await message.answer("Пример текстового сообщения")
@router.message(Command("photo"))
async def handle_photo(message: Message):
await message.answer_photo(FSInputFile("photo.jpg"), caption="Фото с диска")
@router.message(Command("video"))
async def handle_video(message: Message):
await message.answer_video(FSInputFile("video.mp4"))
@router.message(Command("audio"))
async def handle_audio(message: Message):
await message.answer_audio(FSInputFile("audio.mp3"))
@router.message(Command("document"))
async def handle_document(message: Message):
await message.answer_document(FSInputFile("document.pdf"))
@router.message(Command("voice"))
async def handle_voice(message: Message):
await message.answer_voice(FSInputFile("voice.ogg"))
async def main():
bot = Bot(token=os.environ["BOT_TOKEN"])
dp = Dispatcher()
dp.include_router(router)
await dp.start_polling(bot)
if __name__ == "__main__":
asyncio.run(main())
Положите рядом со скриптом файлы photo.jpg, video.mp4, audio.mp3, document.pdf и voice.ogg. Если файла нет, обработчик упадёт с ошибкой FileNotFoundError, а бот продолжит работать.
Бот — фильтр ненормативной лексики
Бот читает все сообщения в группе, ищет слова из списка и, если нашёл, предупреждает автора и удаляет сообщение.
import asyncio
import os
from aiogram import Bot, Dispatcher, F, Router
from aiogram.exceptions import TelegramBadRequest
from aiogram.types import Message
router = Router()
FORBIDDEN_WORDS = ["ругаться", "оскорблять", "ненормативная_лексика"]
@router.message(F.text)
async def check_words(message: Message):
text = message.text.lower()
if not any(word in text for word in FORBIDDEN_WORDS):
return
await message.reply("В чате нельзя ругаться!")
try:
await message.delete()
except TelegramBadRequest:
# у бота нет прав администратора или сообщение старше 48 часов
pass
async def main():
bot = Bot(token=os.environ["BOT_TOKEN"])
dp = Dispatcher()
dp.include_router(router)
await dp.start_polling(bot)
if __name__ == "__main__":
asyncio.run(main())
Чтобы бот видел все сообщения группы, а не только команды, отключите у него режим приватности: в BotFather команда /setprivacy, значение Disable. Потом удалите бота из группы и добавьте снова. Чтобы удаление сработало, сделайте бота администратором с правом удалять сообщения. Список слов замените своим: сейчас в нём слова-заглушки.
Уроки по теме
Спокойный вход с мини-проектами — уроки курса (в нём более простая библиотека telebot):
- Создание чат-ботов. Тема 2. Бот с кнопками — курс «Создание чат-ботов»

