Anti-ban улучшения для Telegram-армии: как мы защищаем аккаунты

Если ты когда-либо управлял пулом Telegram-аккаунтов для автоматизированных задач, ты знаешь это чувство: заходишь утром, а половина аккаунтов уже залочена. Telegram становится умнее с каждым месяцем, и то, что работало год назад, сегодня убивает аккаунты за считанные часы. Именно с этой проблемой мы столкнулись в проекте tg-army — и именно её мы решали в рамках плана 2026-03-13-anti-ban-improvements.
В этой статье расскажу подробно: что именно мы реализовали, почему именно так, какие проблемы пришлось решать по ходу и что получилось в итоге.
Контекст: что такое tg-army и почему банят аккаунты
tg-army — это оркестратор для управления пулом Telegram-аккаунтов. Аккаунты закупаются на маркетплейсе, проходят автоматический setup (установка аватара, bio, 2FA), прогрев и затем используются для отправки сообщений в каналы и группы.
Telegram банит аккаунты не случайно — он смотрит на паттерны поведения. Несколько триггеров, которые нас убивали:
- Несоответствие гео: аккаунт зарегистрирован через российский номер, а работает через американский прокси. Антифрод Telegram это видит.
- Слишком быстрый старт: аккаунт только что «приехал» на новый IP и сразу начинает активность. Реальный пользователь так не делает.
- Роботоподобная отправка: сообщения улетают мгновенно, без паузы на «набор текста». Мгновенная отправка — явный признак бота.
- Перегрузка каналами: один аккаунт состоит в 50+ каналах и активно постит во все. Это нереалистично для живого человека.
Мы знали об этих проблемах давно, но решали их по одной, реактивно. Пришло время системного подхода.
Что было запланировано и как мы это реализовали
План состоял из четырёх независимых задач + интеграционная проверка. Все задачи разрабатывались по TDD: сначала тест (красный), потом реализация (зелёный).
Задача 1: Geo-Match Validation
Проблема: мы закупали аккаунты с российскими номерами (+7), но назначали им прокси без учёта географии. Аккаунт с номером +7 916 XXX-XX-XX выходил через немецкий datacenter IP — и Telegram видел несоответствие.
Решение — функция validate_geo_match() в account_pool.py, которая сопоставляет телефонный код аккаунта с геолокацией прокси:
def validate_geo_match(phone: str, proxy_country: str) -> tuple[bool, str]:
"""Проверяет соответствие страны номера и прокси."""
phone_country = get_country_by_phone(phone)
if phone_country is None:
return True, "unknown phone country, skipping check"
if phone_country == proxy_country:
return True, "geo match ok"
# Разрешённые комбинации (например, RU номер + BY прокси)
ALLOWED_MISMATCHES = {
("RU", "BY"), ("RU", "KZ"), ("RU", "UA"),
}
if (phone_country, proxy_country) in ALLOWED_MISMATCHES:
return True, f"allowed mismatch: {phone_country} -> {proxy_country}"
return False, f"geo mismatch: phone={phone_country}, proxy={proxy_country}"Эта проверка интегрирована в account_setup.py в метод check_all(): если гео не совпадает — аккаунт пропускается с предупреждением, а в lolz_market.py добавлено предупреждение при закупке аккаунтов с несовместимыми прокси.
Задача 2: IP Settle Delay
Даже если гео совпадает — нельзя начинать активность сразу. Реальный пользователь, который переехал на новый IP (например, сменил провайдера или уехал в другой город), какое-то время просто существует — открывает чаты, читает, не делает ничего подозрительного.
Мы ввели задержку 12 часов с момента первого появления аккаунта на IP до начала активного setup:
def should_start_setup(account: Account, now: datetime = None) -> tuple[bool, str]:
"""Проверяет, прошло ли достаточно времени после смены IP."""
if now is None:
now = datetime.utcnow()
if account.ip_assigned_at is None:
return False, "ip_assigned_at not set"
settle_hours = settings.get("IP_SETTLE_HOURS", 12)
elapsed = now - account.ip_assigned_at
if elapsed.total_seconds() < settle_hours * 3600:
remaining = timedelta(hours=settle_hours) - elapsed
return False, f"IP not settled yet, wait {remaining}"
return True, "IP settled, ok to proceed"Проверка should_start_setup встраивается в check_all() сразу после geo-match: если гео ок, но IP не «отлежался» — ждём. Значение 12 часов вынесено в настройки БД и легко меняется без деплоя.
Задача 3: Typing Simulation
Это одно из самых эффективных улучшений с точки зрения «человекоподобности». Telegram предоставляет метод sendChatAction для отправки статуса «печатает...». Мы начали его использовать перед каждой отправкой сообщения.
Но просто вызвать typing и сразу отправить — слишком предсказуемо. Поэтому добавили реалистичный расчёт задержки:
def calc_typing_delay(text: str, wpm: int = None) -> float:
"""
Рассчитывает реалистичное время набора текста.
Средний человек печатает 40-60 WPM.
"""
if wpm is None:
wpm = random.randint(35, 65)
words = len(text.split())
base_delay = (words / wpm) * 60 # секунды
# Добавляем случайный джиттер ±20%
jitter = base_delay * random.uniform(-0.2, 0.2)
delay = base_delay + jitter
# Ограничиваем: минимум 1с, максимум 15с
return max(1.0, min(15.0, delay))
async def _simulate_typing(self, chat_id: int, text: str) -> None:
"""Имитирует набор текста перед отправкой."""
delay = calc_typing_delay(text)
await self.client.send_chat_action(chat_id, "typing")
await asyncio.sleep(delay)Эта логика добавлена в sender.py и вызывается перед send_text, send_photo, send_voice и отправкой альбома. Для медиа используем upload_photo/record_video action вместо typing.
Задача 4: Channel-Per-Account Limit
Последняя задача — ограничение количества каналов на один аккаунт. По нашим наблюдениям и данным из блога GramGPT, аккаунт, состоящий в 50+ каналах и активно постящий в них, — прямой кандидат на бан.
Мы добавили функцию count_account_channels() в pg_database.py и проверку лимита в _select_account():
async def count_account_channels(account_id: int) -> int:
"""Считает количество каналов, в которых активен аккаунт."""
result = await db.fetchval(
"SELECT COUNT(*) FROM account_channels "
"WHERE account_id = $1 AND is_active = true",
account_id
)
return result or 0
async def _select_account(step: SendStep) -> Account | None:
"""Выбирает подходящий аккаунт для отправки."""
candidates = await get_available_accounts(step.channel_id)
channel_limit = settings.get("MAX_CHANNELS_PER_ACCOUNT", 20)
for account in candidates:
# Geo-match
geo_ok, geo_msg = validate_geo_match(account.phone, account.proxy_country)
if not geo_ok:
logger.warning(f"Skipping {account.id}: {geo_msg}")
continue
# Channel limit
channel_count = await count_account_channels(account.id)
if channel_count >= channel_limit:
logger.info(f"Skipping {account.id}: channel limit {channel_count}/{channel_limit}")
continue
return account
return NoneЛимит по умолчанию — 20 каналов на аккаунт. Тоже вынесен в настройки БД.
Параллельный фронт: исправление импорта аккаунтов
Пока основная работа шла по плану anti-ban, всплыла практическая проблема: при попытке импортировать архив с аккаунтами с lolz.team получали ошибку:
'utf-8' codec can't decode byte 0xc8 in position 99: invalid continuation byte
Байт 0xc8 в cp1251 — это кириллическая буква «И». Архивы с lolz.team часто содержат JSON-файлы в кодировке Windows-1251, а Python 3 на Linux по умолчанию открывает файлы в UTF-8.
Фикс простой — детекция кодировки с fallback:
# До (сломанная версия)
with open(json_file) as f:
meta = json.load(f)
# После (с поддержкой cp1251)
raw = json_file.read_bytes()
try:
content = raw.decode("utf-8")
except UnicodeDecodeError:
content = raw.decode("cp1251")
logger.info(f"File {json_file.name} decoded as cp1251")
meta = json.loads(content)Параллельно с фиксом кодировки объединили UI: вкладка «ZIP Импорт» была дублем основной страницы импорта, просто с другим форматом загрузки. Перенесли ZIP-загрузку как карточку на основную страницу /import и убрали лишний роут.
Результаты и метрики
- 12 новых тестов — все зелёные. Каждая из четырёх задач покрыта отдельным тест-файлом (
test_geo_match.py,test_ip_settle.py,test_typing.py,test_channel_limit.py). - Сервис
tg-army-orchestratorзапущен без ошибок импорта после деплоя. - Настройки вынесены в БД:
IP_SETTLE_HOURS,MAX_CHANNELS_PER_ACCOUNT— можно менять без деплоя. - Импорт аккаунтов работает с архивами как в UTF-8, так и в cp1251.
Что касается банрейта — результаты увидим через 2-3 недели работы с новыми аккаунтами. По историческим данным, основные баны происходят в первые 48-72 часа после покупки, именно в этот период geo-match и IP settle delay работают наиболее критично.
Что ещё нашли в процессе исследования
Пока копались в теме anti-ban, наткнулись на блог GramGPT с интересным наблюдением про порядок действий при setup аккаунта.
Наш текущий pipeline: verify → profile/avatar/bio → 2FA → username → warmup → activate.
А рекомендованный порядок: отлежка 24-48ч → warmup 2-4 дня → ПОТОМ profile/avatar/bio.
Оказывается, мы делали профиль сразу после покупки — это один из главных триггеров заморозки. IP settle delay в 12 часов частично решает проблему, но не полностью: профиль всё равно правится до полноценного прогрева. Это следующий кандидат на рефакторинг pipeline.
Выводы и уроки
TDD окупается даже для «быстрых» задач. Казалось бы, добавить проверку гео — 20 строк кода, зачем тесты? Но именно тест на validate_geo_match поймал баг с разрешёнными комбинациями (RU→BY, RU→KZ) ещё до интеграции. Без теста мы бы заблокировали «разрешённые» аккаунты и даже не поняли бы почему.
Конфигурация в БД вместо хардкода — это не overengineering. Значения IP_SETTLE_HOURS=12 и MAX_CHANNELS_PER_ACCOUNT=20 — это гипотезы, не истины. Через две недели, посмотрев на данные, мы можем захотеть поставить 8 часов или 25 каналов. Если это в коде — нужен деплой. Если в БД — одна строчка SQL и перезапуск.
Маленькие UX-улучшения (убрать дублирующую вкладку) — это тоже технический долг. Дублирующийся роут /import_bulk создавал путаницу: «А где загружать ZIP? Там или тут?». Объединение в одну страницу — не большая задача, но это именно то, что накапливается и потом мешает новым разработчикам разобраться в системе.
Encoding-проблемы с внешними источниками данных — это классика. lolz.team — не первый и не последний сервис, который отдаёт файлы в Windows-1251. Паттерн «попробуй UTF-8, при ошибке попробуй cp1251» стоит иметь как утилиту в любом проекте, который работает с файлами от сторонних источников. То же самое касается csv-экспортов из 1С, архивов от российских госсервисов и ещё десятка источников, где cp1251 — норма жизни.
Изучение блогов конкурентов и смежных сервисов — полезная практика. Из анализа блога GramGPT мы получили конкретный инсайт про порядок setup, который у нас неправильный. Это не была задача из плана — это стало следующей задачей в бэклоге. Иногда лучшие идеи по улучшению продукта приходят не изнутри, а из внешнего анализа.
Технические ссылки

AI-инженер, предприниматель, маркетолог. Основатель feberra.com и x10seo.ru. 13 лет в перфоманс-маркетинге, 3 года в системной интеграции AI в бизнес.