Знаете, что происходит, когда вы ставите Telega, «альтернативный клиент Telegram от ВКонтакте»? Ваш Telegram ID тихо уезжает в инфраструктуру OK/VK Calls. Без уведомлений. Без галочки «я согласен». Просто раз, и вы в индексе. Навсегда.
Я решил проверить, сколько людей в моих чатах уже засветились. Руками долго. Через плагин exteraGram можно по одному профилю за раз. Хотелось масштаба. Так появился antitelega: Go userbot, который сканирует целый чат и выдаёт список «засвеченных» прямо в Saved Messages.
А потом мой аккаунт заморозили. Но обо всём по порядку 🙂
Предыстория: что такое Telega и почему она опасна
Telega, если кто не в курсе, это форк Telegram, который ВКонтакте распространяет как «свой мессенджер». При установке клиент делает анонимный логин на бэкенд VK Calls (calls.okcdn.ru), и Telegram-аккаунт пользователя попадает во внутреннюю базу OK. Создаётся mapping: Telegram ID → OK ID.
Зачем ВК это делает, отдельная тема. Факт в том, что mapping существует, он публично доступен через анонимный API, и его можно запросить для любого Telegram ID. Не нужна ни авторизация в ВК, ни доступ к аккаунту жертвы. Достаточно знать числовой ID.
Ребята из exteraGram первыми нашли этот endpoint и сделали плагин, который проверяет по одному профилю за раз. Полезно, но когда у тебя чат на 700 человек, это не вариант.
Ядро детекта: три HTTP-запроса и один захардкоженный ключ
Весь механизм обнаружения укладывается в три шага. Никакой магии, никаких уязвимостей. Просто публичный API, который OK сам отдаёт кому угодно.
Шаг первый: анонимный логин
POST https://calls.okcdn.ru/api/auth/anonymLoginapplication_key=CHKIPMKGDIHBABABAsession_data={"device_id":"antitelega","version":2,...}
CHKIPMKGDIHBABABA это application_key приложения VK Calls, вшитый прямо в APK Telega. Внутренний appId: 512001570726. Как его достали? Скорее всего, jadx и пять минут времени. Ключ публичный, используется для анонимной аутентификации, никакого взлома тут нет.
OK отвечает session_key, который живёт в памяти процесса. Один логин, и дальше можно дёргать lookup-endpoint сколько угодно раз.
Шаг второй: запрос на каждый Telegram ID
POST https://calls.okcdn.ru/api/vchat/getOkIdByExternalIdapplication_key=CHKIPMKGDIHBABABAsession_key=<из шага 1>externalId=123456789
Просто число строкой. Никакого JSON-обёртывания.
Забавная деталь: до апреля 2026 endpoint назывался getOkIdsByExternalIds (во множественном числе) и принимал массив объектов. Потом OK тихо сменил контракт на единственное число. Мы узнали об этом, когда всё разом перестало работать. Починили за час, но осадочек остался. OK может в любой момент поменять что угодно, и ты узнаешь об этом по 500-кам в логах.
Шаг третий: парсинг ответа
Два возможных исхода.
Засвечен:
{"ok_id": 1125899910737868}
ok_id здесь внутренний идентификатор в инфраструктуре OK. Его наличие означает одно: клиент Telega когда-то подключался с этого Telegram-аккаунта и зарегистрировал его в VK Calls.
Чист:
{"error_code": 300, "error_msg": "NOT_FOUND : No ok user found for external id: 123456789; appId: 512001570726"}
Обратите внимание: OK любезно включает appId прямо в сообщение об ошибке. Удобно для дебага. Спасибо, OK.
Есть ещё вариант с протухшей сессией (коды 102/103), тогда клиент автоматически перелогинивается через singleflight, чтобы шесть параллельных воркеров не устроили шесть одновременных логинов.
Архитектура: от /scan до отчёта в Saved Messages

Пользователь пишет /scan @chatname в Saved Messages (или в любой чат). Дальше pipeline:
Enumeration. Бот вытаскивает участников через Telegram MTProto API (ChannelsGetParticipants), страницами, с паузой и jitter между запросами. Боты, удалённые аккаунты и ты сам отфильтровываются автоматически.
Cache lookup. Все собранные ID прогоняются через SQLite-кэш. Если юзер уже проверялся в пределах TTL (по умолчанию 6 часов), повторный запрос к OK не уходит.
OK batch. Некэшированные ID распределяются по worker pool с rate.Limiter. Каждый воркер вызывает CheckOne, и между вызовами добавляется случайная задержка (jitter), чтобы паттерн не выглядел машинным.
Persist + report. Результаты пишутся в кэш, отчёт форматируется и улетает в Saved Messages. Если текст длиннее 3800 символов, он разбивается на чанки.
Весь проект занимает около 1200 строк Go. Статически слинкованный бинарь на 26 мегабайт, CGO отключен, чистый Go SQLite через modernc.org/sqlite. Работает на любом Linux, включая ARM.
Грабли, на которые я наступил (и вы наступите тоже)
Грабля №1: «GoTGProto, Helsinki, Finland»
Самая обидная история за всю разработку. Я запустил бота на VPS, сделал один (один!) скан чата на 700 человек. Через пять минут на другом устройстве прилетело уведомление:
Обнаружен вход в Ваш аккаунт с GoTGProto, Helsinki, Finland. Это были Вы?

