Как мы построили IAM для Telegram поверх Telethon и автоматизировали управление сотней корпоративных Telegram-чатов

от автора

Предисловие

Всем привет! Меня зовут Семенчук Александр, я ведущий разработчик Федеральной девелоперской компании «РАЗУМ». Последние 4 года я посвятил разработке интеграционных решений, оптимизации рутинных процессов и разработке web-приложений на Django, и всегда по-детски радуюсь, когда в конечном итоге все эти категории собираются воедино и позволяют закрыть одну из новых задач. Как раз о таком опыте я и собираюсь рассказать.

Данный проект разрабатывался длительное время, когда мы начали его разрабатывать, информация напрочь отсутствовала в интернете и самое частое, что получалось найти, это десятки вопросов людей, без конкретных ответов, а Chat-GPT каждый раз гнал нас на уже пройденные грабли.

Началось 😉

Когда количество рабочих Telegram-чатов у нас приблизилось к сотне, выяснилось, что проблема уже не в добавлении одного человека в одну группу. Проблема — доказуемо выполнить десятки однотипных действий и не пропустить единственный чат. При этом не допустить утечку ресурсов (ведь кому-то на это всё понадобится тратить время, а новых или уволенных сотрудников может быть больше одного).

Нового сотрудника нужно добавить сразу в набор обязательных групп. Уволенного — вовремя исключить из всех чатов, включённых в корпоративную политику доступа, чтобы у него не сохранялась возможность наблюдать за внутренней деятельностью компании. Служебного бота тоже нужно добавить, а иногда сразу назначить администратором.

В этой статье расскажу, как мы построили отдельный модуль управления корпоративными чатами и каналами на Django, PostgreSQL и Telethon. Без привязки к нашей внутренней инфраструктуре — только архитектура, технические решения и несколько выводов, которые могут пригодиться при решении похожей задачи.

Мы прошли большой путь, наступали на кучу граблей, радовались каждой маленькой победе на этом пути. В конечном итоге нам удалось собрать стабильный проект, который ежедневно экономит время сотрудников и работает за нас.

Сразу обозначу границы: речь не идёт о массовых приглашениях, чужих аудиториях или коммерческих сообществах. Основной объект управления — сотрудники компании, связанные с корпоративными учётными записями и кадровыми процессами. Таким образом мы не нарушаем ToS Telegram и можем не бояться заморозки технического аккаунта. Отдельно привилегированный администратор может добавить явно выбранного служебного бота или пользователя по @username; это ручной сценарий, а не механизм поиска и обработки внешней аудитории.

Откуда появилась задача

У нас уже существовала корпоративная система с профилями сотрудников и структурой. В ней хранятся рабочие учётные записи, подразделения, статус занятости и контактные данные. Telegram при этом развивался параллельно: новые группы создавались под проекты и подразделения, обычные группы превращались в супергруппы, менялись названия и администраторы.

Главные сценарии выглядели так:

  • добавить нового сотрудника во все нужные для него чаты;

  • исключить уволенного сотрудника из всех чатов, включённых в обязательную политику доступа;

  • вручную добавить или удалить сотрудника в выбранных группах;

  • добавить пользователя или бота по @username;

  • при необходимости сразу назначить добавленного пользователя или бота администратором;

  • регулярно обновлять список чатов, их типы, названия и доступные права;

  • синхронизировать Telegram-контакты с корпоративными профилями;

  • видеть результат каждой операции, включая частичные ошибки.

Самая неприятная особенность ручного процесса — его трудно проверить. Можно удалить человека из 30 групп, пропустить 31-ю и не заметить этого. Поэтому нам была нужна не просто кнопка «удалить везде», а система с очередью, идемпотентностью, аудитом и результатом по каждому чату, которая бы могла в фоне обслуживать рабочие чаты без ручного вмешательства (хоть мы его и предусмотрели).

Почему Telethon и сервисный пользователь, а не только Bot API

Первое решение, которое нужно принять, — от чьего имени работать с Telegram.

Bot API отлично подходит для ботов, которые обслуживают конкретные чаты. Но в нашем случае требовалось:

  • видеть полный список диалогов сервисной учётной записи;

  • работать с обычными группами, супергруппами и каналами;

  • разрешать пользователей через Telegram contacts;

  • сопоставлять сотрудников по телефону;

  • приглашать пользователей и других ботов;

  • корректно обрабатывать миграцию обычной группы в супергруппу.

Но в основном — Bot API довольно ограничен в части получения информации о пользователях, что делает невозможным полноценное извлечение информации об участнике чата и связывание его с корпоративной учётной записью.

