惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

A
About on SuperTechFans
C
Cybersecurity and Infrastructure Security Agency CISA
N
News and Events Feed by Topic
C
Cisco Blogs
Cisco Talos Blog
Cisco Talos Blog
A
Arctic Wolf
Scott Helme
Scott Helme
P
Palo Alto Networks Blog
S
Schneier on Security
D
Darknet – Hacking Tools, Hacker News & Cyber Security
T
Tor Project blog
量子位
G
Google Developers Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
B
Blog RSS Feed
NISL@THU
NISL@THU
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
AWS News Blog
AWS News Blog
爱范儿
爱范儿
Last Week in AI
Last Week in AI
Y
Y Combinator Blog
L
LINUX DO - 最新话题
Security Archives - TechRepublic
Security Archives - TechRepublic
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
S
Secure Thoughts
Cloudbric
Cloudbric
aimingoo的专栏
aimingoo的专栏
L
Lohrmann on Cybersecurity
TaoSecurity Blog
TaoSecurity Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Hacker News: Ask HN
Hacker News: Ask HN
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
The GitHub Blog
The GitHub Blog
有赞技术团队
有赞技术团队
S
Security @ Cisco Blogs
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
C
Cyber Attacks, Cyber Crime and Cyber Security
G
GRAHAM CLULEY
P
Proofpoint News Feed
V
V2EX
Martin Fowler
Martin Fowler
C
CERT Recently Published Vulnerability Notes
Attack and Defense Labs
Attack and Defense Labs
C
CXSECURITY Database RSS Feed - CXSecurity.com
The Cloudflare Blog
SecWiki News
SecWiki News
罗磊的独立博客
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
小众软件
小众软件
The Last Watchdog
The Last Watchdog

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Production MTProto user-бот на FastAPI + Telethon: WARP для обхода DPI и 5 граблей с Telegram
viktdo · 2026-05-13 · via Все публикации подряд на Хабре

Уровень сложностиСредний

Время на прочтение14 мин

Охват и читатели183

Туториал

Production MTProto user-бот на FastAPI + Telethon: WARP для обхода DPI и 5 граблей с Telegram

В большинстве туториалов по Telegram-ботам всё начинается с одного куска кода: получили токен у @BotFather, поставили python-telegram-bot или aiogram, написали хендлер, deploy. Это Bot API. И в 90% задач этого хватает.

А потом приходит задача которую Bot API не закрывает в принципе: программно создать супергруппу под конкретный проект и добавить туда нужных людей по @username, и сделать это десятки раз в день. Bot API такое не умеет даже теоретически - метода «создать группу» там нет, метода «добавить юзера в группу» тоже. Лезете в полную документацию Telegram API искать обход, упираетесь в раздел  channels.createChannel  /  channels.inviteToChannel под MTProto, и начинается совсем другая история - не Bot API, а user-бот через telethon.

В этой статье разбираю как мы сделали production MTProto user-бот на FastAPI + Telethon. Под капотом: Cloudflare WARP для обхода DPI (без него с российского VPS просто не подключиться), Singleton-клиент с keepalive, in-memory cache resolve-юзеров, и 5 ограничений Telegram которые знают только те кто лез туда ногами. Реальный production-сервис у клиента в нише строительства/монтажа, обслуживает связку Planfix → Telegram-группы под каждый проект.

Сервис написан на Python 3.11. Стек: Telethon 1.43.2, FastAPI 0.136.1, Uvicorn 0.46.0, Pydantic 2.13.4. На VPS под systemd, наружу через Cloudflare Tunnel. Вызывается из n8n через HTTP-ноду.

Когда MTProto user-бот вообще нужен

Идти в MTProto имеет смысл только когда стандартный Bot API упирается в свои потолки. И не «лень разобраться», а методов нет в принципе - есть конкретные операции, которые Bot API не закрывает никак.

