MAATRIX / Блог / Бот для модерации чата в Telegram

Бот для модерации чата в Telegram

Бот для модерации чата в Telegram

MAATRIX

Чат на пару тысяч участников без модерации живёт максимум до первой волны спам-ботов: за ночь реклама казино и крипто-каналов затирает обсуждение, а разгребать вручную — работа на сутки. Автоматическая модерация закрывает три источника мусора разом: спам по ключевым словам, флуд и вступление ботов пачками. Ниже — рабочая связка на aiogram 3: права боту, антиспам-фильтр, антифлуд, капча и лог действий в отдельный канал, с кодом, который можно брать и адаптировать.

Какие права дать боту-администратору

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

Право в TelegramЗачем модерации
Удаление сообщенийСтирать спам и флуд сразу после детекта
Блокировка пользователейАвтобан за грубые нарушения
Ограничение участниковМут при флуде и до прохождения капчи
Закрепление сообщенийОпционально, для правил чата

Права «Изменение информации о группе» и «Назначение администраторов» боту не нужны: если токен утечёт, ущерб ограничен тем, что бот и так мог делать.

Отдельная, часто упускаемая настройка — Privacy Mode у @BotFather. По умолчанию он включён, и бот видит только команды, реплаи и упоминания — обычный текст проходит мимо, ключевые слова ловить нечем. Отключается через /mybots → бот → Bot Settings → Group Privacy → Turn off. После смены режима бота нужно заново добавить в чат.

Антифлуд: лимит сообщений в единицу времени

Флуд — не всегда спам-бот: живой участник в перепалке генерирует те же 15 сообщений за 10 секунд. Лечится счётчиком по скользящему окну на пользователя, без внешней БД — deque с метками времени в памяти процесса:

import time
from collections import defaultdict, deque

FLOOD_LIMIT = 5       # сообщений
FLOOD_WINDOW = 10      # секунд
MUTE_SECONDS = 300     # мут при превышении

user_timestamps: dict[tuple[int, int], deque] = defaultdict(deque)

def is_flooding(chat_id: int, user_id: int) -> bool:
    key = (chat_id, user_id)
    now = time.monotonic()
    window = user_timestamps[key]
    window.append(now)
    while window and now - window[0] > FLOOD_WINDOW:
        window.popleft()
    return len(window) > FLOOD_LIMIT

Дальше — middleware, вызывающий проверку раньше основной логики хендлеров, чтобы мутить флудера до обработки текста:

from aiogram import BaseMiddleware
from aiogram.types import Message, ChatPermissions

class AntiFloodMiddleware(BaseMiddleware):
    async def __call__(self, handler, event: Message, data):
        if event.chat.type in ("group", "supergroup") and event.from_user:
            if is_flooding(event.chat.id, event.from_user.id):
                await event.bot.restrict_chat_member(
                    event.chat.id, event.from_user.id,
                    permissions=ChatPermissions(can_send_messages=False),
                    until_date=int(time.time()) + MUTE_SECONDS,
                )
                await event.delete()
                await log_action(event.bot, "antiflood_mute", event.from_user, event.chat, MUTE_SECONDS)
                return
        return await handler(event, data)

Порог 5/10 сек — ориентир, не измеренная величина: в тихом чате поддержки его стоит ужесточить до 3/15, в активном обсуждении — ослабить, иначе под мут попадут живые люди в разгаре спора.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать VPS

Автобан и мут по ключевым словам и спам-паттернам

Спам узнаваем по набору признаков: реф-ссылки на биржи, «заработок от 500$ в день», юникод-подмена символов в ссылках. Правила стоит разделить на два уровня — мягкие удаляют и предупреждают, жёсткие банят сразу:

import re

SOFT_PATTERNS = [
    re.compile(r"подпишись.{0,15}канал", re.IGNORECASE),
    re.compile(r"пиши в личку", re.IGNORECASE),
]

HARD_PATTERNS = [
    re.compile(r"(t\.me|telegram\.me)/joinchat", re.IGNORECASE),
    re.compile(r"заработ(ок|ай).{0,20}(день|час)", re.IGNORECASE),
    re.compile(r"(бинанс|bybit|казино|casino).{0,10}(бонус|промокод)", re.IGNORECASE),
]

def classify(text: str) -> str | None:
    if not text:
        return None
    if any(p.search(text) for p in HARD_PATTERNS):
        return "hard"
    if any(p.search(text) for p in SOFT_PATTERNS):
        return "soft"
    return None

Хендлер регистрируется в роутере модерации раньше остальных текстовых обработчиков и реагирует на оба уровня:

from aiogram import Router
from aiogram.types import Message

moderation_router = Router()

@moderation_router.message(F.text | F.caption)
async def spam_filter(message: Message):
    verdict = classify(message.text or message.caption or "")
    if verdict is None:
        return
    await message.delete()
    if verdict == "hard":
        await message.bot.ban_chat_member(message.chat.id, message.from_user.id, revoke_messages=True)
        await log_action(message.bot, "autoban_keyword", message.from_user, message.chat, reason=message.text)
    else:
        await message.chat.send_message(
            f"⚠️ {message.from_user.mention_html()}, сообщение удалено — похоже на рекламу.",
            parse_mode="HTML",
        )
        await log_action(message.bot, "delete_keyword", message.from_user, message.chat, reason=message.text)

Честная оговорка: регулярки — фильтр первого рубежа, а не защита от целеустремлённого спамера. Обход через кириллическую «т» вместо латинской регулярку не поймает — список паттернов придётся пополнять по факту новых волн спама, готового «вечного» набора не существует. Для более серьёзной защиты добавляют проверку по CAS (Combot Anti-Spam System) — публичной базе уже забаненных ID: запрос GET https://api.cas.chat/check?user_id=<id> перед первым сообщением новичка отсекает заметную часть спам-ботов ещё до того, как они написали хоть слово.

