Telegram-бот для модерации: grace window, чтобы не банить честных продавцов
Вчера вечером мне написал модератор одной из Telegram-групп, с которой я работаю. Продавец виллы в Чангу опубликовал нормальный листинг: фото, описание, цена, контакт. Всё по правилам. Через 20 минут дослал ещё две фотки с другого ракурса — без подписи, просто дополнение к посту. Мой бот-модератор увидел: один пользователь, три сообщения без текста за 25 минут, классический паттерн агрессивного постинга. Мгновенный бан. Во всех пяти группах сразу, потому что у меня единый блеклист.
Дальше предсказуемо: продавец написал в личку модератору, что его забанили без причины за нормальную публикацию. Модератор написал мне. Я полчаса разбирался, нашёл пользователя в блеклисте, разбанил вручную, добавил в исключения. Потерянное время у троих. Виновата логика, которую я сам написал.
Это не редкий случай. В любой профессиональной Telegram-группе с объявлениями — недвижимость, авто, услуги, оборудование — продавцы публикуют контент сериями. Сначала основное объявление, потом несколько дополнительных фото, потом ответы на вопросы. Классический антиспам-алгоритм не различает этот паттерн от настоящего спама. Когда у вас 500 участников и 20–30 новых объявлений в день, таких ложных срабатываний набирается 3–5 в неделю. Кажется немного — до тех пор, пока модераторы не устают разгребать.
За вечер переписал три механизма бота. В этой статье покажу что именно изменилось, с кодом и объяснением логики — потому что это работающая система, а не теория.
Почему стандартный антиспам ломается в профессиональных сообществах
Большинство готовых Telegram-ботов-модераторов заточены под одну задачу: остановить очевидный спам. Рекламные ссылки, казино, инвайты в другие группы, каналы с криптой. С этим они справляются неплохо. Проблема начинается, когда аудитория группы — не случайные прохожие, а профессиональные участники с легитимными объявлениями.
В группах по аренде недвижимости, авто, услугам паттерн продавца и паттерн спамера внешне неотличимы для простого алгоритма:
- Спамер публикует объявление без текста — несколько картинок, телефон в картинке.
- Продавец публикует объявление, потом дополнительные фото через 5–20 минут.
- Спамер долбит несколько групп одновременно с одного аккаунта.
- Продавец тоже может дублировать объявление в 3–4 тематических группах.
Если бот видит «несколько сообщений без текста за короткое время» как сигнал спама — он будет банить обоих. Первый год это кажется нормой: «ну есть ложные срабатывания, модераторы разберутся». На пятом ложном срабатывании за неделю модераторы начинают отключать бота вовсе. Что, конечно, ещё хуже.
Откуда берётся ложное срабатывание
Классический алгоритм антиспама смотрит на абстрактные признаки: частота постинга, наличие ссылок, совпадение с блеклист-словами, количество медиа без текста. Каждый из них по отдельности — слабый сигнал. Вместе — всё равно слабый, просто с большим весом.
Продавец виллы дослал фотки к своему посту. С точки зрения бота: 2 новых сообщения, нет текста, быстро. Всё. Сигнал «подозрительно». Если порог срабатывания низкий — бан. Если высокий — реальный спам проскакивает.
Решение не в том, чтобы поднять порог. Решение — добавить контекст: что делал этот пользователь прямо перед этим. Именно это делает grace window.
Что такое grace window и как он работает
Grace window — это временной промежуток после того, как пользователь опубликовал «хорошее» сообщение (с текстом, одобренное ботом), в течение которого его следующие публикации без текста не вызывают срабатывания антиспама.
Логика такая: если человек только что написал нормальный пост — скорее всего, следующие несколько минут это продолжение того же контента, а не атака спамера. Спамер не делает качественный первый пост — он сразу грузит пачку рекламы без вводного текста.
В таблице базы данных хранится поле last_approved_post_ts для каждой пары (user_id, chat_id). Каждый раз, когда бот видит текстовое сообщение длиннее 50 символов без спам-паттернов — обновляет это поле текущим временем. Когда приходит следующее сообщение без текста — проверяет разницу:
- Если прошло меньше 5 минут после одобренного поста — пропускает без антиспам-проверки.
- Если прошло больше — анализирует как обычное.
5 минут — эмпирически подобранный порог для групп по недвижимости. Продавец обычно дошлёт допфото в течение 1–3 минут после основного поста. Реальный спамер между «хорошим» постом и спам-атакой ждёт дольше. Для других ниш — например, криптовалютные чаты — таймер можно уменьшить до 2 минут.
Как определить, что пост «одобрен»
Не каждый текстовый пост должен триггерить grace window. Иначе спамер напишет одну строку текста и получит 5 минут на отправку рекламы без ограничений.
Мои правила для «одобренного поста», который обновляет таймер:
- Текст длиннее 50 символов (коротких «👍» или «ок» не считаем).
- Нет спам-паттернов в тексте (блеклист слов, подозрительные ссылки).
- Нет совпадения с блеклистом ключевых слов (казино, крипта, заработок, инвайт в канал).
Дополнительно: если пользователь публикует новый полноценный пост — таймер сбрасывается. То есть через 5 минут после первого поста grace window закрыт, но если человек написал новый листинг — он получает ещё 5 минут для дополнений к нему. Это позволяет профессиональным продавцам, которые публикуют 3–4 объявления в день, нормально работать.
Ограничения grace window
Этот механизм не спасает от всего. Если спамер научится имитировать «хороший» первый пост — он обойдёт grace window. На практике это редкость: автоматические спам-боты не заморачиваются написанием качественного текста. Живые спамеры — иногда да, но их значительно меньше.
Для высококонкурентных групп с 10 000+ участниками, где профессиональный спам — это чей-то бизнес, grace window нужно комбинировать с другими методами: верификация нового участника, рейтинг доверия, ручное одобрение первого поста. В группах с 200–1000 участниками grace window + блеклист закрывают около 90% случаев без дополнительных механизмов.
Публичная причина бана: не «спам», а конкретный паттерн
Когда бот банит пользователя, он обычно пишет что-то вроде «Вы забанены за нарушение правил группы». Или вообще молча банит. Оба варианта плохие.
Молчаливый бан: модератор получает жалобу от пользователя, который не понимает, за что его забанили. Модератор не знает причину. Разбираться нужно через логи бота — а это время, которого у модераторов-волонтёров нет.
Абстрактная причина: пользователь всё равно не понимает, что именно сработало, и может снова нарушить то же правило с другого аккаунта. Особенно болезненно это в профессиональных группах: человек тратил время на составление объявления, а бот молча его выкинул.
После редизайна бот пишет в чат публичное сообщение при бане с конкретным паттерном:
🚫 @username забанен. Причина: паттерн «Casino» — совпадение с ключевыми словами в профиле (casino, казино), мгновенная публикация 3 сообщений без текста в 5 чатах одновременно.
Или в случае с медиафлудом:
🚫 @username забанен. Причина: паттерн «MediaFlood» — 3 сообщения без текста в течение 25 минут, нет истории одобренных постов в этой группе.
Это публично видят все участники. Модератор сразу видит причину без залезания в логи. Пользователь понимает, что именно сработало. При спорных случаях сразу понятно, ложное это срабатывание или обоснованный бан.
Как формируются паттерны
В конфиге бота список именованных паттернов. Каждый паттерн — это набор условий и человекочитаемое имя:
- Casino: слова casino/казино/slots в сообщении или в bio, ссылки на известные домены казино.
- Crypto: упоминания конкретных монет, «пассивный доход», «гарантированная прибыль», ссылки на биржи.
- InviteFlood: ссылки-инвайты в другие группы/каналы, более 2 сообщений за час.
- MediaFlood: более 3 сообщений без текста за 30 минут, нет одобренных постов в истории.
- NewAccountSpam: аккаунт создан менее 30 дней назад плюс более 2 ссылок в первом сообщении.
Когда бот банит — он пишет имя паттерна и основное условие, которое сработало. Не просто «спам».
Soft-unban: кнопка для модератора без постоянного whitelist
После каждого публичного сообщения о бане бот добавляет инлайн-кнопки, видимые только администраторам группы:
- ✅ Разбанить — восстанавливает участника, убирает из блеклиста этого чата, пишет в чат «Бан отменён модератором».
- 📝 Разбанить + Предупреждение — восстанавливает, но добавляет предупреждение в историю пользователя. Следующий бан будет неотменяемым.
- 🔒 Подтвердить бан — фиксирует бан как обоснованный, блокирует кнопки разбана на 30 дней.
Ключевое: нажатие «Разбанить» не добавляет пользователя в постоянный whitelist. Это мягкая отмена конкретного инцидента. Пользователь возвращается в группу, но будущие его сообщения по-прежнему проходят через модерацию.
Почему не whitelist
Whitelist — это долгосрочное доверие. Пользователь в whitelist не проверяется никогда. Это проблема по двум причинам.
Первая: whitelist накапливается. Через год у вас 200 аккаунтов в whitelist, часть из которых уже взломана или продана. Вы фактически открыли постоянный коридор для спама через скомпрометированные аккаунты.
Вторая: ложное срабатывание — это не повод доверять навсегда. Модератор хочет разобрать один конкретный случай, а не выдать пожизненный пропуск. Soft-unban решает именно этот конкретный инцидент.
В audit-логе каждое действие фиксируется: кто разбанил, в какое время, какая кнопка нажата, с каким паттерном был связан бан. Это важно при спорах с пользователями, при анализе качества модерации за месяц, и при принятии решения, нужно ли обновить список паттернов.
Архитектура: как три механизма работают в цепочке
Когда приходит сообщение от пользователя, бот проходит цепочку проверок:
Шаг 1: Блеклист пользователя. Если пользователь уже в постоянном блеклисте (несколько подтверждённых банов) — мгновенный бан без дальнейших проверок.
Шаг 2: Grace window. Проверяем last_approved_post_ts. Если прошло меньше 5 минут после одобренного поста — пропускаем сообщение без дальнейших проверок.
Шаг 3: Паттерн-матчинг. Проверяем сообщение по списку именованных паттернов. Если совпадение — определяем имя паттерна и основной триггер.
Шаг 4: Бан. Применяем бан во всех связанных чатах, публикуем сообщение с именем паттерна и причиной, добавляем инлайн-кнопки для модератора.
Шаг 5: Обновление grace window. Если сообщение прошло проверку и это полноценный текстовый пост — обновляем last_approved_post_ts.
Вся цепочка — около 150 строк Python с aiogram 3. БД — PostgreSQL, таблица на несколько тысяч записей работает без задержек. При больших группах имеет смысл добавить индекс по (user_id, chat_id) и кэшировать last_approved_post_ts в Redis с TTL равным grace window — тогда при попадании в кэш SELECT в БД не нужен.
Синхронизация между несколькими чатами
5 групп связаны в один блеклист: если пользователь забанен в одной — автоматически банится во всех. Бот является администратором во всех пяти группах, и при бане в чате A сразу вызывает ban_chat_member для остальных.
Soft-unban работает на уровне отдельного чата: разбан в группе A не разбанивает в B–E. Если инцидент был в одной группе и это очевидно ложное срабатывание — модератор этой группы разбанивает у себя. Если участник нужен во всех — каждый модератор подтверждает по отдельности. Это занимает 10–15 секунд, но защищает от «цепочки помилований» через одного дружественного модератора.
Реализация: схема БД и ключевые функции
Для тех, кто хочет повторить это у себя — минимальная схема и логика, которую я использую.
Таблица контекста пользователей в PostgreSQL:
user_id BIGINT— Telegram user IDchat_id BIGINT— Telegram chat IDlast_approved_post_ts TIMESTAMPTZ— время последнего одобренного постаwarning_count INTEGER DEFAULT 0— счётчик предупрежденийbanned_until TIMESTAMPTZ— если пользователь в мягком бане- PRIMARY KEY
(user_id, chat_id)
При проверке нового сообщения функция is_in_grace_window делает один SELECT по (user_id, chat_id) и сравнивает last_approved_post_ts с now() - interval 5 minutes. Если запись отсутствует — grace window нет, пользователь новый.
При публикации одобренного поста функция update_grace_window делает INSERT ... ON CONFLICT DO UPDATE — обновляет last_approved_post_ts и сбрасывает счётчик предупреждений до нуля, если он был 1. Два предупреждения накопленных — не сбрасываются, только если модератор явно снял через кнопку «Разбанить + Предупреждение».
Весь цикл одного сообщения: 1–2 запроса к БД, время отклика менее 10 миллисекунд даже без кэша. Для групп до 2 000 участников этого вполне достаточно. При большем масштабе grace window стоит хранить в Redis: ключ grace:{user_id}:{chat_id}, значение — timestamp последнего поста, TTL — 5 минут. Тогда при попадании в кэш SQL-запросы вообще не нужны.
Логирование: каждое событие (бан, разбан, обновление grace window, подтверждение бана модератором) пишется в отдельную таблицу moderation_events с created_at, event_type, user_id, chat_id, pattern_name, moderator_id. Раз в неделю смотрю на эту таблицу: если паттерн MediaFlood встречается чаще 5 раз за неделю без последующего подтверждения реального спама — сигнал, что порог слишком низкий.
Полный стек: Python 3.11, aiogram 3.x, PostgreSQL 15, опционально Redis для кэша. Зависимости минимальны. Если у вас уже есть aiogram-бот с доступом к PostgreSQL — добавить grace window займёт меньше часа.
Кейс: пять групп по аренде на Бали
Работаю с несколькими профессиональными группами по недвижимости на Бали — чаты, где участники ищут и предлагают аренду вилл и квартир. Специфика этой аудитории: большинство продавцов не технические пользователи, они не понимают почему бот их забанил, и просто уходят из группы. Для владельца группы это потеря активных участников, которых сложно вернуть.
Группы, о которых идёт речь — чаты по недвижимости на Бали. В каждой от 200 до 800 участников: риелторы, хозяева вилл, арендаторы. Типичное содержание — объявления об аренде, поиск жилья, вопросы по договорам. Высокая доля участников, которые постят объявления сериями: сначала основное описание, потом план, потом фото с видом.
До добавления grace window: 2–3 жалобы от честных продавцов в неделю. Примерно половина проходила через ручной разбан модераторами. Вторая половина просто уходила из группы, не разобравшись — это потерянные активные участники.
После добавления grace window: за первую неделю ни одного ложного срабатывания по паттерну MediaFlood. Реальный спам по-прежнему ловится — у спамеров нет «одобренного поста» в истории, они не готовят почву перед атакой.
Нагрузка на модераторов упала заметно. До редизайна 2–3 раза в неделю кто-то из модераторов писал мне в личку. После — один раз за неделю, и тот был по другому вопросу.
Публичные причины банов с конкретными паттернами начали работать как обучение аудитории. После первой недели с публичными сообщениями несколько участников написали в чат: «понял, почему забанили — убрал ссылку на канал». Механизм работает не только как реакция на нарушение, но и как профилактика повторных.
Когда эти механизмы не помогут
Три ситуации, где описанный подход не спасёт:
Скоординированный спам с разных аккаунтов. Если спамер использует 20 свежих аккаунтов, каждый из которых пишет по одному «хорошему» посту перед спамом — grace window сработает против вас. Решение: дополнительная проверка по возрасту аккаунта. Новые аккаунты с минимальной историей не получают grace window даже после одобренного поста.
Группы с открытой ссылкой для вступления без верификации. Спамеры вступают, публикуют один нормальный пост, получают grace window, грузят рекламу. В таких группах нужна верификация нового участника: кнопка «я человек», решение капчи, или первое сообщение на модерации администратора.
Очень активные группы (5 000+ сообщений в день). PostgreSQL справится, но каждое сообщение вызывает один SELECT и иногда UPDATE — около 10 000 запросов в день. Для такого масштаба стоит кэшировать last_approved_post_ts в Redis с TTL равным grace window, тогда при попадании в кэш SELECT в БД не нужен вообще.
Итоги: три часа на то, что убирает проблему навсегда
После вечера работы у меня было 3 изменения в коде, 1 новая таблица в БД и 1 перезапуск Docker-контейнера. Ничего радикального.
Три часа на переписку логики вернули мне спокойствие нескольких модераторов и перестали выталкивать честных участников из профессионального сообщества. Это не масштабная автоматизация — просто одна хорошо написанная проверка на правильном уровне.
Самые болезненные проблемы в автоматизации — не технические. Они поведенческие. Система ведёт себя правильно по своим правилам, но неправильно в контексте реального использования. Grace window не делает антиспам умнее — он добавляет контекст о предыстории пользователя. Этого достаточно, чтобы срезать большинство ложных срабатываний в профессиональных группах.
Практические шаги, если хотите добавить это в свой бот:
- Добавьте таблицу
user_contextс полямиuser_id,chat_id,last_approved_post_ts,warning_count. - В хэндлере каждого сообщения добавьте проверку grace window в самом начале — перед паттерн-матчингом.
- При каждом подтверждённом хорошем посте обновляйте
last_approved_post_ts— этим создаёте историю доверия пользователя. - Переименуйте ваши паттерны из кода в человекочитаемые строки и добавьте их в публичное сообщение о бане.
- Добавьте инлайн-кнопки к сообщению о бане с тремя уровнями ответа: разбанить, разбанить с предупреждением, подтвердить бан.
Реализация на aiogram 3 с PostgreSQL занимает примерно 150–200 строк Python. Если у вас уже есть работающий бот-модератор — изменения минимальны: новая таблица в БД, одна функция проверки grace window, обновлённый формат сообщения о бане.
Если вы строите Telegram-автоматизацию для профессионального сообщества — в этой статье я разбирал более широкий кейс Telegram-автоматизации для малого бизнеса, там больше контекста по выбору инструментов и типичным ошибкам при запуске.