Капча для новых участников

Сегодня научим «Цыплёнка-помощника» встречать новичков в группе как строгий, но добрый охранник: впускать живых людей и не пускать спам-ботов.
Капча — это простая проверка «а ты вообще человек?», которую новый участник должен пройти, прежде чем сможет писать в группе.

Зачем вообще капча: история одного игрового чата

Представь: ты сделал чат своего игрового клана. Пара сотен человек, делитесь скриншотами, договариваетесь о рейдах. И вот однажды утром заходишь, а там — десяток свежих «участников» с именами вроде Anna_18_crypto, и каждый второй кидает ссылку на «бесплатные звёзды» и сайт казино. Это не люди. Это спам-боты: программы, которые автоматически вступают в открытые группы и сыплют рекламой.

Живой человек, заходя в чат, сначала осмотрится, прочитает закреп, поздоровается. Бот-спамер делает одно: вступил — и сразу написал рекламу. Вся идея капчи в том, чтобы поставить на входе крошечное препятствие, которое человеку пройти легко (нажать одну кнопку), а тупому спам-боту — нет, потому что он не умеет «думать» и нажимать конкретную кнопку по смыслу.

Само слово «капча» пришло из английского CAPTCHA — это сокращение от длинной фразы, которую можно перевести как «полностью автоматический тест, чтобы отличить компьютер от человека». Ты её сто раз видел: «выберите все картинки со светофорами», «введите буквы с искажённого фото», «поставьте галочку — я не робот». Везде смысл один и тот же: задачка, которую человек решает за секунду, а программе она даётся тяжело. В Telegram-чате нам не нужна сложная картинка с дорожными знаками — хватит и одной кнопки, потому что большинство спам-ботов вообще не умеют осмысленно нажимать на inline-кнопки внутри группы. Наша задача — не построить непробиваемую крепость, а отсечь самый массовый и тупой спам, которого как раз больше всего.

Почему это вообще твоя забота, а не Telegram? Telegram частично борется со спамом сам, но он не знает правил именно твоего чата и не может угадать, кого ты хочешь видеть. Админ группы — это ты (точнее, твой бот). Если ты ведёшь чат кружка по программированию, клана в игре или класса — именно ты отвечаешь за то, чтобы людям было комфортно. Один спамер с порнокартинкой в учебном чате — и половина участников молча выйдет. Капча — самый дешёвый способ этого избежать: она работает круглосуточно, не устаёт и не пропускает «по доброте душевной».

К концу урока «Цыплёнок-помощник» будет делать так: новый человек заходит в группу — бот сразу лишает его права писать, шлёт сообщение «Привет! Чтобы доказать, что ты не робот, нажми кнопку ниже» с inline-кнопкой, и только после нажатия возвращает право общаться. Спамер кнопку не нажмёт — и так и останется молчать.

Мы опираемся на то, что ты уже умеешь: про права бота в группе и событие входа нового участника мы говорили в уроке «Бот в группах: права и события», а про inline-кнопки и обработку нажатий — в уроке «Inline-кнопки и callback». Сегодня мы соединим эти два кусочка в одну рабочую защиту.

Как это работает: метафора с фейс-контролем

Подумай о капче как о фейс-контроле на входе в клуб. Охранник (наш бот) стоит у двери. Когда подходит новый гость, охранник не пускает его сразу в зал — он говорит: «Стой тут, пока не покажешь, что ты свой». Гость показывает (нажимает кнопку) — охранник отступает и пропускает. Если человек просто стоит и ничего не делает — он так и остаётся за бортиком.

В терминах Telegram «не пускать в зал» означает ограничить участника. У бота-администратора есть право временно запретить человеку отправлять сообщения. Делается это методом restrict_chat_member: мы передаём чат, id пользователя и набор разрешений, где всё запрещено. Человек остаётся в группе, видит переписку, но писать не может — ровно как гость за бортиком.

«Показал, что свой» — это нажатие на inline-кнопку. Помнишь: inline-кнопка живёт прямо под сообщением и при нажатии шлёт боту скрытый сигнал — callback. Бот ловит этот callback отдельным хэндлером, проверяет, что нажал именно тот, кого он ждал, и снимает ограничение — снова через restrict_chat_member, но теперь с разрешениями «можно писать».

Получается простой круг: вошёл → ограничили → показали кнопку → нажал → сняли ограничение. Всё остальное в уроке — детали этого круга.

