Бот для модерации чата в Telegram
Чат на пару тысяч участников без модерации живёт максимум до первой волны спам-ботов: за ночь реклама казино и крипто-каналов затирает обсуждение, а разгребать вручную — работа на сутки. Автоматическая модерация закрывает три источника мусора разом: спам по ключевым словам, флуд и вступление ботов пачками. Ниже — рабочая связка на 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), а в лог-канале сразу видно причину и можно сузить регулярку.