Telegram Bot на Python
Разработка Telegram-бота на Python
1. Введение: что такое Telegram-бот и зачем он нужен
Telegram-бот — это программный агент, функционирующий внутри мессенджера Telegram и взаимодействующий с пользователями посредством обмена сообщениями в рамках чатов (личных, групповых или каналов). Бот не является отдельным клиентом Telegram, а представляет собой внешний сервис, подключённый к платформе через официальный Bot API — RESTful-интерфейс, предоставляемый Telegram.
Важно подчеркнуть: бот не запускается внутри Telegram. Он запускается на стороннем вычислительном ресурсе (локальная машина, сервер, облачная платформа), откуда инициирует HTTP-запросы к API Telegram (для отправки сообщений) и получает входящие обновления (updates) либо посредством long polling, либо — в production-сценариях — через вебхуки (webhooks). Архитектурно бот — это обычное веб-приложение, принимающее и отправляющее JSON-пакеты.
Telegram-боты не обладают собственной «волей»: вся логика работы определяется бэкендом. Платформа Telegram лишь предоставляет канал связи и гарантирует доставку событий (сообщений, нажатий кнопок, добавлений в чат и т.д.). Это фундаментальное понимание позволяет корректно проектировать масштабируемые и отказоустойчивые решения.
Telegram-боты используются в самых разных доменах:
- Информационные сервисы — рассылка новостей, курсов валют, статусов интеграций.
- Интерактивные интерфейсы — замена веб-форм (регистрация, опросы, техподдержка).
- Автоматизация процессов — модерация чатов, сбор обратной связи, интеграция с CRM/ERP.
- Образовательные и игровые приложения — чат-квесты, тренажёры, интерактивные учебники.
- DevOps-инструменты — уведомления о деплоях, мониторинг, триггеры на события в CI/CD.
Ограничения Telegram Bot API продиктованы политикой платформы: бот не может инициировать диалог с пользователем первым (только в ответ на действие), не имеет доступа к истории чата до своего добавления, и ограничен в скорости отправки сообщений (flood control). Эти ограничения не являются техническими дефектами, а отражают баланс между функциональностью и защитой пользователей от спама.
2. Архитектурные основы: как устроен и работает бот
2.1. Токен — идентификатор и ключ доступа
Каждый бот в Telegram имеет уникальный токен доступа — строку вида 123456789:ABCdefGHIjklMNOpqrsTUVwxyZ123456789. Этот токен:
- выдаётся один раз при создании бота через BotFather;
- однозначно идентифицирует бота в системе Telegram;
- предоставляет право на выполнение HTTP-запросов к Bot API от имени бота;
- не должен храниться в открытом виде (например, в репозитории на GitHub), так как его компрометация эквивалентна передаче полного контроля над ботом третьим лицам.
Токен используется как часть URL при вызове методов API:
https://api.telegram.org/bot<TOKEN>/METHOD_NAME
Например, отправка сообщения:
POST https://api.telegram.org/bot123456:ABCdef/sendMessage
{
"chat_id": 987654321,
"text": "Привет!"
}
2.2. BotFather — официальный инструмент управления ботами
@BotFather — это системный бот Telegram, предназначенный исключительно для регистрации, настройки и администрирования других ботов. Он не является частью Bot API, а представляет собой интерфейс к внутренней системе управления ботами Telegram.
Основные функции BotFather:
/start— начало работы;/newbot— создание нового бота (запрашивает имя и username, возвращает токен);/setcommands— установка списка команд/start,/helpи т.д. (влияет на подсказки в клиенте);/setdescriptionи/setabouttext— описание и «о боте» (отображаются в профиле);/setprivacy— управление режимом конфиденциальности (в группах бот по умолчанию получает только сообщения, начинающиеся с/, или упоминания —@my_bot; отключение приватности даёт доступ ко всем сообщениям, но требует одобрения администратора чата);/revoke— отзыв текущего токена и генерация нового (необходимо при утечке);/deletebot— полное удаление бота.
BotFather работает строго в персональном чате. Любые попытки автоматизировать взаимодействие с ним (например, через скрипты) нарушают условия использования и могут привести к блокировке.
2.3. Модели взаимодействия: Long Polling vs. Webhooks
Telegram предоставляет два способа получения входящих событий (updates):
Long Polling (getUpdates)
Клиент (ваш бот) периодически отправляет HTTP-запрос к методу getUpdates, передавая offset — идентификатор последнего обработанного обновления. Сервер Telegram удерживает соединение открытым до появления новых событий или истечения таймаута (до 60 секунд). После получения пакета обновлений клиент обрабатывает их и отправляет следующий запрос с увеличенным offset.
Преимущества:
- Простота отладки (не требует публичного IP/HTTPS);
- Подходит для разработки и тестирования;
- Нет зависимости от внешней инфраструктуры.
Недостатки:
- Задержка доставки событий (вплоть до 60 сек);
- Нагрузка на клиент (постоянные запросы);
- Не масштабируется горизонтально без дополнительной синхронизации offset’ов.
Webhooks
Telegram сам инициирует HTTPS-POST-запросы на указанный вами URL при каждом новом событии. Вы регистрируете webhook один раз методом setWebhook, передав URL (например, https://example.com/webhook) и, опционально, SSL-сертификат (если используется самоподписанный).
Преимущества:
- Мгновенная доставка обновлений;
- Эффективное использование ресурсов (обработка только по факту события);
- Естественная поддержка масштабирования (балансировщик перед несколькими инстансами бота).
Недостатки:
- Требуется публично доступный HTTPS-эндпоинт;
- Необходимо корректное управление SSL/TLS;
- Сложнее отлаживать локально (требуются туннели, например,
ngrokилиlocaltunnel).
В production-средах рекомендуется использовать webhooks, особенно при высокой нагрузке.
3. Выбор инструментов и библиотек
Хотя Bot API можно использовать напрямую через requests, в реальной практике применяются высокоуровневые библиотеки, абстрагирующие HTTP-взаимодействие, десериализацию JSON, управление состояниями и маршрутизацию.
Наиболее популярные библиотеки для Python:
| Библиотека | Поддержка асинхронности | Состояния (FSM) | Популярность | Комментарий |
|---|---|---|---|---|
| python-telegram-bot (PTB) | ✅ (через telegram.ext.Application) | ✅ (ConversationHandler) | Очень высокая | Активно развивается (v20+), официально рекомендуется. Поддержка webhooks и polling «из коробки». |
| aiogram | ✅ (нативно, на asyncio) | ✅ (FSMContext) | Очень высокая | Быстрая, лёгкая, ориентирована на асинхронность. Хорошо документирована. |
| Telethon | ✅ | ❌ (требует доп. слоёв) | Средняя | Низкоуровневая, больше подходит для клиентских ботов (не Bot API, а User API), но может работать и с ботами. |
Для данной главы выбрана python-telegram-bot (v20+), так как:
- её архитектура (на основе
ApplicationBuilder) соответствует современным best practices; - она предоставляет чёткое разделение ответственности (handlers, filters, middlewares);
- имеет мощную систему типизации (stub-файлы, Pydantic-подобные модели);
- официально поддерживается сообществом и имеет стабильную обратную совместимость.
4. Подготовка среды разработки
4.1. Установка и настройка VS Code
Visual Studio Code — оптимальный выбор для разработки на Python благодаря:
- интеграции с Python-интерпретатором и виртуальными окружениями;
- поддержке отладки (
launch.json); - расширениям (Python, Pylance, Docker, GitLens);
- встроенному терминалу.
Рекомендуемые расширения:
- Python (Microsoft);
- Pylance (для статического анализа и автодополнения);
- Docker (для работы с контейнерами);
- GitLens (для контроля версий);
- EditorConfig for VS Code (единые настройки отступов и кодировки).
4.2. Создание проекта и виртуального окружения
mkdir telegram-moderator-bot
cd telegram-moderator-bot
python -m venv .venv
source .venv/bin/activate # Linux/macOS
# .venv\Scripts\activate # Windows
Создадим requirements.txt:
python-telegram-bot[httpx]==20.7
python-dotenv==1.0.1
uvicorn==0.29.0 # для webhooks через ASGI
Установим зависимости:
pip install -r requirements.txt
Для управления секретами (токеном) используем .env-файл, исключённый из Git:
TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrsTUVwxyZ123456789
WEBHOOK_URL=https://yourdomain.com/webhook
WEBHOOK_PORT=8443
Загрузка переменных:
from dotenv import load_dotenv
import os
load_dotenv()
TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
5. Архитектура обработки событий в python-telegram-bot (v20+)
Начиная с версии 20, python-telegram-bot перешёл на полностью асинхронную, декларативную архитектуру на основе класса Application. Это не просто «обёртка над API» — это полноценный фреймворк с чётко определёнными слоями:
5.1. Жизненный цикл обновления (update)
- Получение — Telegram доставляет JSON-пакет (update) либо через
getUpdates, либо POST на webhook. - Десериализация — библиотека преобразует JSON в объекты типов
Update,Message,User,Chatи др. Все поля строго типизированы. - Препроцессинг — проход через
Middleware. Позволяет логировать, кэшировать, проверять права до попадания в обработчики. - Фильтрация — система
BaseFilterиMessageFilterпозволяет отсеивать нежелательные события (например, игнорировать ботов, оставлять только текстовые сообщения). - Маршрутизация — выбор подходящего
Handler(например,CommandHandlerдля/start,MessageHandlerдля обычных сообщений). - Обработка — вызов пользовательской корутины (async def) с передачей
updateиcontext: ContextTypes.DEFAULT_TYPE. - Постпроцессинг — необязательный этап (например, логирование ответа, сбор метрик).
Важно: все обработчики должны быть асинхронными (async def). Даже если внутри используется синхронный код (например, запись в SQLite), его следует оборачивать в loop.run_in_executor, чтобы не блокировать event loop.
5.2. Контекст (context) — единое хранилище состояния
Объект context — это центральная точка доступа к:
context.bot— экземплярBot, через который отправляются сообщения;context.job_queue— планировщик фоновых задач (таймеры, напоминания);context.user_data— словарь, привязанный кuser_id(сохраняется между сессиями при использованииPersistence);context.chat_data— словарь, привязанный кchat_id;context.bot_data— глобальное хранилище для всего приложения.
Это избавляет от необходимости передавать bot и update в каждую вложенную функцию — достаточно передать context.
5.3. Система фильтров: точечная маршрутизация
Вместо громоздких if-else внутри обработчика, PTB предлагает декларативные фильтры:
from telegram.ext import MessageHandler, filters
# Только текстовые сообщения от обычных пользователей (не ботов)
text_handler = MessageHandler(
filters.TEXT & ~filters.COMMAND & ~filters.StatusUpdate.ALL,
handle_text
)
# Только команды /start и /help
start_handler = CommandHandler("start", start_command)
help_handler = CommandHandler("help", help_command)
# Только новые участники в группе
new_member_handler = MessageHandler(
filters.StatusUpdate.NEW_CHAT_MEMBERS,
greet_new_member
)
Фильтры можно комбинировать через & (И), | (ИЛИ), ~ (НЕ). Это делает код декларативным и легко расширяемым.
6. Реализация бота-модератора: пошаговое проектирование
Цель — бот для группового чата, выполняющий:
- Приветствие новых участников;
- Автоматическую фильтрацию сообщений с запрещёнными словами и ссылками;
- Реакцию на команду
/ban [причина]от администраторов; - Логирование действий в отдельный чат (для аудита).
6.1. Структура проекта
telegram-moderator-bot/
├── .env
├── .gitignore
├── requirements.txt
├── Dockerfile
├── docker-compose.yml
├── src/
│ ├── __init__.py
│ ├── main.py # точка входа
│ ├── handlers/
│ │ ├── __init__.py
│ │ ├── start.py # /start
│ │ ├── moderation.py # модерация
│ │ └── admin.py # /ban и др.
│ ├── utils/
│ │ ├── filters.py # кастомные фильтры
│ │ └── logger.py # логирование
│ └── config.py # загрузка настроек
└── logs/
└── moderation.log
6.2. Конфигурация и безопасность
src/config.py:
import os
from dotenv import load_dotenv
from pathlib import Path
load_dotenv()
class Config:
TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
if not TOKEN:
raise ValueError("TELEGRAM_BOT_TOKEN не задан в .env")
# Чат для логов модерации (можно получить через @RawDataBot)
LOG_CHAT_ID = int(os.getenv("LOG_CHAT_ID", "0"))
# Список администраторов (user_id, можно получить через бота-отладчик)
ADMINS = set(map(int, os.getenv("ADMINS", "").split(","))) if os.getenv("ADMINS") else set()
WEBHOOK_URL = os.getenv("WEBHOOK_URL")
WEBHOOK_PORT = int(os.getenv("WEBHOOK_PORT", "8443"))
WEBHOOK_SECRET_TOKEN = os.getenv("WEBHOOK_SECRET_TOKEN") # для проверки подлинности запросов
# Фильтры
FORBIDDEN_WORDS = {"лох", "дурак", "спам", "казино", "кредит", "реклама"}
FORBIDDEN_DOMAINS = {"t.me", "vk.com", "ok.ru"} # пример
Обратите внимание:
WEBHOOK_SECRET_TOKEN— случайная строка, передаваемая при установке webhook и проверяемая в заголовкеX-Telegram-Bot-Api-Secret-Token. Это защита от подделки запросов.ADMINSзадаются поuser_id, а не по username — это надёжнее (username можно сменить).FORBIDDEN_WORDS— в нижнем регистре, без пунктуации.
6.3. Приветствие новых участников
src/handlers/moderation.py:
from telegram import Update
from telegram.ext import ContextTypes
from src.config import Config
async def greet_new_member(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Обработка события добавления новых участников в чат."""
if not update.message or not update.message.new_chat_members:
return
for member in update.message.new_chat_members:
# Пропускаем добавление самого бота
if member.id == context.bot.id:
continue
# Формируем приветствие
welcome_text = (
f"Добро пожаловать, {member.full_name}!\n\n"
"📌 Пожалуйста, ознакомьтесь с правилами чата:\n"
"• Запрещены спам, реклама, оскорбления;\n"
"• Ссылки разрешены только по теме обсуждения;\n"
"• Уважайте других участников.\n\n"
"Нарушения будут пресекаться модерацией."
)
try:
await update.message.reply_text(welcome_text)
except Exception as e:
# Логируем ошибку (например, бот не имеет прав на отправку)
await log_moderation_event(
context,
f"Не удалось отправить приветствие {member.id}: {e}"
)
6.4. Фильтрация сообщений
Для эффективной проверки текста — нормализуем его:
import re
def normalize_text(text: str) -> str:
"""Приведение текста к каноническому виду: нижний регистр, удаление пунктуации и пробелов."""
text = re.sub(r"[^\w\s]", "", text.lower())
return " ".join(text.split())
def contains_forbidden_content(text: str, config: Config) -> bool:
"""Проверка на наличие запрещённых слов или доменов."""
norm = normalize_text(text)
# Проверка слов
for word in config.FORBIDDEN_WORDS:
if word in norm:
return True
# Проверка доменов (в исходном тексте — сохраняем регистр и точки)
for domain in config.FORBIDDEN_DOMAINS:
if domain in text.lower():
return True
return False
Обработчик:
async def filter_message(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
"""Фильтрация текстовых сообщений на предмет нарушений."""
if not update.effective_message or not update.effective_message.text:
return
message = update.effective_message
user = update.effective_user
chat = update.effective_chat
if contains_forbidden_content(message.text, Config):
# Удаляем сообщение
try:
await message.delete()
except Exception as e:
await log_moderation_event(context, f"Не удалено сообщение {message.message_id}: {e}")
# Отправляем предупреждение (только если не админ)
if user.id not in Config.ADMINS:
warning = f"{user.mention_html()}, ваше сообщение удалено: обнаружено запрещённое содержание."
try:
await message.chat.send_message(warning, parse_mode="HTML")
except:
pass
# Логируем
await log_moderation_event(
context,
f"Автоудаление: {user.id} ({user.full_name}) в чате {chat.id} — '{message.text[:50]}...'"
)
Примечание. В production-решении рекомендуется:
- использовать NLP-фильтры (например,
razdel+pymorphy2для лемматизации);- добавить каунтер нарушений в
user_data, чтобы после N нарушений — бан;- исключить проверку для администраторов и владельца чата.
6.5. Административная команда /ban
src/handlers/admin.py:
from telegram import Update
from telegram.constants import ParseMode
from telegram.ext import ContextTypes, ConversationHandler, CommandHandler, MessageHandler, filters
from src.config import Config
# Состояния диалога
SELECTING_USER, ENTERING_REASON = range(2)
async def ban_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Начало диалога бана: ожидание user_id или username."""
if update.effective_user.id not in Config.ADMINS:
await update.message.reply_text("❌ У вас нет прав на выполнение этой команды.")
return ConversationHandler.END
await update.message.reply_text(
"Введите user_id или username (с @) пользователя для бана.\n"
"Для отмены — /cancel"
)
return SELECTING_USER
async def select_user(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Получение и проверка целевого пользователя."""
text = update.message.text.strip()
if text.startswith("@"):
username = text[1:]
try:
user = await context.bot.get_chat(username) # возвращает Chat, где type == 'private'
context.user_data["ban_target"] = user.id
except:
await update.message.reply_text("❌ Пользователь не найден.")
return SELECTING_USER
else:
try:
user_id = int(text)
context.user_data["ban_target"] = user_id
except ValueError:
await update.message.reply_text("❌ Некорректный user_id.")
return SELECTING_USER
await update.message.reply_text("Введите причину бана (до 100 символов):")
return ENTERING_REASON
async def enter_reason(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Получение причины и выполнение бана."""
reason = update.message.text[:100]
target_id = context.user_data.get("ban_target")
if not target_id:
await update.message.reply_text("❌ Внутренняя ошибка: цель не задана.")
return ConversationHandler.END
try:
# Баним навсегда (until_date=None)
await context.bot.ban_chat_member(
chat_id=update.effective_chat.id,
user_id=target_id,
until_date=None
)
# Формируем сообщение для чата
admin = update.effective_user
target_info = f"user_id={target_id}" # можно улучшить запросом getChat
log_msg = f"🛡️ Администратор {admin.full_name} ({admin.id}) забанил {target_info}. Причина: {reason}"
await update.effective_chat.send_message(log_msg, parse_mode=ParseMode.HTML)
# Лог в отдельный чат
await log_moderation_event(context, log_msg)
except Exception as e:
await update.message.reply_text(f"❌ Ошибка бана: {e}")
return ConversationHandler.END
await update.message.reply_text("✅ Пользователь забанен.")
return ConversationHandler.END
async def cancel_ban(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
"""Отмена диалога."""
await update.message.reply_text("🚫 Операция отменена.")
return ConversationHandler.END
Регистрация диалога в основном приложении:
from telegram.ext import ApplicationBuilder, ConversationHandler
from src.handlers.admin import ban_command, select_user, enter_reason, cancel_ban
ban_handler = ConversationHandler(
entry_points=[CommandHandler("ban", ban_command)],
states={
SELECTING_USER: [MessageHandler(filters.TEXT & ~filters.COMMAND, select_user)],
ENTERING_REASON: [MessageHandler(filters.TEXT & ~filters.COMMAND, enter_reason)],
},
fallbacks=[CommandHandler("cancel", cancel_ban)],
allow_reentry=True # позволяет начать новый бан, не завершив предыдущий
)
7. Логирование и аудит
src/utils/logger.py:
import logging
from telegram.ext import ContextTypes
from src.config import Config
# Настройка локального логгера
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[
logging.FileHandler("logs/moderation.log", encoding="utf-8"),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
async def log_moderation_event(context: ContextTypes.DEFAULT_TYPE, message: str) -> None:
"""Логирование события в файл и, при наличии, в отдельный Telegram-чат."""
logger.info(message)
if Config.LOG_CHAT_ID != 0:
try:
await context.bot.send_message(
chat_id=Config.LOG_CHAT_ID,
text=f"📝 [Модерация] {message}",
disable_notification=True
)
except Exception as e:
logger.error(f"Не удалось отправить лог в чат: {e}")
8. Полный код main.py (точка входа)
import logging
import asyncio
from telegram.ext import ApplicationBuilder, CommandHandler, MessageHandler, filters
from src.config import Config
from src.handlers.start import start
from src.handlers.moderation import greet_new_member, filter_message
from src.handlers.admin import ban_handler
from src.utils.logger import logger
async def post_init(application):
"""Действия после запуска бота."""
bot_info = await application.bot.get_me()
logger.info(f"Бот @{bot_info.username} (id={bot_info.id}) запущен.")
async def main() -> None:
"""Запуск бота в режиме polling (для разработки)."""
application = (
ApplicationBuilder()
.token(Config.TOKEN)
.post_init(post_init)
.build()
)
# Регистрация обработчиков
application.add_handler(CommandHandler("start", start))
application.add_handler(ban_handler)
application.add_handler(MessageHandler(filters.StatusUpdate.NEW_CHAT_MEMBERS, greet_new_member))
application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, filter_message))
# Запуск
logger.info("Запуск бота в режиме long polling...")
await application.run_polling()
if __name__ == "__main__":
asyncio.run(main())
Команда /start (просто для проверки):
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
await update.message.reply_text(
"🛡️ Модератор-бот активен.\n"
"Работает в группах. Добавьте бота в чат и выдайте права:\n"
"• Удаление сообщений\n"
"• Блокировка участников\n"
"• Видеть участников\n\n"
"Администраторы должны быть заданы в переменной ADMINS."
)
9. Развёртывание: от локального запуска до production
9.1. Локальный запуск (разработка)
# Активируем окружение
source .venv/bin/activate
# Заполняем .env
echo "TELEGRAM_BOT_TOKEN=ваш_токен_от_BotFather" > .env
echo "ADMINS=ваш_user_id" >> .env
# Запуск
python -m src.main
Проверка: отправьте /start боту в личные сообщения.
9.2. Развёртывание на сервере (webhook)
Требования:
- Публичный IP или домен;
- SSL-сертификат (Let’s Encrypt);
- Порт 443 или 8443 (Telegram разрешает 80, 88, 443, 8443).
Изменяем main.py:
async def main_webhook() -> None:
application = (
ApplicationBuilder()
.token(Config.TOKEN)
.post_init(post_init)
.build()
)
# ... регистрация обработчиков (как выше)
# Установка webhook
await application.bot.set_webhook(
url=Config.WEBHOOK_URL,
secret_token=Config.WEBHOOK_SECRET_TOKEN,
allowed_updates=Update.ALL_TYPES
)
# Запуск ASGI-сервера
logger.info(f"Запуск webhook-сервера на порту {Config.WEBHOOK_PORT}...")
await application.run_webhook(
listen="0.0.0.0",
port=Config.WEBHOOK_PORT,
secret_token=Config.WEBHOOK_SECRET_TOKEN,
webhook_url=Config.WEBHOOK_URL,
drop_pending_updates=True
)
Запуск через uvicorn (в Procfile или systemd):
uvicorn src.main:application --host 0.0.0.0 --port 8443 --loop asyncio
Важно. При использовании Nginx/Apache:
- проксируйте
/webhookна ваш сервер;- передавайте заголовок
X-Telegram-Bot-Api-Secret-Token;- отключите buffering (
proxy_buffering off).
9.3. Docker-контейнеризация
Dockerfile:
FROM python:3.11-slim
WORKDIR /app
# Копируем зависимости отдельно — для кэширования слоя
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Копируем код
COPY src/ ./src/
COPY .env ./
# Создаём директорию для логов
RUN mkdir -p logs
EXPOSE 8443
CMD ["python", "-m", "src.main"]
docker-compose.yml (для локального теста webhook через ngrok):
version: '3.8'
services:
bot:
build: .
ports:
- "8443:8443"
environment:
- TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
- ADMINS=${ADMINS}
- WEBHOOK_URL=${WEBHOOK_URL}
- WEBHOOK_SECRET_TOKEN=${WEBHOOK_SECRET_TOKEN}
volumes:
- ./logs:/app/logs
Для production — добавьте:
restart: unless-stopped;healthcheck;- подключение к внешнему volume для логов;
- secrets через
docker secretили HashiCorp Vault.
9.4. Публикация образа и деплой
# Сборка
docker build -t telegram-moderator-bot:1.0 .
# Тегирование (для Docker Hub или внутреннего registry)
docker tag telegram-moderator-bot:1.0 your-registry/telegram-moderator-bot:1.0
# Пуш
docker push your-registry/telegram-moderator-bot:1.0
# Запуск на сервере
docker run -d \
--name moderator-bot \
-p 8443:8443 \
-v /opt/bot/logs:/app/logs \
-e TELEGRAM_BOT_TOKEN=... \
-e ADMINS=... \
your-registry/telegram-moderator-bot:1.0
Чек-лист безопасности Telegram-ботов
Актуально для python-telegram-bot, aiogram и других библиотек, использующих Bot API
1. Управление токеном бота
Токен (123456789:ABCdefGHIjklMNOpqrsTUVwxyZ123456789) — это полномочный учётный ключ, эквивалентный паролю с правами на:
- Чтение всех сообщений, поступающих боту;
- Отправку сообщений от имени бота в любые чаты, где он состоит;
- Изменение описания, команд, аватара;
- Получение списка администраторов чатов (метод
getChatAdministrators); - Полное отключение бота (
logOut,close).
Угрозы
| Угроза | Вектор атаки | Последствия |
|---|---|---|
| Компрометация токена через репозиторий | Токен в config.py, Dockerfile, docker-compose.yml, CI-логах | Полный контроль над ботом; рассылка спама; сбор данных пользователей |
| Перехват токена в памяти | Утечка через ps aux, docker inspect, /proc/PID/environ | То же, что выше — сессия бота может быть перехвачена |
| Подделка запросов к вашему webhook | Отсутствие secret_token или его утечка | Выполнение произвольных действий от имени Telegram (например, отправка «вредоносного» обновления) |
Контрмеры
✅ Хранение токена только в защищённых источниках
- Используйте
.env, исключённый из Git (.gitignore); - В CI/CD — секреты через
secrets(GitHub Actions),VAULT_TOKEN(HashiCorp Vault),AWS Secrets Manager; - В Docker —
--secret(Swarm) илиdocker secret(Compose v3.8+), неenvironment.
✅ Защита памяти процесса
- Запускайте бота от непривилегированного пользователя (
USER 1001в Dockerfile); - Отключайте
psдля других пользователей (hidepid=2в/procна хосте); - Никогда не логируйте
os.environилиsys.argvцеликом.
✅ Валидация webhook-запросов
- При установке webhook передавайте
secret_token:await bot.set_webhook(
url="https://example.com/webhook",
secret_token="c8b3f9a1e2d4..." # генерируется один раз, хранится в секрете
) - На стороне сервера проверяйте заголовок:
from fastapi import Header, HTTPException
@app.post("/webhook")
async def webhook(
update: dict,
x_telegram_bot_api_secret_token: str | None = Header(default=None)
):
if x_telegram_bot_api_secret_token != Config.WEBHOOK_SECRET_TOKEN:
raise HTTPException(status_code=403)
# обработка update
✅ Ротация токенов
- Раз в 3–6 месяцев —
/revokeчерез BotFather и обновление токена в инфраструктуре; - При подозрении на утечку — немедленно
/revoke.
2. SSRF (Server-Side Request Forgery) через методы Bot API
Некоторые методы Bot API позволяют боту загружать файлы по URL, переданному клиентом. Это создаёт уязвимость SSRF.
Угрожаемые методы
sendPhoto,sendVideo,sendDocument,sendAudio,sendVoice,sendAnimation— параметрphoto,videoи т.д. может быть URL, а неfile_idилиInputFile.setWebhook— параметрurl(только при вызове от имени бота, но если у вас API для управления ботами — угроза).
Сценарий атаки
- Пользователь отправляет боту команду:
/upload https://169.254.169.254/latest/meta-data/ - Бот вызывает
bot.send_photo(chat_id, photo="https://169.254.169.254/...") - Telegram-сервер выполняет HTTP-запрос к указанному URL и возвращает содержимое (например, IAM-роли AWS EC2)
- Бот отправляет ответ пользователю — утечка внутренних данных.
Контрмеры
✅ Запрет внешних URL в медиа
- Всегда проверяйте, что входящий URL — из доверенного домена:
from urllib.parse import urlparse
ALLOWED_MEDIA_DOMAINS = {"i.imgur.com", "cdn.example.com"}
def is_safe_url(url: str) -> bool:
try:
parsed = urlparse(url)
return parsed.scheme in ("http", "https") and parsed.netloc in ALLOWED_MEDIA_DOMAINS
except:
return False - Лучше — не принимать URL от пользователя вообще. Загружайте файлы через
InputFileиз локального хранилища или через прокси-валидатор.
✅ Использование прокси-валидатора
- Реализуйте промежуточный сервис: бот → ваш сервер → (валидация + загрузка) → Telegram.
- Валидация: проверка MIME-типа, размера, сканирование на вирусы (ClamAV), ограничение доменов.
✅ Отказ от setWebhook в пользовательском интерфейсе
- Если у вас админка для управления ботами — валидируйте URL по белому списку:
import re
WEBHOOK_URL_RE = re.compile(r"^https://([a-z0-9\-]+\.)*example\.com(:\d+)?/webhook/?$")
3. Фишинг и социальная инженерия через интерфейс бота
Telegram предоставляет богатые возможности для UI: кнопки, меню, веб-приложения. Неправильное их использование создаёт векторы фишинга.
Угрозы
| Угроза | Пример | Последствия |
|---|---|---|
| Поддельные inline-кнопки | Кнопка {"text": "✅ Подтвердить вход", "url": "https://fake-login.example.com"} | Перенаправление на фишинговый сайт под видом официального сервиса |
| Веб-приложения (Web Apps) без валидации | https://your-app.com/auth?tgWebAppData=... — не проверяется подпись | Подделка данных пользователя (user_id, имя), авторизация под чужим аккаунтом |
| Команды без контекста | /confirm 12345 — без проверки, что пользователь инициировал операцию | Выполнение действия по чужой команде (например, подтверждение платежа) |
Контрмеры
✅ Валидация tgWebAppData в Web Apps
- Все данные, передаваемые через
window.Telegram.WebApp.initData, подписаны секретным ключом, производным от токена. - На бэкенде проверяйте подпись:
import hmac
import hashlib
from urllib.parse import unquote
def validate_webapp_data(data: str, token: str) -> bool:
# Разбираем initData
pairs = [pair.split('=', 1) for pair in data.split('&')]
pairs.sort(key=lambda x: x[0])
hash_val = None
data_check_string = []
for key, value in pairs:
if key == 'hash':
hash_val = value
else:
data_check_string.append(f"{key}={unquote(value)}")
if not hash_val:
return False
# Генерация секретного ключа
secret_key = hmac.new(b"WebAppData", token.encode(), hashlib.sha256).digest()
# Проверка хеша
computed_hash = hmac.new(secret_key, "\n".join(data_check_string).encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(computed_hash, hash_val)
✅ Явное указание домена в кнопках
- Для
url-кнопок используйте полные, проверяемые URL:from telegram import InlineKeyboardButton
# ПЛОХО:
# InlineKeyboardButton("Войти", url=user_input_url)
# ХОРОШО:
button = InlineKeyboardButton(
"Войти в личный кабинет",
url=f"https://secure.example.com/login?user_id={user_id}&nonce={secrets.token_urlsafe(16)}"
) - Добавляйте
nonce(одноразовый токен) для защиты от повторного использования.
✅ Контекстная привязка команд
- Для команд вроде
/confirmхраните ожидаемое действие вuser_data:# При инициации операции:
context.user_data["pending_action"] = {
"type": "ban",
"target_id": 123,
"nonce": secrets.token_hex(8)
}
# В /confirm:
nonce = update.message.text.split()[-1]
expected = context.user_data.get("pending_action")
if not expected or expected["nonce"] != nonce:
await update.message.reply_text("❌ Недействительная команда.")
return
4. Инъекции и XSS в форматировании сообщений
Telegram поддерживает HTML и MarkdownV2. Некорректная экранировка пользовательских данных приводит к:
- Разрыву разметки — искажение интерфейса;
- XSS-like эффектам — например, внедрение скрытых ссылок;
- Утечке данных — через
tg://-ссылки (например,tg://resolve?domain=fake_bot).
Контрмеры
✅ Жёсткая экранировка при parse_mode="HTML"
- Используйте
html.escape()для всех переменных:from html import escape
user_name = escape(update.effective_user.full_name)
await update.message.reply_text(
f"Привет, <b>{user_name}</b>!",
parse_mode="HTML"
)
✅ Предпочтение MarkdownV2 с экранированием спецсимволов
- В
python-telegram-botесть утилита:from telegram.helpers import escape_markdown
text = f"Ваш баланс: *{escape_markdown(str(balance), 2)}* ₽"
await update.message.reply_text(text, parse_mode="MarkdownV2")
✅ Запрет tg://-ссылок в пользовательском вводе
- Фильтр:
if "tg://" in message_text.lower():
await message.delete()
await log_moderation_event(context, "Обнаружена tg:// ссылка")
5. Дополнительные меры защиты
| Категория | Рекомендация |
|---|---|
| Права бота в чате | Выдавайте минимально необходимые права: can_delete_messages, can_restrict_members. Не давайте can_post_messages в каналах без необходимости. |
| Логирование | Не логируйте update целиком — он содержит text, first_name, phone_number (если разрешено). Обезличивайте: user_id, chat_id, тип события. |
| Ограничение частоты | Реализуйте rate limiting (например, aiolimiter) для команд /start, /help, чтобы предотвратить DoS. |
| Обновления зависимостей | python-telegram-bot и httpx должны быть в актуальных версиях — в них регулярно исправляют CVE (например, CVE-2023-24997 — SSRF в httpx). |
| Тестирование | Включите в CI: |
banditдля поиска утечек токенов;safety checkдля проверки уязвимостей в зависимостях;- E2E-тесты с поддельными webhook-запросами. |