Важно понять одну тонкость, которая часто пугает новичков: ограничение и исключение — это разные вещи. Ограничить (restrict) — значит оставить человека в группе, но временно запретить ему что-то делать: писать, слать стикеры, прикреплять фото. Он по-прежнему участник, видит чат, и ты в любой момент можешь вернуть ему права. Исключить (ban) — значит выкинуть из группы совсем. На входе нам нужно именно ограничение: мы же не знаем заранее, бот это или живой человек, который просто ещё не нажал кнопку. Сначала придерживаем, даём шанс пройти проверку, и только если он молчит слишком долго — выгоняем. Это как охранник, который сначала просит подождать, а гонит прочь только откровенных дебоширов.

И ещё про асинхронность, раз уж мы её упомянули. Слово «асинхронный» звучит страшно, но идея простая: бот не делает дела строго по очереди, замирая на каждом. Пока он ждёт ответа от Telegram (а сеть — это всегда ожидание), он успевает заняться другими участниками. Представь официанта: он не стоит над одним столиком, пока повар готовит, — он принимает заказы у соседей, разносит напитки, возвращается, когда блюдо готово. Поэтому в коде ты увидишь слова async и await — это пометки «тут можно подождать, не блокируя остальных». Тебе пока не нужно понимать их до конца: просто запомни, что почти все методы бота вызываются через await.

Пример 1. Ловим вход и сразу ограничиваем

Сначала вспомним, как поймать момент, когда в группу зашёл новый человек. В aiogram 3.x для этого есть специальное событие chat_member — но для группового приветствия проще и надёжнее ловить системное сообщение о входе через фильтр на message.new_chat_members. Telegram присылает такое сообщение, когда кто-то вступил.

Добавляем в наш bot.py хэндлер. Напомню: объект bot и диспетчер dp у нас уже созданы в начале файла, токен берётся из переменной окружения — мы это настраивали в самых первых уроках.

from aiogram import F, Router
from aiogram.types import (
    Message,
    ChatPermissions,
    InlineKeyboardMarkup,
    InlineKeyboardButton,
)

router = Router()

# Запрет писать: все разрешения False
MUTED = ChatPermissions(can_send_messages=False)


@router.message(F.new_chat_members)
async def on_new_member(message: Message):
    for user in message.new_chat_members:
        # Самого бота не трогаем
        if user.is_bot:
            continue

        # 1. Лишаем новичка права писать
        await message.bot.restrict_chat_member(
            chat_id=message.chat.id,
            user_id=user.id,
            permissions=MUTED,
        )

        # 2. Готовим кнопку проверки
        kb = InlineKeyboardMarkup(inline_keyboard=[[
            InlineKeyboardButton(
                text="Я не робот \U0001F423",
                callback_data=f"captcha:{user.id}",
            )
        ]])

        # 3. Приветствуем и просим нажать
        await message.answer(
            f"Привет, {user.full_name}! Чтобы писать в чате, "
            f"докажи, что ты человек — нажми кнопку ниже.",
            reply_markup=kb,
        )

Результат: в чате бот ответит сообщением-приветствием с одной кнопкой «Я не робот». При этом новичок уже лишён права писать — если он попробует отправить сообщение, Telegram его не пропустит, пока ограничение не снято.

Разберём по шагам. @router.message(F.new_chat_members) — это магический фильтр aiogram: хэндлер сработает только на сообщениях о входе. Внутри мы перебираем new_chat_members, потому что за раз могут добавить сразу нескольких. Бота среди них пропускаем (if user.is_bot) — глупо требовать капчу от самого себя. Затем restrict_chat_member с пустыми правами молча затыкает рот новичку. И наконец собираем inline-клавиатуру: в callback_data мы зашиваем id того, кого проверяем, — это пригодится, чтобы кнопку не нажал кто-то другой.

Почему id прячем в callback_data

Кнопка одна, а в чате людей много. Если просто написать «нажми кнопку», то её может нажать любой случайный человек, и тогда мы снимем ограничение не с того. Поэтому мы кладём id нужного пользователя прямо в callback_data (получается строка вроде captcha:123456789) и потом сверяем: тот ли это человек нажал.

Пример 2. Обрабатываем нажатие и снимаем ограничение

Теперь вторая половина круга — хэндлер на callback. Когда новичок нажимает кнопку, нам прилетает callback_query. Мы достаём из callback_data зашитый id, сравниваем с тем, кто нажал, и если всё совпало — возвращаем право писать.

from aiogram.types import CallbackQuery

# Полные права: можно снова писать
UNMUTED = ChatPermissions(
    can_send_messages=True,
    can_send_other_messages=True,
    can_add_web_page_previews=True,
)