Что Bot API делает хорошо:

  • Отправка и редактирование сообщений

  • Обработка inline-кнопок и callback-запросов

  • Управление существующими группами/каналами (если бот добавлен админом): banChatMemberunbanChatMemberpinChatMessagesetChatTitle

  • Приём chat_join_request и approveChatJoinRequest / declineChatJoinRequest - то есть автоматический контроль входа по invite-link

Что Bot API не умеет:

  • Создавать супергруппы. Совсем. Метода нет

  • Добавлять юзеров в группу. Бот не может никого «затащить» внутрь - только принимать заявки от тех, кто пришёл по invite-link

  • Резолвить @username ↔ user_id. Никак

  • Стартовать диалог с юзером первым (нужен /start от юзера)

Каждая из этих операций есть в MTProto. Если хотя бы две из них в задаче - это уже довод за user-бота.

В нашем случае у клиента (ниша монтажа/ремонта/строительства, десятки одновременных проектов с подрядчиками и заказчиками) был запрос: каждая новая задача в Planfix должна автоматически порождать Telegram-чат со специалистами из карточки задачи. На один проект 3-10 человек. Раньше эту работу руками делал project-менеджер/офис-менеджер: создаёшь группу, ищешь людей в адресной книге, добавляешь, потом меняется состав проекта - снова правишь руками. На бот эта роль переносится почти на 100%.

Конкретно через MTProto закрываем:

  1. Создание супергруппы (только MTProto)

  2. Добавление участников по @username из карточки Planfix (только MTProto)

  3. Resolve id ↔ @username (только MTProto)

Bot API в той же связке используется для оперативного управления уже созданной группой: banChatMember/unbanChatMember при изменении состава проекта, approveChatJoinRequest для автоматического одобрения вернувшихся через invite-link, pin сообщений и отправка статусов. Получается гибридная архитектура: создание и пополнение группы через MTProto user-бота, остальное через Bot API.

Архитектура сервиса

Перед кодом - общая картина. Что есть в production:

   ┌──────────────────────────────────────┐
   │      Planfix (event source)          │
   └────────────────┬─────────────────────┘
                    │ webhook
                    ▼
   ┌──────────────────────────────────────┐
   │   n8n (orchestrator, 56 нод)         │
   └────────────────┬─────────────────────┘
                    │ HTTP + X-API-Key
                    ▼
   ┌──────────────────────────────────────┐
   │  FastAPI service (mtproto-api)       │
   │  ├─ Singleton TelegramClient         │
   │  ├─ /create-group  /add-users        │
   │  │  /resolve-user  /health           │
   │  └─ keepalive 180s + ensure_connected│
   └────────────────┬─────────────────────┘
                    │ MTProto
                    ▼
   ┌──────────────────────────────────────┐
   │  Cloudflare WARP (SOCKS5 :40000)     │
   │  обход РКН-DPI                       │
   └────────────────┬─────────────────────┘
                    ▼
   ┌──────────────────────────────────────┐
   │       Telegram MTProto API           │
   └──────────────────────────────────────┘

Снаружи сервис доступен только через Cloudflare Tunnel (mtproto.example.ru → localhost:8080), порт 8080 на сам VPS наружу не пробрасывается. Аутентификация на уровне HTTP - заголовок X-API-Key (генерируется через openssl rand -base64 32), валидируется в FastAPI dependency перед каждым endpoint.

Endpoints:

Метод

Путь

Назначение

GET

/health

Статус MTProto-сессии, доступен без авторизации

POST

/create-group

Создать супергруппу, добавить бота и админов, опционально участников

POST

/add-users

Добавить участников/админов в существующую группу

POST

/resolve-user

Resolve id ↔ username, с in-memory кешем + deep_search по группам

GET

/cache-stats

Размер кеша resolve-юзеров

Запрос на /create-group выглядит так:

POST /create-group
X-API-Key: <secret>
Content-Type: application/json

{
  "title": "Проект Объект-42 Москва",
  "description": "Команда проекта",
  "admins": ["@admin_username"],
  "users": ["@team_member1", "@team_member2"],
  "request_approval": true
}

В ответе - chat_id в формате Bot API (с префиксом -100, для дальнейших вызовов Bot API), invite_link, и критичная штука для production - словари *_errors с pereason'ом если кого-то не получилось добавить:

{
  "status": "ok",
  "chat_id": -1001234567890,
  "invite_link": "https://t.me/+abc...",
  "bot_added": true,
  "admins_added": ["admin_username"],
  "admins_errors": {},
  "users_added": ["team_member1"],
  "users_errors": {"team_member2": "user_privacy_restricted"}
}

То есть HTTP-статус 200, группа создана, но team_member2 не добавлен с указанной причиной. n8n дальше может попробовать fallback - отправить приглашение через бота лично для каждого юзера из *_errors, а на стороне юзера это будет персональный invite-link, по которому он сам кликнет и попадёт в группу.

Теперь к самой тяжёлой части - почему всё это вообще работает с российского VPS.

Главный блокер: РКН-DPI на уровне MTProto-протокола

Если вы попробуете запустить Telethon-клиент с российского VPS напрямую, то увидите красивое:

ConnectionError: Connection to Telegram failed 5 time(s)

Не resolve hostname'а, не TLS-проблема, не firewall. Проблема в том, что РКН-DPI распознаёт MTProto-протокол по сигнатуре пакета и режет соединение на уровне TCP. Обфускация, встроенная в Telethon (ConnectionTcpObfuscated), на это не реагирует - DPI её прекрасно видит.

Когда поднимал сервис в первый раз без обхода блокировки, перепробовал:

  • Альтернативные DC (datacenters Telegram) - блокируются одинаково

  • Все варианты connection в Telethon (ConnectionTcpFullConnectionTcpIntermediateConnectionTcpAbridgedConnectionTcpObfuscated) - результат тот же

Финальное рабочее решение - Cloudflare WARP в режиме SOCKS5 на 127.0.0.1:40000. Что это:

  1. WARP - VPN-клиент Cloudflare, есть консольная версия warp-cli под Linux. Бесплатный

  2. После warp-cli connect - весь исходящий трафик с VPS идёт через CF-сеть. Внешний IP меняется на CF Moscow (DPI его не видит, потому что трафик зашифрован между VPS и CF и снаружи выглядит как обычный HTTPS на CF)

  3. Telethon настраивается на использование SOCKS5 через локальный порт 40000 (стандартный WARP SOCKS-режим)

В Telethon это выглядит так:

from telethon import TelegramClient
from telethon.network.connection import ConnectionTcpFull
import socks

PROXY = (socks.SOCKS5, "127.0.0.1", 40000)

client = TelegramClient(
    "session",
    API_ID,
    API_HASH,
    connection=ConnectionTcpFull,
    proxy=PROXY,
)

После этого client.start() подключается без проблем. Проверка живости WARP:

warp-cli status        # ожидается: Connected
curl --socks5 127.0.0.1:40000 -m 10 https://ipinfo.io/
# должен вернуть IP в формате Cloudflare Moscow

WARP стартует автоматически после systemctl enable warp-svc. За всё время работы сервиса WARP не падал ни разу - стартует с VPS, работает непрерывно. Единственный риск который видел - после reboot VPS WARP поднимается не моментально, и если mtproto-api.service стартует раньше, первый коннект к Telegram падает с ConnectionError. Решение - startup-зависимость mtproto-api.service от warp-svc:

[Unit]
Description=MTProto FastAPI service
After=network-online.target warp-svc.service
Requires=warp-svc.service

[Service]
ExecStart=/opt/mtproto-api/venv/bin/uvicorn app:app --host 127.0.0.1 --port 8080
Restart=on-failure

После этого периодические reconnect'ы в journald - это keepalive после idle-таймаута, не WARP-инциденты.

Альтернативы WARP'у - платный VPS за границей с проксированием на РФ либо свой MTProxy на foreign-хосте. Дороже, сложнее, чаще обрывается, плюс ещё один компонент в цепочке. WARP бесплатный, нативно поддерживается на Linux, и снимает проблему DPI на уровне сетевого слоя.

Singleton TelegramClient - почему это критично