Капча для новых участников против ботов-спамеров

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

pending: dict[tuple[int, int], asyncio.Task] = {}

@moderation_router.message(F.new_chat_members)
async def on_join(message: Message):
    for user in message.new_chat_members:
        if user.is_bot:
            continue
        await message.bot.restrict_chat_member(
            message.chat.id, user.id, permissions=ChatPermissions(can_send_messages=False),
        )
        kb = InlineKeyboardMarkup(inline_keyboard=[[
            InlineKeyboardButton(text="Я не бот ✅", callback_data=f"captcha:{user.id}")
        ]])
        prompt = await message.answer(
            f"{user.mention_html()}, нажмите кнопку за 3 минуты, иначе будете удалены.",
            reply_markup=kb, parse_mode="HTML",
        )
        pending[(message.chat.id, user.id)] = asyncio.create_task(
            kick_if_not_confirmed(message.bot, message.chat.id, user.id, prompt.message_id)
        )

async def kick_if_not_confirmed(bot, chat_id, user_id, prompt_id):
    await asyncio.sleep(180)
    if (chat_id, user_id) in pending:
        await bot.ban_chat_member(chat_id, user_id)
        await bot.unban_chat_member(chat_id, user_id)  # кик без постоянного бана
        await bot.delete_message(chat_id, prompt_id)
        del pending[(chat_id, user_id)]

@moderation_router.callback_query(F.data.startswith("captcha:"))
async def confirm_captcha(callback: CallbackQuery):
    target_id = int(callback.data.split(":")[1])
    if callback.from_user.id != target_id:
        return await callback.answer("Кнопка не для вас.", show_alert=True)
    key = (callback.message.chat.id, target_id)
    if key in pending:
        pending[key].cancel()
        del pending[key]
    await callback.message.bot.restrict_chat_member(
        callback.message.chat.id, target_id,
        permissions=ChatPermissions(can_send_messages=True, can_send_other_messages=True),
    )
    await callback.message.delete()
    await callback.answer("Добро пожаловать!")

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

Логирование действий модерации в отдельный канал

Без лога модерация — чёрный ящик: администратор не видит, кого забанили и за что, пока участник не пожалуется в личку. Решение — приватный канал, куда бот пересылает каждое действие:

LOG_CHANNEL_ID = -1001234567890  # узнать через getUpdates после добавления бота в канал

async def log_action(bot, action: str, user, chat, reason=None):
    text = (
        f"🛡 <b>{action}</b>\nЧат: {chat.title} (<code>{chat.id}</code>)\n"
        f"Пользователь: {user.mention_html()} (<code>{user.id}</code>)\n"
    )
    if reason:
        text += f"Причина: <code>{reason[:200]}</code>\n"
    await bot.send_message(LOG_CHANNEL_ID, text, parse_mode="HTML")

Канал-лог держите закрытым, доступ только у модераторов. По логу удобно калибровать пороги: всплеск однотипных записей autoban_keyword за пару минут обычно означает, что правило слишком широкое и режет живых людей.

Собирается всё в точке входа тремя строками: dp.message.middleware(AntiFloodMiddleware()), dp.include_router(moderation_router) и обычный dp.start_polling(bot). Порядок в aiogram 3 важен: хендлер, зарегистрированный раньше, забирает апдейт первым — держите модерацию в отдельном роутере, подключённом до роутеров с пользовательскими командами.

Где развернуть бота модерации

Бот модерации — тот же процесс под systemd, что и обычный Telegram-бот на Python: venv, автозапуск, перезапуск при падении, только права шире. По ресурсам он легче ИИ-нагрузки — регулярки и счётчики в памяти не требуют GPU, ориентир по RAM смотрите в отдельном разборе, сколько памяти нужно Telegram-боту. Для одного-двух чатов хватает младшего тарифа: 1 vCPU, 1 ГБ RAM плюс пара гигабайт swap. Для десятка активных чатов сразу — 2 vCPU, 2–4 ГБ RAM, чтобы antiflood-счётчики не конкурировали с event loop за процессор.

Режим polling из примеров выше не требует домена и открытых портов — сравнение с webhook разобрано в статье про выбор между webhook и polling. Локация сервера почти не влияет на скорость реакции модерации. Выбирайте по тем же критериям, что и для основного бота: рядом ИИ-модерация или платёжные интеграции через внешние API — UK или US; чат российской аудитории с данными, которые должны оставаться в РФ по 152-ФЗ — RU. Оплата на все локации доступна из России — картой, по СБП, криптовалютой или токеном MAAT.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать VPS

Частые вопросы

Можно ли обойтись без прав на бан и ограничивать только удалением сообщений?

Можно, но это полумера: спам-бот за минуту напишет то же сообщение снова с нового аккаунта. Право на ограничение участников — минимум, без него антифлуд и капча не работают, Telegram API ответит Bot is not enough rights.

Почему бот не видит сообщения других участников, хотя добавлен в группу?

Почти всегда — включённый Privacy Mode в @BotFather: по умолчанию бот получает только команды и реплаи. Отключите Group Privacy и добавьте бота в чат заново — старое членство сохраняет прежний уровень видимости.

Как отличить флудера от активного участника, чтобы не мутить зря?

Поднимите порог для конкретного чата и добавьте исключение для доверенных ID перед вызовом is_flooding. Единого порога не существует — начинайте с 5 сообщений за 10 секунд и корректируйте по логу модерации.

Что делать, если бот забанил человека по ложному срабатыванию фильтра?

Разбан — bot.unban_chat_member(chat_id, user_id, only_if_banned=True), а в лог-канале сразу видно причину и можно сузить регулярку.