Telegram-боты на aiogram
Telegram-бот — это программа, которая отвечает на сообщения пользователей через API Telegram. В этом уроке напишем бота на Python с библиотекой aiogram: от токена до запуска на сервере.
1. Как работают Telegram-боты
Бот — это обычная программа, у которой есть токен — секретный ключ доступа к вашему боту в Telegram. Через API бот получает события (сообщения, нажатия кнопок) и отправляет ответы. Есть два способа получать события.
| Способ | Как работает | Когда нужен |
|---|---|---|
| Long polling | бот сам опрашивает сервер Telegram | учебные и средние проекты |
| Webhook | Telegram сам шлёт события на ваш URL | боевые боты и высокая нагрузка |
2. Токен и BotFather
Бота создаёт официальный аккаунт @BotFather. Напишите ему команду /newbot, задайте имя и username — в ответ придёт токен.
/newbot
Имя бота: Учебный бот
Username: my_first_study_bot
Токен: 1234567890:AAHwLg...-секретная-частьЕсли токен потерялся, команда /token в BotFather покажет его снова. Username бота должен заканчиваться на bot.
3. Установка aiogram
aiogram — асинхронная библиотека для Telegram Bot API на Python. Работает от Python 3.9 и новее. Установим её в виртуальном окружении проекта.
python -m venv .venv
.venv\Scripts\activate # Windows
source .venv/bin/activate # Linux и macOS
pip install aiogram4. Первый бот: команда /start
Самый маленький рабочий бот. Токен удобно хранить в переменной окружения BOT_TOKEN.
from aiogram import Bot, Dispatcher
from aiogram.types import Message
from aiogram.filters import CommandStart
bot = Bot(token="1234567890:AAHwLg...")
dp = Dispatcher()
@dp.message(CommandStart())
async def start(message: Message):
await message.answer("Привет! Я учебный бот.")
if __name__ == "__main__":
dp.run_polling(bot)Запустите файл командой python bot.py и напишите боту /start в Telegram.
import os, затем os.environ["BOT_TOKEN"]. Так токен не попадёт в репозиторий.5. Обработчики сообщений
Обработчик — функция с декоратором @dp.message и фильтром. Фильтры выбирают, какие события попадёт в функцию: текстовые, с фото, определённая команда.
from aiogram import F
from aiogram.filters import Command
@dp.message(Command("help"))
async def help_cmd(message: Message):
await message.answer("Команды: /start, /help")
@dp.message(F.text)
async def echo(message: Message):
await message.answer("Вы написали: " + message.text)| Фильтр | Срабатывает когда |
|---|---|
| Command("start") / CommandStart() | прислана команда /start |
| F.text | прислан обычный текст |
| F.text.lower() == "привет" | текст строго равен «привет» |
| F.photo | прислана фотография |
6. Клавиатуры и inline-кнопки
Клавиатур бывает два вида: обычная (под полем ввода) и inline (кнопки прямо в сообщении). Обычная — ReplyKeyboardMarkup.
from aiogram.types import ReplyKeyboardMarkup, KeyboardButton
kb = ReplyKeyboardMarkup(keyboard=[
[KeyboardButton(text="Каталог"), KeyboardButton(text="Помощь")]
], resize_keyboard=True)
@dp.message(Command("menu"))
async def menu(message: Message):
await message.answer("Выберите:", reply_markup=kb)Inline-кнопки отправляют callback — нажатие обрабатывает @dp.callback_query.
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton, CallbackQuery
from aiogram import F
kb = InlineKeyboardMarkup(inline_keyboard=[[
InlineKeyboardButton(text="Да", callback_data="yes"),
InlineKeyboardButton(text="Нет", callback_data="no"),
]])
@dp.callback_query(F.data.in_({"yes", "no"}))
async def on_click(cb: CallbackQuery):
await cb.message.answer("Выбрано: " + cb.data)
await cb.answer()await cb.answer() — иначе Telegram покажет «часики» у кнопки. Поле callback_data ограничено 64 байтами.7. Состояния FSM
FSM (Finite State Machine) нужна для многошаговых диалогов: бот запоминает, на каком шаге находится пользователь, и спрашивает данные по очереди.
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
class Order(StatesGroup):
name = State()
address = State()
@dp.message(Command("order"))
async def start_order(message: Message, state: FSMContext):
await state.set_state(Order.name)
await message.answer("Как вас зовут?")
@dp.message(Order.name)
async def get_name(message: Message, state: FSMContext):
await state.update_data(name=message.text)
await state.set_state(Order.address)
await message.answer("Укажите адрес доставки.")Пока пользователь в состоянии Order.name, срабатывает обработчик этого состояния, а не обычные фильтры. После сбора данных состояние снимается командой await state.clear().
await state.get_data().8. Отправка файлов
Бот умеет отправлять фото, документы, аудио и видео. Файл можно передать по file_id (после первого получения) либо отправить из локального файла через BufferedInputFile.
from aiogram.types import BufferedInputFile
with open("report.txt", "rb") as f:
data = f.read()
await message.answer_document(
BufferedInputFile(data, filename="report.txt")
)Аналогично работают answer_photo, answer_audio и answer_video.
file_id и переиспользуйте: повторная передача по file_id почти бесплатна и мгновенна.9. Логирование и обработка ошибок
Добавьте логирование, чтобы видеть, что происходит: кто написал, какая ошибка возникла. Логи улучшают бота быстрее, чем перечитывание кода.
import logging
logging.basicConfig(level=logging.INFO)
@dp.message(Order.address)
async def finish_order(message: Message, state: FSMContext):
data = await state.get_data()
if len(message.text) < 5:
await message.answer("Адрес слишком короткий, повторите.")
return
await state.clear()
await message.answer("Заказ принят! Ждите курьера.")Ошибки в обработчиках можно оборачивать в try / except и возвращать пользователю короткое сообщение вместо тишины. Для отлова всех исключений aiogram поддерживает специальный обработчик @dp.errors.
10. Деплой на VPS
Чтобы бот работал круглосуточно, его запускают на сервере. Для системы инициализации systemd создайте юнит, который сам перезапускает бота после сбоев.
# /etc/systemd/system/mybots.service
[Unit]
Description=Telegram bot
After=network.target
[Service]
User=myuser
WorkingDirectory=/home/myuser/bot
EnvironmentFile=/home/myuser/bot/.env
ExecStart=/home/myuser/bot/.venv/bin/python bot.py
Restart=always
[Install]
WantedBy=multi-user.targetsudo systemctl enable mybots
sudo systemctl start mybots
sudo systemctl status mybotsВ файле .env на сервере хранится переменная BOT_TOKEN — код остаётся одинаковым и содержит только её имя. Подробнее про серверы — в уроке Хостинг и деплой.
11. Платежи и подписки
Telegram умеет принимать платежи прямо в чате через Payments API: бот отправляет инвойс, пользователь оплачивает банковской картой или через Telegram Stars, а бот получает статус оплаты.
from aiogram.types import LabeledPrice
from aiogram.methods import SendInvoice
await bot(SendInvoice(
chat_id=user_id,
title="Подписка на месяц",
description="Доступ к контенту на 30 дней",
payload="sub_30",
provider_token=prov_token,
currency="rub",
prices=[LabeledPrice(label="Подписка", amount=29900)]
))Обработчик pre_checkout_query должен подтвердить платёж, а после подтверждения вы даёте доступ: выдаёте роль, ключ или продлеваете срок.
Цены задаются в минимальных единицах валюты: 29900 означает 299 ₽. Для подписок проверяйте статус на своей стороне: при отмене доступа вовремя отзывайте. Telegram Stars упрощают процесс — эквайринг работает на стороне Telegram.
provider_token — это деньги и блокировка аккаунта бота.12. Webhook вместо long polling
При большом потоке сообщений long polling неэффективен: бот постоянно «спрашивает» сервер Telegram. Webhook выгоднее — Telegram сам отправляет обновления на ваш HTTPS-адрес.
from aiogram.webhook.aiohttp_server import SimpleRequestHandler
async def main():
await bot.delete_webhook()
await bot.set_webhook(url="https://mybot.example.com/bot")
# запуск aiohttp-сервера с точкой /botТребования: публичный домен с HTTPS-сертификатом. Классическая связка — Nginx как прокси и сертификат Let's Encrypt, см. уроки Nginx и Хостинг и деплой.
- webhook один на бота: новый адрес отменяет предыдущий;
- секретный токен в URL повышает безопасность;
- при сбоях Telegram повторяет доставку — обработчики должны быть идемпотентными.
Дополнительная защита: поставьте секретный токен в адрес webhook — Telegram подписывает им каждый запрос, и злоумышленник не сможет имитировать апдейты. Здоровье бота проверяется методом getWebhookInfo со списком последних обращений.
13. Нагрузка и устойчивость
Популярный бот получает тысячи обновлений в секунду. Чтобы он не падал и не банил пользователей за спам, заложите защиту заранее:
- ограничьте частоту команд по пользователю (rate limit), прежде чем Telegram сделает это за вас;
- добавьте анти-флуд: при повторе одной команды отвечайте один раз;
- тяжёлые действия (парсинг, генерация) выполняйте в фоне через
asyncio.create_task; - кэшируйте частые ответы, а долгие внешние запросы — таймаут.
Перед запуском под нагрузкой прогоните нагрузочный тест: десятки тысяч сообщений за минуту. Смотрите на время ответа и ошибки, а не только на «а упал ли». Предельную скорость запишите в документацию, чтобы знать, когда пора масштабировать.
import asyncio
async def heavy_processing(message):
await asyncio.sleep(2) # имитация работы
await message.answer("Готово!")
@dp.message()
async def incoming(message: Message):
asyncio.create_task(heavy_processing(message))
await message.answer("Принято, считаю...")14. Безопасность бота
Любой публичный бот — мишень: кто-то попробует украсть токен, заспамить или подменить команду. Защита, заложенная с первого дня, дешевле разбора инцидента.
- токен только в переменных окружения, а не в коде и не в репозитории;
- для webhook — секретный токен в URL, чтобы не принимались «чужие» апдейты;
- ограничивайте права бота: не давайте админ-роль без необходимости;
- ввод пользователя считайте недоверенным: длина, тип, санитайз перед подстановкой в команды.
import os
TOKEN = os.environ["BOT_TOKEN"] # не в коде, не в .env в gitПроверяйте источник апдейта: у long polling это делает библиотека, у webhook — секретный токен. Если бот вдруг начал рассылать спам — быстрый доступ к логам укажет, кто и как запустил команду.
15. Хранилища состояний и данных
Бот без памяти отвечает только на одиночные команды. Появляется сценарий из нескольких шагов (заказ, опрос) — нужно состояние: его хранят FSM-хранилище и база данных.
from aiogram.fsm.storage.redis import RedisStorage
storage = RedisStorage.from_url("redis://localhost:6379/0")- FSM-состояния в Redis: перезапуск бота не «роняет» пользователя на середине формы;
- долгоживущие данные (пользователи, подписки, история) — в PostgreSQL;
- ключам хранилища задавайте префиксы и TTL, чтобы «мусорные» состояния не копились;
- частые ответы и очереди — Redis (курс Redis).
16. Интеграция с внешними сервисами
Ценность бота часто в связках: погода, CRM, оплата, внутренние API. Внешние запросы выполняйте асинхронно и с запасом прочности на случай, когда сервис лежит или отвечает медленно.
import httpx
async def fetch_weather(city):
async with httpx.AsyncClient(timeout=10) as client:
data = (await client.get(f"/v1/weather?city={city}")).json()
return data- таймаут на каждый запрос: без него «подвисший» сервис вешает всего бота;
- повторы с задержкой для транзиентных ошибок 5xx;
- если внешняя система шлёт веб-хуки вам — обработчики делайте идемпотентными;
- медленные операции — в фоновую задачу, а пользователю мгновенный ответ «принято».
17. Аналитика, A/B-тесты и монетизация
Бот, который не измеряется, не развивается. Минимальный набор — события ключевых действий: через две недели станет ясно, какие сценарии живые, а какие «мёртвые».
| Метрика | Что показывает |
|---|---|
| DAU / MAU | активность и возвращаемость |
| Retention | сколько пользователей остаётся после недели |
| Воронка команды | где теряется путь «старт → целевое действие» |
# A/B: две версии приветствия
pool = ["hi_v1", "hi_v2"]
variant = random.choice(pool)
await message.answer(hi_texts[variant])Монетизация: подписки через Payments API (раздел 11), разовые продажи доступа, спонсорская реклама и партнёрские программы. Правило — сначала метрики и удержание, потом платное.
18. Форматирование сообщений: HTML и Markdown
Telegram поддерживает разметку сообщений двумя способами: HTML-тегами и Markdown. От выбора зависит, как красиво и безопасно рендерится ваш текст в чатах.
# HTML-вариант
Визит: <b>завтра</b>
Цена: <a href="https://site.ru/pay">оплатить</a>
<pre>2026-09-19 10:00</pre>
# MarkdownV2 (здесь нужны экранирования)
Визит: <b>завтра</b>- HTML-теги проще: <b>, <i>, <code>, <pre>, <a>, <tg-spoiler>;
- Markdown требует экранировать спецсимволы — легко «сломать» сообщение;
- важно: ширина и длина сообщения — лимит 4096 символов, режьте длинный текст.
Практическое правило: в aiogram используйте parse_mode="HTML", а текст стройте через класс HTMLBuilder или f-строки с экранированием пользовательского ввода.
link_preview_options и контролируйте превью ссылок — оно портит вид сообщения.19. Тестирование бота: pytest и моки
Бот без тестов растёт до момента, когда новая фича ломает старую. Основной инструмент — pytest: вы подменяете сетевой слой моком и проверяете логику, не дергая Telegram API.
import pytest
from unittest.mock import AsyncMock
@pytest.mark.asyncio
async def test_help_handler(bot):
message = AsyncMock(text="/start")
await start_handler(message)
message.answer.assert_called_once()- мок-объект импруется из интерфейса aiogram, не из реального клиента;
- фикстуры создают приложение с тестовым токеном-заглушкой;
- тесты состояний FSM — отдельные блоки, не забывайте про них.
Не смешивайте «интеграционные» тесты с реальным API в CI: Telegram может лимитировать и «упасть» по сети. Держите быстрый набор моков локально.
20. Фоновые задачи и планировщики
Часто нужно выполнять работу вне событий чата: рассылки, проверки подписок, сбор данных. Для этого бот запускает фоновые задачи и планировщики рядом с циклом обработки.
# APScheduler + aiogram
from apscheduler.schedulers.asyncio import AsyncIOScheduler
scheduler = AsyncIOScheduler()
scheduler.add_job(send_digest, "cron", hour=9, minute=0)
scheduler.start()- cron-задачи (ежедневно/еженедельно) — через планировщик;
- фоновые процессы (долгие вычисления) — через asyncio.create_task с учётом сессий;
- расшифровка: фоновые задачи делят ресурсы с основным циклом — проверяйте нагрузку.
Осторожно с состоянием: каждая задача — отдельный контекст; храните зависимости вне, получая их по одиночке. Логирование фоновых задач — обязательно: падение «в тишине» незаметно.
21. Обновление aiogram: миграция версий
Допустим, бот написан на aiogram 2.x, а вышел 3.x с другим API. Переход затрагивает почти все импорты и логику хендлеров, поэтому миграцию делают проект за проект, а не «в лоб».
# aiogram 2 → 3
# было: from aiogram import Bot, types
# стало: from aiogram import Bot, Dispatcher, types
# было: dp.register_message_handler(...)
# стало: @router.message(Command("start"))- роутеры заменили «регистрацию хендлеров» на декларативные функции;
- FSM, кнопки и parsing разнесены по отдельным пространствам имен;
- Long polling теперь большей частью вынесен в библиотеку.
Стратегия: сначала поднять версию, потом пройтись по явным изменениям (changelog aiogram), потом — чинить тесты. Каждый блок — отдельный коммит с рабочей сборкой.
Что дальше
Закрепите язык в уроке Python. Храните данные бота в SQL, подключайте внешние HTTP-запросы из FastAPI и REST API. А чтобы бот научился отвечать умнее — подтяните Промпт-инжиниринг.