Базовое правило, которое я установил для себя жирной красной чертой после двух инвалидаций сессии: один MTPROTO_SESSION = один TelegramClient в один момент времени. Никаких параллельных Python-процессов с одной и той же session-строкой.

Что происходит при нарушении правила (реальный инцидент на этапе разработки сервиса): я запустил отдельный отладочный скрипт check_user.py параллельно с уже работающим mtproto-api. Оба клиента стучались в Telegram с одинаковым session-string. Telegram security-инфраструктура ловит это как подозрительную активность («один аккаунт коннектится с двух мест одновременно») и присылает AUTH_KEY_UNREGISTERED старшему по времени подключения. Сессия инвалидирована, сервис падает, восстановление через reauth.

Дважды за один день влетел в эту яму (после первого раза думал «случайность», запустил тесты resolve-user параллельно - снова инвалидация). После этого вышло простое правило в виде файла feedback_mtproto_rules.md в репозитории сервиса:

Запрещено запускать любые скрипты с Telethon на ту же MTPROTO_SESSION пока работает mtproto-api. Для интерактивных тестов - отдельная сессия на тестовом номере или остановка сервиса перед запуском.

В коде сервиса это реализовано Singleton-паттерном через FastAPI lifespan:

from contextlib import asynccontextmanager
from fastapi import FastAPI
from telethon import TelegramClient

@asynccontextmanager
async def lifespan(app: FastAPI):
    # startup
    app.state.client = TelegramClient(
        SESSION, API_ID, API_HASH,
        connection=ConnectionTcpFull,
        proxy=(socks.SOCKS5, "127.0.0.1", 40000),
    )
    await app.state.client.start()
    # warmup entity cache
    async for _ in app.state.client.iter_dialogs(limit=300):
        pass
    # background keepalive
    app.state.keepalive_task = asyncio.create_task(_keepalive(app.state.client))
    yield
    # shutdown
    app.state.keepalive_task.cancel()
    await app.state.client.disconnect()

app = FastAPI(lifespan=lifespan)

Каждый endpoint берёт клиента из request.app.state.client, никакой client = TelegramClient(...) внутри обработчиков нет. Гарантировано один экземпляр на процесс.

Восстановление после инвалидации (~5 минут):

  1. systemctl stop mtproto-api

  2. pgrep -af telethon - убедиться что параллельных процессов нет

  3. Атомарный reauth-скрипт через systemd-run --user --scope (не nohup/tmux через SSH, иначе скрипт умрёт при разрыве SSH-сессии и аккаунт зависнет в полу-авторизованном состоянии)

  4. Получаем новый session-string. Важно: запускать с того же IP что и production (тот же WARP), иначе Telegram security ставит флаг «вход с нового IP» и не даёт восстановить через SMS

После того случая правило соблюдается строго и ни одной инвалидации больше не было.

Ещё четыре грабли с Telegram

С DPI и инвалидацией разобрались. Теперь короче - четыре оставшихся ограничений, на которых я лично спотыкался.

Mutual contact и почему свежие группы пополняются легче

InviteToChannelRequest (он же channels.inviteToChannel в MTProto) может выкинуть USER_NOT_MUTUAL_CONTACT если у добавляемого юзера privacy My Contacts или Nobody, а сессионный аккаунт не в его контактах. В официальной документации Telegram перечислены коды ошибок (USER_NOT_MUTUAL_CONTACTUSER_PRIVACY_RESTRICTEDUSER_BLOCKEDUSER_KICKED и ещё пятнадцать), но точные условия срабатывания не раскрываются - только краткие текстовые описания.

Эмпирическое наблюдение из практики: в первые часы после создания супергруппы Telegram пропускает добавление через inviteToChannel чаще, чем при добавлении в существующую группу которой неделя/месяц. Полагаться на это нельзя - такое поведение не зафиксировано в документации, может измениться в любой момент и без предупреждения. Архитектурно нужно всегда иметь fallback на invite-link с request_approval=true.