Поэтому мы использовали Telethon — асинхронный Python-клиент для MTProto — и выделенную служебную Telegram-учётную запись. Она добавлена только в корпоративные чаты и получает минимально необходимые административные права.

Это важный организационный момент: не стоит использовать личный аккаунт разработчика или руководителя. У сервисной учётной записи должен быть владелец со стороны компании, регламент восстановления, включённая двухфакторная аутентификация и контролируемый список активных сессий.

Главный архитектурный принцип: Telegram не живёт внутри web-процесса

Telethon построен вокруг долгоживущего асинхронного соединения. Django под Apache или Gunicorn, наоборот, работает с короткими HTTP-запросами и несколькими процессами. Попытка создать общий TelegramClient внутри web-приложения обычно заканчивается одним из следующих сценариев:

  • несколько процессов используют одну и ту же сессию;

  • event loop создаётся и уничтожается в неожиданный момент;

  • долгий Telegram-запрос блокирует HTTP request;

  • после перезапуска web-процесса теряется выполняемая операция;

  • становится невозможно понять, кто сейчас владеет соединением.

Мы разделили контур на три части:

   HR-событие из 1с              │              ▼        Django web-приложение              │ создаёт операцию              ▼     PostgreSQL: очередь и аудит              │              ▼   отдельный Telethon worker ───► Telegram MTProto API   systemd timer ───► команда постановки sync в PostgreSQL

Web-приложение только валидирует запрос, создаёт операцию в PostgreSQL и сразу возвращает её идентификатор. Оно не подключается к Telegram.

Отдельный worker постоянно держит один TelegramClient, забирает операции из очереди и сохраняет результат. Периодические синхронизации запускаются отдельным systemd timer.

Такой подход оказался значительно проще в эксплуатации, чем попытки встроить асинхронный Telegram runtime в жизненный цикл WSGI-приложения.

Почему очередь сделали на PostgreSQL

Для этой задачи можно было использовать Celery и Redis или RabbitMQ. Но отдельный брокер нам не был обязателен: объём небольшой, операции важнее скорости, а PostgreSQL уже является критической частью системы.

В базе есть две основные сущности:

  • операция — например, «исключить сотрудника» или «синхронизировать чаты»;

  • цель операции — результат обработки конкретного чата.

Упрощённо это выглядит так (псевдокод, не Django ORM):

class Operation:    id: UUID    type: str    employee_id: int | None    status: str    idempotency_key: str    attempts: int    lease_expires_at: datetime | None    next_retry_at: datetime    payload: dictclass OperationTarget:    operation_id: UUID    peer_id: int    desired_state: str    status: str    error_code: str | None    telegram_result: dict

Worker забирает задания через SELECT ... FOR UPDATE SKIP LOCKED, устанавливает lease и регулярно обновляет heartbeat. Если процесс завершается аварийно, операция не теряется: после истечения lease её подхватит следующий запуск worker.

Для временных ошибок используется retry с увеличивающейся задержкой. Для FloodWait мы не угадываем паузу, а используем точное количество секунд, которое вернул Telegram.

У этой схемы есть ещё одно полезное свойство: прогресс хранится по чатам. Если сотрудник успешно удалён из 80 групп, а в двух не хватило прав, повторная попытка обрабатывает только эти две группы.

Идемпотентность важнее скорости

HR-система может повторно отправить событие, HTTP-клиент — не получить ответ из-за сетевого таймаута, а администратор — дважды нажать кнопку. Поэтому каждая кадровая операция получает внешний идентификатор события, а в базе создаётся уникальный idempotency key.

Повторный запрос с тем же событием возвращает существующую операцию, а не создаёт новую.

При этом повторное увольнение после возврата сотрудника в компанию должно быть новой операцией. Поэтому ключ строится не только из email, но и из идентификатора кадрового события и версии политики.

Для удаления мы используем desired-state подход:

желаемое состояние: сотрудник не состоит в чате

Если Telegram сообщает, что пользователь уже не является участником, это не ошибка, а успешно достигнутое состояние. Такой подход сильно упрощает повторные запуски.

Как связать корпоративного сотрудника с Telegram

Это один из самых сложных участков интеграции.

Telegram user ID сам по себе недостаточен для произвольного MTProto-запроса. Во многих случаях требуется пара user_id + access_hash. Кроме того, get_entity(numeric_id) может работать на машине разработчика благодаря локальному entity cache и перестать работать в чистом production-окружении.