А ещё через три минуты аккаунт ушёл в read-only. Заморозка. «Нарушение Пользовательского соглашения». Оспаривай через @SpamBot до такого-то числа, иначе удалим.
Что произошло? Библиотека gotgproto, на которой построен бот, при подключении к Telegram отправляет device info. Если ты не передаёшь свой DeviceConfig, она шлёт захардкоженный дефолт: DeviceModel: "GoTGProto". Буквально вот эту строку.
А в моём коде конфиг-поля telegram.device_model, telegram.app_version, telegram.lang_code парсились из YAML, хранились в структуре, но никуда не передавались. Мёртвый код. Три поля, которые никогда не доходили до gotgproto. Telegram видел клиента как «GoTGProto на Linux». Это приговор.
Фикс занял 10 строк: добавить Device: &telegram.DeviceConfig{...} в ClientOpts. Теперь по дефолту бот представляется как Telegram Desktop на Windows 10. Но аккаунт-то уже заморожен.
Мораль: всегда проверяйте, что ваши конфиг-поля реально доходят до потребителя. Особенно если потребитель библиотека, которая молча подставляет свои дефолты.
Грабля №2: идеально ровные интервалы
Первая версия делала запросы к ChannelsGetParticipants с фиксированной паузой 350 мс. Ровно 350. Каждый раз. Для антиспам-системы Telegram это метроном. Живой человек не скроллит список участников с точностью до миллисекунды.
Решение: jitter. Случайный разброс ±40% от базовой паузы. Вместо 350-350-350 получаем 210-490-380-520. Плюс аналогичный jitter на запросы к OK API. Паттерн становится нерегулярным, ближе к тому, как реальный человек тыкает в интерфейс.
Реализация тривиальная:
func jitteredDuration(d time.Duration, jitterPct float64) time.Duration { factor := 1.0 + jitterPct*(2*rand.Float64()-1) return time.Duration(float64(d) * factor)}
Пять строк, которые, возможно, спасут ваш аккаунт. А возможно и нет. Антиспам Telegram учитывает много факторов, и jitter закрывает только один из них.
Грабля №3: OK может всё поменять в любой момент
Я уже упоминал, что endpoint сменил имя с множественного на единственное. Это произошло без предупреждения, без документации (её никогда и не было), без deprecation-периода. Просто в один день все запросы начали возвращать err=4 "common.finder".
Ключ CHKIPMKGDIHBABABA тоже не вечный. OK может его отозвать, может начать проверять подпись запросов, может вообще закрыть анонимный доступ. В конфиге есть поле ok.application_key именно на этот случай: чтобы подсунуть новый ключ без пересборки бинаря.
Где взять новый ключ, если старый протух? Честный ответ: нигде официально. Декомпиляция свежего APK Telega через jadx, MITM-сниффинг трафика через mitmproxy, или мониторинг канала авторов плагина exteraGram. Не самый надёжный supply chain, согласен.
risk_level: одна ручка вместо пяти
После истории с заморозкой стало ясно, что дефолтные настройки (6 воркеров, 8 req/s, страницы по 200, пауза 350 мс) это режим самоубийства. Но объяснять каждому юзеру, что такое participants_sleep_ms и зачем ему participants_jitter_pct, тоже не вариант.
Поэтому появился risk_level. Один параметр, который выставляет всё остальное:

paranoid: 1 воркер, 1 req/s, страницы по 30, jitter ±50%. Чат на 700 человек сканируется 16 минут. Зато выглядит как человек, который очень медленно скроллит список.
careful (дефолт): 2 воркера, 3 req/s, страницы по 80, jitter ±40%. Пять минут на 700 человек. Баланс между скоростью и скрытностью.
normal: 4 воркера, 6 req/s, 150 за страницу, jitter ±30%. Две с половиной минуты. Для одноразовых аккаунтов, которые не жалко.
aggressive: старые дефолты. Полторы минуты, никакого jitter, почти гарантированный бан.
Технически пресет это просто структура, которая переопределяет ScannerConfig в момент загрузки конфига. Если юзер выставит risk_level: custom, все ручки становятся доступны по отдельности.
Что это доказывает (и чего не доказывает)
Положительный ответ от OK API означает ровно одно: mapping Telegram ID → OK ID существует в базе VK Calls. Эта запись создаётся, когда Telega делает свой anonymLogin при первом запуске.
Это не доказывает, что человек сейчас пользуется Telega. Он мог поставить, потыкать и удалить. Запись осталась. Для атрибуции «активный пользователь прямо сейчас» метод не годится. Только «когда-либо засветился».
И ещё момент: мы спрашиваем тот же бэкенд, тем же ключом, через тот же anonymous endpoint, что и сама Telega. OK технически не может отличить запрос от «настоящего клиента» и от нашего бота. Протокол идентичен. Это не эксплуатация уязвимости, это использование публичного API ровно так, как он задуман. Просто не для того, для чего предполагалось.
Стоит ли запускать

Тут надо быть честным. Userbot, это ваш реальный Telegram-аккаунт. Telegram может его заморозить. Я проверил это на себе, хватило одного скана.
Если вам действительно нужно просканировать чат, используйте одноразовый номер. Не держите на аккаунте ничего ценного. Ставьте risk_level: paranoid или хотя бы careful. И будьте готовы к тому, что аккаунт после скана может стать read-only.
Готовые бинари лежат в релизах на GitHub. Go устанавливать не нужно, скачал и запустил. Исходники открыты, MIT-лицензия. Если OK поменяет контракт, issues открыты.
antitelega на GitHub. MIT, Go 1.23+, zero CGO, ~2045 строк.
ссылка на оригинал статьи https://habr.com/ru/articles/1022518/