Практический вывод для воркфлоу:

  1. При создании группы сразу добавлять всех начальных участников (большая часть пройдёт)

  2. Для каждого, кто вернулся в users_errors - сразу отправлять invite-link через бота в личку

  3. Для всех новых членов через дни/недели после создания - использовать только invite-link + автоодобрение через Bot API при chat_join_request event

Главное: не строить архитектуру на предположении «у нас будет окно когда можно добавлять любых». Окно может схлопнуться - и схлопнется когда вам надо будет добавить ключевого юзера в проект.

После удаления из группы повторное добавление не работает

Если юзер был забанен через Bot API (banChatMember) или вышел сам - повторное добавление через InviteToChannelRequest уже не сработает. Telegram отдаст UserNotMutualContactError даже если технически до этого они были mutual contact, либо запрос пройдёт без ошибки но юзер фактически в группе не появится. Поведение зависит от конкретной комбинации privacy-настроек юзера и истории его действий, и формальной спеки на это нет.

Сценарий: специалист поработал на проекте, проект закрыли, его удалили из группы через Bot API. Через месяц проект возобновили - в Planfix снова появилось его ФИО в карточке задачи. Прямой add-user через MTProto - не работает.

Рабочая схема:

  1. При удалении специалиста из проекта - banChatMember через Bot API (или kickChatMember, что для супергрупп то же самое - юзер уходит в banned list)

  2. При возврате специалиста в проект - сначала unbanChatMember (вывести из banned list), затем сформировать персональный invite-link с request_approval=true и отправить юзеру в личку через бота

  3. Когда юзер кликает на invite-link, Telegram присылает chat_join_request на webhook

  4. Bot API workflow проверяет: есть ли его user_id в списке участников этого проекта в нашей DataTable? Если есть - approveChatJoinRequest, если нет - declineChatJoinRequest

Через unban + invite-link + автоодобрение петля «удалили → вернули» работает прозрачно для специалиста: он получает приглашение в личку, кликает и попадает в группу за пару секунд без человеческого менеджера в цикле.

access_hash и почему нельзя добавлять по numeric id

Telethon не может разрешить произвольного юзера по числовому user_id без access_hash. Access_hash - это security-токен, который Telegram выдаёт только сессии которая уже видела этого юзера (общий чат, контакт, открытый диалог).

В Bot API эта проблема скрыта (API возвращает chat_id с которым можно работать дальше). В MTProto access_hash - отдельная сущность, которую нужно где-то взять и где-то хранить.

Best practice: всегда передавать @username вместо id в /create-group и /add-users. Telethon ресолвит username через client.get_input_entity('@username') - это работает без access_hash, потому что username сам по себе достаточен.

Если в системе есть только id (например, прилетел из webhook Bot API), есть две стратегии:

  • Сначала прогнать через /resolve-user с deep_search=true - итерируем участников всех групп где сессия есть, ищем нужный id

  • Добавлять через invite-link (без необходимости в access_hash, юзер сам кликнет)

Deep_search занимает 5-60 секунд (зависит от количества групп), поэтому используем его только когда без id никак.

Лимиты Telegram и стратегия retry

Telegram не публикует точные числовые лимиты на rate операций - в официальной документации указан только максимум участников супергруппы (200 000), а на остальное вы натыкаетесь через FLOOD_WAIT exception с конкретным retry_after в секундах для вашей сессии в вашей ситуации. То есть лимит динамический и зависит от реальной нагрузки на DC + истории аккаунта.

У нас bulk-операций нет в принципе: бизнес-специфика - средний проект 3-10 человек в группе, массовых добавлений не делаем. Точных потолков по InviteToChannelRequest и созданию супергрупп в час не нащупывали - до FLOOD_WAIT не доходим из-за самой природы задачи.