@router.callback_query(F.data.startswith("captcha:"))
async def on_captcha(callback: CallbackQuery):
    target_id = int(callback.data.split(":")[1])

    # Кнопку нажал не тот, кого проверяем
    if callback.from_user.id != target_id:
        await callback.answer(
            "Эта кнопка не для тебя \U0001F643",
            show_alert=True,
        )
        return

    # Возвращаем право писать
    await callback.bot.restrict_chat_member(
        chat_id=callback.message.chat.id,
        user_id=target_id,
        permissions=UNMUTED,
    )

    await callback.message.edit_text(
        f"{callback.from_user.full_name} прошёл проверку, добро пожаловать!"
    )
    await callback.answer("Готово, теперь можешь писать!")

Результат: в чате бот ответит так — если кнопку нажал правильный человек, сообщение-приветствие превратится в «… прошёл проверку, добро пожаловать!», а сверху всплывёт короткое уведомление «Готово, теперь можешь писать!». Если же кнопку трогает посторонний, он увидит всплывашку «Эта кнопка не для тебя», и ничего не изменится.

Главные детали. Фильтр F.data.startswith("captcha:") ловит именно наши кнопки капчи, не путая их с другими callback в боте. callback.data.split(":")[1] вытаскивает id обратно — это та самая строка, которую мы зашили в первом примере. Проверка callback.from_user.id != target_id — наш «фейс-контроль для кнопки»: чужому отвечаем всплывашкой и выходим через return. И обязательный callback.answer() в конце — без него у нажавшего на кнопке будет крутиться вечный «часик загрузки».

Маленький разбор: как разложить callback_data

Чтобы было понятнее, что именно делает split, посмотри на чистом Python — этот кусочек уже можно запустить:

data = "captcha:123456789"
prefix, user_id = data.split(":")
print("Тип проверки:", prefix)
print("Кого проверяем:", int(user_id))

Вывод:

Тип проверки: captcha
Кого проверяем: 123456789

Видишь: строка "captcha:123456789" по двоеточию делится на две части. Первую (captcha) мы используем, чтобы узнать «это вообще наша кнопка?», вторую превращаем в число функцией int() и получаем id участника. Ровно это и происходит в хэндлере, просто там вокруг ещё работа с Telegram.

Пример 3. Тайм-аут: что если кнопку так и не нажали

Спамер кнопку не нажмёт никогда. Но если просто оставить его в немом состоянии, он так и будет висеть в списке участников «мёртвой душой». Полезно добавить тайм-аут: дать минуту на проверку, и если человек не нажал — выгнать его из группы. Для отложенного действия в асинхронном коде используем asyncio.sleep, а исключить участника поможет ban_chat_member с быстрым разбаном (тогда человек сможет зайти снова, если это всё-таки был живой).

import asyncio

PASSED = set()  # id тех, кто уже прошёл капчу


async def kick_if_silent(bot, chat_id: int, user_id: int):
    await asyncio.sleep(60)  # ждём минуту
    if user_id in PASSED:
        return  # успел нажать — не трогаем
    # Бан + сразу разбан = «выгнать, но пусть сможет вернуться»
    await bot.ban_chat_member(chat_id, user_id)
    await bot.unban_chat_member(chat_id, user_id)

Результат: в чате бот ответит молча — спустя минуту после входа участник, не нажавший кнопку, просто исчезнет из списка группы. Тот, кто успел нажать (его id попал в PASSED), останется.

Чтобы это заработало, в первом хэндлере после ограничения нужно запустить фоновую задачу: asyncio.create_task(kick_if_silent(message.bot, message.chat.id, user.id)), а в хэндлере капчи при успехе добавить PASSED.add(target_id). Так бот не блокируется на эту минуту — он спокойно обслуживает остальных, а таймер тикает в фоне. Это и есть та самая асинхронность, которой ты раньше боялся: бот умеет «ждать» сразу много вещей одновременно, не зависая.

Почему именно бан с разбаном, а не просто бан? Если ты человека просто забанишь, он не сможет вернуться в группу, даже если очень захочет, — придётся вручную снимать бан. Но мы не уверены на сто процентов, что молчун — спамер. Может, человек зашёл с метро, связь пропала, и он не успел нажать кнопку за минуту. Поэтому связка «бан + сразу разбан» работает мягко: она выкидывает участника из чата прямо сейчас (чтобы он не висел немой душой), но не закрывает дверь насовсем — он может зайти заново по той же ссылке-приглашению и пройти капчу нормально. Для спамера это ничего не меняет (он всё равно не вернётся), а живого человека не наказывает зря.

Минута — это не догма. Для маленького дружеского чата хватит и тридцати секунд, для большого паблика, где люди заходят и отвлекаются, можно дать две-три минуты. Главное — не делать тайм-аут слишком коротким, иначе ты будешь выгонять живых людей, которые просто медленно читают. Подбирай число под свой чат и наблюдай, не жалуются ли участники, что их выкидывает.

