Перейти к основному содержимому

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)

  1. Получение — Telegram доставляет JSON-пакет (update) либо через getUpdates, либо POST на webhook.
  2. Десериализация — библиотека преобразует JSON в объекты типов Update, Message, User, Chat и др. Все поля строго типизированы.
  3. Препроцессинг — проход через Middleware. Позволяет логировать, кэшировать, проверять права до попадания в обработчики.
  4. Фильтрация — система BaseFilter и MessageFilter позволяет отсеивать нежелательные события (например, игнорировать ботов, оставлять только текстовые сообщения).
  5. Маршрутизация — выбор подходящего Handler (например, CommandHandler для /start, MessageHandler для обычных сообщений).
  6. Обработка — вызов пользовательской корутины (async def) с передачей update и context: ContextTypes.DEFAULT_TYPE.
  7. Постпроцессинг — необязательный этап (например, логирование ответа, сбор метрик).

Важно: все обработчики должны быть асинхронными (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
)

Запуск через uvicornProcfile или 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 для управления ботами — угроза).

Сценарий атаки

  1. Пользователь отправляет боту команду: /upload https://169.254.169.254/latest/meta-data/
  2. Бот вызывает bot.send_photo(chat_id, photo="https://169.254.169.254/...")
  3. Telegram-сервер выполняет HTTP-запрос к указанному URL и возвращает содержимое (например, IAM-роли AWS EC2)
  4. Бот отправляет ответ пользователю — утечка внутренних данных.

Контрмеры

Запрет внешних 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-запросами. |