Предисловие
Всем привет! Меня зовут Семенчук Александр, я ведущий разработчик Федеральной девелоперской компании «РАЗУМ». Последние 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: dict class 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;
нормализованный телефон;
ссылку на корпоративный профиль;
источник и время последнего подтверждения связи.
Основной ключ сопоставления — телефон. При синхронизации мы:
берём профили сотрудников, переданные корпоративной системой в контур синхронизации;
приводим телефоны к единому международному формату;
импортируем их в contacts служебной Telegram-учётной записи;
получаем актуальные Telegram contacts;
связываем запись только при единственном однозначном совпадении;
неоднозначные случаи отправляем в список конфликтов для ручной проверки.
Мы сознательно не связываем людей по похожему имени или 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. Поэтому после добавления мы:
Проверяем
missing_invitees;Повторно запрашиваем фактическое членство;
Только после этого отмечаем 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:00 Persistent=true RandomizedDelaySec=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-систему. Он закрывает конкретный разрыв между кадровым событием, корпоративной учётной записью и фактическим доступом к рабочим чатам.
Для администратора процесс теперь выглядит так:
выбрать сотрудника или бота;
выбрать группы;
указать действие и при необходимости роль администратора;
получить операцию с результатом по каждому чату.
Для HR offboarding выполняется автоматически и идемпотентно. При этом остаётся понятный журнал: где человек удалён, где уже отсутствовал, а где нужно поправить права сервисной учётной записи.
Главный эффект здесь не в экономии нескольких минут на одном сотруднике. Ценность появляется на масштабе: для чатов, включённых в обязательную политику, результат offboarding становится проверяемым, а пропущенная группа — видимой как отдельная проблема, а не скрытая ручная ошибка.
Выводы, которые могут пригодиться
Если вам предстоит решать похожую задачу, я бы выделил несколько принципов.
Не запускайте Telethon внутри web-процесса. Один долгоживущий worker значительно проще и надёжнее.
Не полагайтесь на numeric ID без access hash. Локальный entity cache легко создаёт ложное ощущение работоспособности.
Различайте Chat, megagroup и channel. Для них нужны разные MTProto requests.
Проверяйте постусловие. Успешный RPC не всегда означает, что пользователь действительно добавлен.
Храните результат по каждой группе. Частичный успех — нормальная ситуация, а не исключение из архитектуры.
Делайте offboarding идемпотентным. «Уже отсутствует» — это достигнутое состояние.
Учитывайте миграцию групп. Старый ID обычной группы нельзя бесконечно использовать после превращения в супергруппу.
Не подменяйте реальные права флагами в БД. Возможность приглашать и исключать участников должна приходить из актуальной Telegram entity.
Шифруйте сессию и разделяйте окружения. Telegram-сессия фактически является ключом доступа.
Связывайте только корпоративных сотрудников. У такой автоматизации должны быть понятные границы, основание и владелец процесса, чтобы не схватить заморозку аккаунта.
Telegram не предоставляет готовую корпоративную IAM-модель для десятков внутренних групп. Но если отделить web от MTProto runtime, хранить desired state и аккуратно работать с entity resolution, поверх Telegram можно построить вполне предсказуемый и контролируемый контур управления доступом к чатам.
hssergey
Бедные работники, которые вынуждены сидеть в 30 и больше рабочих чатах...