Частые ошибки и подводные камни

  • Бот не админ — и ничего не работает. Метод restrict_chat_member доступен только администратору группы с правом ограничивать участников. Если бот не админ, Telegram вернёт ошибку, а новичок спокойно напишет спам. Сначала сделай бота админом — про права мы разбирали в прошлом уроке.
  • Забыл вызвать callback.answer(). Тогда у нажавшего на кнопке бесконечно крутится индикатор загрузки, и человек думает, что всё сломалось. Всегда отвечай на callback, даже коротким пустым await callback.answer().
  • Не проверил, кто нажал кнопку. Без сравнения callback.from_user.id с зашитым id любой участник чата (в том числе сам спамер) сможет «пройти проверку» за новичка. Кнопка должна работать только для того, кому адресована.
  • Требуешь капчу с самого бота или других ботов. Если не пропустить user.is_bot, бот попытается ограничить и вернувшегося админ-бота, и сам себя — будут ошибки и странные сообщения. Ботов в проверке пропускаем.
  • Используешь time.sleep вместо asyncio.sleep. Обычный time.sleep(60) заморозит вообще всего бота на минуту: он перестанет отвечать всем остальным. В асинхронном коде только await asyncio.sleep(...).
  • Ограничения работают не во всех типах чатов. Метод restrict_chat_member действует в супергруппах, а в обычных маленьких группах ограничить участника нельзя. Если твой бот пишет «нет прав», а ты уверен, что он админ, проверь: возможно, чат ещё не стал супергруппой. Обычно Telegram превращает группу в супергруппу автоматически, когда в ней включают расширенные настройки администрирования.

Мини-практика: «капча с выбором цыплёнка»

Простую капчу «нажми единственную кнопку» особо умный спам-бот всё же может прожать наугад. Сделай её чуть хитрее. Задание:

  1. Покажи новичку не одну, а три inline-кнопки с эмодзи разных животных, например 🐣, 🐱, 🐶, и в тексте напиши: «Нажми на цыплёнка».
  2. Зашей в callback_data правильной кнопки что-то вроде captcha_ok:<user_id>, а в остальные — captcha_fail:<user_id>.
  3. Сделай два хэндлера (или один с проверкой префикса): при нажатии на правильную — снимай ограничение, при нажатии на неправильную — отвечай всплывашкой «Не угадал, попробуй ещё» и ничего не снимай.
  4. Не забудь оставить проверку, что нажал именно проверяемый пользователь.

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

Итоги

Сегодня «Цыплёнок-помощник» получил настоящую охранную функцию. Ты научился: понимать, зачем нужна капча (отсекать спам-ботов на входе); ограничивать нового участника через restrict_chat_member с пустыми правами; показывать проверку inline-кнопкой и проверять, что нажал именно нужный человек по id из callback_data; снимать ограничение после успеха и даже выгонять молчунов по тайм-ауту через asyncio.sleep. Это уже взрослая защита, какую ставят в реальных больших чатах.

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

Проверьте себя
1. Зачем боту вообще нужна капча для новых участников группы?
AЧтобы отсекать спам-ботов, которые вступают и сразу сыплют рекламой
BЧтобы ускорить отправку сообщений в чате
CЧтобы Telegram давал боту больше прав автоматически
DЧтобы участники могли менять название группы
2. Каким методом aiogram бот лишает нового участника права писать?
Adelete_message
Brestrict_chat_member с запрещающими правами
Canswer с пустым текстом
Dban_chat_member навсегда
3. Зачем в callback_data кнопки зашивают id пользователя (например captcha:123456789)?
AЧтобы Telegram быстрее доставлял сообщение
BЭто требование, иначе кнопка не отобразится
CЧтобы проверить, что кнопку нажал именно тот новичок, а не посторонний
DЧтобы сохранить пользователя в базу данных
4. Что будет, если в хэндлере callback забыть вызвать callback.answer()?
AБот упадёт с ошибкой и перезапустится
BУ нажавшего на кнопке будет бесконечно крутиться индикатор загрузки
CСообщение удалится автоматически
DTelegram забанит бота
5. Почему для тайм-аута капчи нужен asyncio.sleep, а не обычный time.sleep?
Atime.sleep заморозит всего бота, и он перестанет отвечать всем остальным
Btime.sleep не умеет считать секунды
Casyncio.sleep работает быстрее в 10 раз
DTelegram запрещает использовать time.sleep
6. Почему в хэндлере входа мы пропускаем участников с user.is_bot?
AБоты не умеют читать сообщения
BЧтобы не пытаться ограничивать других ботов и самого себя, что вызовет ошибки
CTelegram не присылает ботов в new_chat_members
DБоты автоматически проходят капчу за 0 секунд