Мы построили отдельную модель Telegram identity, в которой храним:

  • Telegram user ID;

  • access hash;

  • username;

  • нормализованный телефон;

  • ссылку на корпоративный профиль;

  • источник и время последнего подтверждения связи.

Основной ключ сопоставления — телефон. При синхронизации мы:

  1. берём профили сотрудников, переданные корпоративной системой в контур синхронизации;

  2. приводим телефоны к единому международному формату;

  3. импортируем их в contacts служебной Telegram-учётной записи;

  4. получаем актуальные Telegram contacts;

  5. связываем запись только при единственном однозначном совпадении;

  6. неоднозначные случаи отправляем в список конфликтов для ручной проверки.

Мы сознательно не связываем людей по похожему имени или username. Фамилии и отображаемые имена не уникальны, а username пользователь может изменить.

Телефон при этом не должен попадать в прикладные логи или audit payload. Для диагностики достаточно идентификаторов операции, сотрудника и стабильного кода ошибки.

Импорт телефона в contacts внешнего сервиса — отдельная обработка персональных данных, а не просто техническая деталь. До запуска такого контура нужно определить правовое основание, круг сотрудников, сроки хранения, правила обработки уволенных и переиспользованных номеров. В нашей архитектуре источник и допустимый состав профилей задаёт корпоративный контур; Telegram-модуль не должен самостоятельно расширять этот список.

У Telegram нет единого типа «группа»

В пользовательском интерфейсе Telegram всё выглядит достаточно однородно, но на уровне MTProto есть несколько разных сущностей:

  • обычная группа — Chat;

  • супергруппа — Channel с признаком megagroup;

  • вещательный канал — Channel с признаком broadcast.

Для них используются разные запросы.

Например, добавление участника:

обычная группа: messages.AddChatUserRequestсупергруппа/канал: channels.InviteToChannelRequest

Удаление тоже отличается:

обычная группа: messages.DeleteChatUserRequestсупергруппа/канал: ban, затем unban

Мы используем ban + unban, чтобы человек был исключён, но не оставался навсегда заблокированным и мог снова вступить после повторного найма или по новому приглашению.

Ещё одна особенность — обычная группа может быть преобразована в супергруппу. После этого меняются ID, тип peer и набор допустимых запросов. Поэтому в модели чата есть ссылка migrated_to, а все операции сначала разрешают канонический peer. Старую запись мы архивируем, но сохраняем для истории операций.

Это не теоретическая тонкость. Если отправить DeleteChatUserRequest или EditChatAdminRequest с ID уже мигрировавшей группы, Telegram может вернуть ChatIdInvalidError. Поэтому тип peer нельзя определять по названию или историческому строковому полю — только по актуальной Telegram entity.

EntityResolver вместо надежды на кэш Telethon

Чтобы вся логика разрешения Telegram entity не разъехалась по worker, мы вынесли её в отдельный EntityResolver.

Синхронизация диалогов заранее сохраняет тип peer, raw ID, access hash и сведения о миграции. Во время операции resolver сначала переходит по migrated_to, а затем строит InputPeerChat или InputPeerChannel в зависимости от канонического типа. Если Telegram прямо сообщает Chat.migrated_to, связь обновляется в базе.

Для связанного сотрудника основной путь — сохранённый InputUser(user_id, access_hash). Если access hash ещё неизвестен, resolver ищет подтверждение в contacts и, в контексте конкретного чата, среди его участников. Username используется для явно введённых вручную пользователей и ботов, а не как автоматический критерий связи с сотрудником.

После авторитетного ответа access hash обновляется в базе. Благодаря этому production worker не зависит от случайного локального cache-файла Telethon.

Почему успешный RPC ещё не означает успешное добавление

У Telegram встречается неприятный сценарий: запрос приглашения формально выполняется без исключения, но пользователь не добавляется из-за настроек конфиденциальности.

В ответе InviteToChannelRequest может находиться список missing_invitees. Поэтому после добавления мы:

  1. Проверяем missing_invitees;

  2. Повторно запрашиваем фактическое членство;

  3. Только после этого отмечаем target успешным.

После удаления из супергруппы или канала также проверяем, что участник действительно отсутствует. Для legacy Chat текущий adapter опирается на результат DeleteChatUserRequest; таких групп становится всё меньше, но это осознанное отличие контракта.

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

Добавление ботов и назначение администратора

Через административный интерфейс можно выбрать корпоративного сотрудника или указать @username пользователя/бота. После этого выбирается одна или несколько управляемых групп.