Тем не менее заложили защиту от FLOOD_WAIT на уровне сервиса - если задача когда-нибудь поменяется и потребуется bulk:

  • asyncio.Lock в FastAPI сериализует все Telegram-вызовы → один Telethon-запрос за раз внутри сервиса, никаких параллельных API-вызовов

  • В n8n между batch-операциями ставлю Wait 3-10 секунд с jitter (хотя в реальной нагрузке между задачами Planfix проходят минуты)

  • На уровне endpoint - retry x2 при ConnectionError

  • При FloodWaitError от Telethon - читаем e.seconds, ждём seconds + 5, делаем один повтор; если упало повторно - возвращаем 429 в HTTP-ответ с тем же seconds в payload, дальше уже n8n решает что делать

Если когда-нибудь придётся делать массовое добавление - правильнее посылать один InviteToChannelRequest с массивом всех username сразу, чем тот же объём через отдельные вызовы. Один API-запрос дешевле по rate-budget.

Production-механика: что ещё внутри

Большую часть граблей разобрал, теперь основные паттерны, которые делают сервис стабильным.

ensure_connected перед каждым endpoint. Telethon может «висеть в connected, но send падает» при долгом idle. Поэтому перед любым API-вызовом - проверка client.is_connected() и переподключение если упало:

async def ensure_connected(client: TelegramClient):
    if not client.is_connected():
        await client.connect()
    # дополнительный health-check
    try:
        await asyncio.wait_for(client.get_me(), timeout=3)
    except (asyncio.TimeoutError, ConnectionError):
        await client.disconnect()
        await client.connect()

Keepalive 180 секунд. Background-task, который раз в три минуты дёргает get_me(). Без него Telegram закрывает idle-соединение, и первый запрос после долгого простоя падает. С keepalive - соединение всегда живое.

Warmup iter_dialogs(limit=300) при старте. Telethon кеширует entity-объекты юзеров и групп, которые встречает. При старте мы загружаем последние 300 диалогов (~5-10 секунд), что наполняет кеш. После этого client.get_input_entity('@username') работает мгновенно, без round-trip'а к Telegram.

In-memory cache resolved юзеров. В дополнение к Telethon-кешу, держу свой dict[int, dict] в памяти сервиса. Когда /resolve-user возвращает результат, кладём в кеш. Очищается при рестарте - приемлемо, потому что warmup восстановит большую часть.

Post-invite verification. После InviteToChannelRequest сервис делает дополнительный GetParticipantRequest чтобы убедиться что юзер реально в группе. Если в users_added - это подтверждённое добавление, не «отправили запрос и надеемся». Это важно потому что Telegram иногда говорит «ок, добавлено», но фактически добавление откатывается без ошибки - GetParticipantRequest ловит это и переводит юзера в *_errors.

Безопасность

Короткие правила, которые в production-сервисе с Telegram-сессией нельзя нарушать:

  1. Один client per session - строго (разбирал выше)

  2. Не коммитить .env с MTPROTO_SESSION в git. Никогда. Session-string даёт полный доступ к аккаунту, эквивалентен паролю

  3. Не логировать тело запросов - содержит user_id, который в связке с @username может раскрыть приватные данные

  4. CORS не настраивать - сервис вызывается только server-side из n8n, фронтенд туда не ходит

  5. systemd-run для интерактивных операций, особенно reauth - nohup и tmux через SSH ненадёжны при разрыве SSH-сессии, скрипт умирает на середине, аккаунт остаётся в полу-авторизованном состоянии

Что в итоге

Production MTProto user-бот сводится к набору правил, многие из которых неочевидны до того как нарушишь их:

  • Singleton-клиент на сервис (не нарушать)

  • WARP SOCKS5 для обхода DPI с РФ-VPS

  • Mutual contact и льготный период - спроектировать архитектуру под это

  • Opt-out юзера - fallback на invite-link с автоодобрением

  • @username всегда лучше чем numeric id

  • Keepalive + ensure_connected + warmup - паттерны без которых стабильности не будет

Сам по себе сервис не сложный - основная масса кода это правильная обработка ошибок и edge cases. Самое сложное - понять что именно эти 5 граблей существуют, до того как сервис уехал к клиенту в production.

Делаем подобные интеграции в BotKraft - пишите если у вас похожий кейс с автоматизацией Telegram под бизнес-процессы.

Полезные ссылки