Для операции добавления есть опция «сделать администратором». Worker сначала добавляет участника и проверяет членство, а затем выполняет отдельный запрос повышения:

обычная группа: messages.EditChatAdminRequestсупергруппа/канал: channels.EditAdminRequest

Для супергрупп и каналов права отличаются. В группе администратору нужны возможности управлять участниками, сообщениями и приглашениями. В вещательном канале — публиковать и редактировать сообщения. Мы выдаём заранее определённый набор операционных прав, но не разрешаем назначать других администраторов и не включаем анонимный режим.

Повышение — отдельная стадия, поэтому возможен частичный результат: пользователь уже состоит в чате, но сервисному аккаунту не хватает права назначать администраторов. В таком случае система не делает вид, что ничего не произошло. Она сохраняет:

{  "reached": true,  "changed": true,  "admin_granted": false}

После исправления прав администратор повторяет проблемную цель. Add-стадия выполняется идемпотентно: Telegram сообщает, что участник уже существует, после чего worker снова пытается выполнить повышение.

Offboarding: удалить и не потерять результат

Когда сотрудник увольняется, кадровая система отправляет внутреннее событие с уникальным ID. Django создаёт одну offboarding-операцию и цели для всех активных чатов, отмеченных как управляемые и обязательные. Флаг required здесь является частью политики доступа: если чат должен гарантированно участвовать в увольнении, он обязан быть включён в эту политику. Необязательные проектные чаты требуют отдельного правила жизненного цикла и не должны случайно выпадать из организационного процесса.

Worker обходит только эти targets и сохраняет один из результатов:

  • удалён;

  • уже отсутствовал;

  • не хватило прав;

  • чат недоступен;

  • пользователь не разрешён;

  • требуется повтор после FloodWait или сетевой ошибки.

Предсказуемые постоянные ограничения, например недостаток прав в одном чате, дают completed_with_warnings. Ошибки, которые нельзя безопасно считать предупреждением, переводят операцию в requires_attention. В обоих случаях успешные цели сохраняются и повторно не выполняются.

Это важно с операционной точки зрения. В реальном мире один архивный чат или временно снятое право администратора не должны отменять удаление из остальных 99 групп. Но проблемные группы при этом нельзя скрывать.

Синхронизация чатов и контактов

Список корпоративных чатов меняется независимо от нашего приложения. Поэтому отдельный systemd timer регулярно создаёт две операции:

  • синхронизацию peer;

  • синхронизацию contacts и identities.

Peer sync обновляет:

  • новые и исчезнувшие чаты;

  • название и username;

  • тип Chat/megagroup/channel;

  • access hash;

  • число участников;

  • доступные служебной учётной записи права;

  • связи мигрировавших групп.

Identity sync обновляет Telegram contacts и связи сотрудников по телефону.

Планировщик ничего не выполняет сам — он только создаёт операции. Telegram-вызовы по-прежнему делает единственный worker.

Расписание задаётся обычным systemd timer, поэтому его можно менять без изменения приложения. Например, ежедневный запуск выглядит так:

[Timer]OnCalendar=*-*-* 02:00:00Persistent=trueRandomizedDelaySec=300

Persistent=true полезен для внутреннего сервера: если он был выключен в момент запуска, systemd выполнит пропущенное задание после старта.

Полный список участников каждого чата мы не обновляем ежедневно. Это более тяжёлая операция, которая создаёт лишнюю нагрузку и повышает вероятность FloodWait. Снимок участников синхронизируется по запросу администратора со страницы конкретной группы.

Безопасность Telegram-сессии

Telethon хранит авторизацию в session. Класть обычный .session-файл рядом с кодом или тем более коммитить StringSession в Git — плохая идея.

Мы используем StringSession, но сохраняем её в базе только в зашифрованном виде. Ключ шифрования находится вне БД в secret storage окружения. В базе разрешена только одна активная production credential.

Дополнительные правила:

  • production и test используют разные StringSession;

  • worker получает эксклюзивный PostgreSQL advisory lock;

  • второй worker не может одновременно использовать ту же сессию;

  • session string, API hash, access hash, телефоны и 2FA не попадают в логи;

  • в development membership-операции разрешены только для специально отмеченных тестовых групп;

  • обычная остановка worker вызывает disconnect(), а не Telegram logout.

Logout отзовёт сессию на стороне Telegram, тогда как disconnect просто корректно закроет соединение.

Аудит и кабинет администратора

Администратору недостаточно увидеть сообщение «задача поставлена в очередь». Поэтому мы сделали отдельный кабинет операций.

В нём отображаются:

  • состояние worker и heartbeat;

  • глубина очереди;

  • история операций;

  • прогресс по чатам;

  • стабильные коды ошибок;

  • время следующей попытки;

  • подробный результат синхронизации;

  • кнопка повторения только проблемных targets.

Отдельно хранится журнал операций: кто создал операцию, откуда пришло кадровое событие, какие чаты были выбраны, агрегированный итог и текущее состояние каждой цели.

Таблица audit events защищена PostgreSQL trigger от обычных UPDATE/DELETE. Контролируемое удаление доступно только активному superuser через отдельный кодовый путь с transaction-local разрешением и само записывается в системный журнал.

Наблюдаемость

Worker пишет структурированные логи. В основных событиях присутствуют operation_id, target_id, номер попытки и worker_id; стабильный код и подробность ошибки сохраняются в состоянии цели в PostgreSQL.

Нас интересуют не только исключения, но и эксплуатационные показатели:

  • размер очереди;

  • возраст самой старой операции;

  • количество requires_attention;

  • доля операций с предупреждениями;

  • FloodWait;

  • ошибки прав;

  • состояние Telegram authorization;

  • свежесть heartbeat.

Критичный для нас показатель — offboarding не должен оставаться необработанным дольше установленного SLA.

Что пришлось тестировать

Реальные интеграционные тесты с Telegram неудобны: они медленные, меняют состояние групп и зависят от внешнего сервиса. Поэтому основной контракт Telethon adapter мы проверяем на fake client.

Тестами покрыты:

  • обычная группа;

  • супергруппа и канал;

  • мигрировавшая группа;

  • отсутствие access hash;

  • пользователь с privacy restriction;

  • missing_invitees;

  • отсутствующий участник;

  • ban + unban;

  • назначение администратором;

  • частичный успех повышения;

  • восстановление просроченного lease после остановки worker;

  • повторное кадровое событие;

  • development-ограничение на production-чаты.

Живые проверки выполняются отдельно на безопасной тестовой группе и не входят в обычный CI.

Что в итоге получила компания

Модуль не пытается заменить Telegram или корпоративную IAM-систему. Он закрывает конкретный разрыв между кадровым событием, корпоративной учётной записью и фактическим доступом к рабочим чатам.

Для администратора процесс теперь выглядит так:

  1. выбрать сотрудника или бота;

  2. выбрать группы;

  3. указать действие и при необходимости роль администратора;

  4. получить операцию с результатом по каждому чату.

Для HR offboarding выполняется автоматически и идемпотентно. При этом остаётся понятный журнал: где человек удалён, где уже отсутствовал, а где нужно поправить права сервисной учётной записи.

Главный эффект здесь не в экономии нескольких минут на одном сотруднике. Ценность появляется на масштабе: для чатов, включённых в обязательную политику, результат offboarding становится проверяемым, а пропущенная группа — видимой как отдельная проблема, а не скрытая ручная ошибка.

Выводы, которые могут пригодиться

Если вам предстоит решать похожую задачу, я бы выделил несколько принципов.

  1. Не запускайте Telethon внутри web-процесса. Один долгоживущий worker значительно проще и надёжнее.

  2. Не полагайтесь на numeric ID без access hash. Локальный entity cache легко создаёт ложное ощущение работоспособности.

  3. Различайте Chat, megagroup и channel. Для них нужны разные MTProto requests.

  4. Проверяйте постусловие. Успешный RPC не всегда означает, что пользователь действительно добавлен.

  5. Храните результат по каждой группе. Частичный успех — нормальная ситуация, а не исключение из архитектуры.

  6. Делайте offboarding идемпотентным. «Уже отсутствует» — это достигнутое состояние.

  7. Учитывайте миграцию групп. Старый ID обычной группы нельзя бесконечно использовать после превращения в супергруппу.

  8. Не подменяйте реальные права флагами в БД. Возможность приглашать и исключать участников должна приходить из актуальной Telegram entity.

  9. Шифруйте сессию и разделяйте окружения. Telegram-сессия фактически является ключом доступа.

  10. Связывайте только корпоративных сотрудников. У такой автоматизации должны быть понятные границы, основание и владелец процесса, чтобы не схватить заморозку аккаунта.

Telegram не предоставляет готовую корпоративную IAM-модель для десятков внутренних групп. Но если отделить web от MTProto runtime, хранить desired state и аккуратно работать с entity resolution, поверх Telegram можно построить вполне предсказуемый и контролируемый контур управления доступом к чатам.

ссылка на оригинал статьи https://habr.com/ru/articles/1064130/