Telegram-боты, которые не бесят: 7 фич для вовлечения пользователей с кодом

от автора

Большинство Telegram-ботов выглядят одинаково. /start — стена текста — кнопки. Пользователь тыкает, получает ответ, закрывает. Никакого ощущения что за ботом стоит что-то живое. Конверсия падает, люди не возвращаются, и ты не понимаешь почему — ведь функционал вроде работает.

Проблема обычно не в функционале. Проблема в деталях. Бот отвечает мгновенно как машина, не помнит кто ты, не даёт ощущения прогресса, не реагирует на действия. Пользователь это чувствует — даже если не осознаёт. И просто уходит.

В этой статье я собрал 7 конкретных фич с кодом на aiogram 3.x которые это исправляют. Некоторые внедряются за пять минут, некоторые требуют больше времени — но каждая влияет либо на удержание, либо на монетизацию, либо на рост аудитории. Без воды, сразу к делу.

1. Рассылка за Telegram Stars: монетизация контента прямо в мессенджере

Telegram Stars — внутренняя валюта Telegram. Ты как разработчик получаешь звёзды и выводишь их в деньги через Fragment. Метод send_paid_media позволяет продавать контент прямо внутри бота — без подключения платёжных шлюзов, без форм, без головной боли с эквайрингом.

Как это выглядит для пользователя: приходит сообщение с описанием и размытым превью. Нажимает «Оплатить», подтверждает списание Stars — медиа мгновенно открывается. Весь процесс — секунд десять.

Где применяют:

  • Авторский контент: фотографы, художники, дизайнеры продают работы напрямую

  • Обучение: уроки и разборы в формате видео

  • Эксклюзивные рассылки: пользователь видит интригующий анонс и платит за полную версию

  • Закрытые материалы каналов и сообществ

from aiogram import Bot, Routerfrom aiogram.types import Message, InputPaidMediaPhoto, FSInputFilefrom aiogram.filters import Commandrouter = Router()@router.message(Command("buy"))async def send_paid_content(message: Message, bot: Bot):    await bot.send_paid_media(        chat_id=message.chat.id,        star_count=50,  # цена в Stars        media=[InputPaidMediaPhoto(media=FSInputFile("exclusive.jpg"))],        caption="🔥 Эксклюзивный контент — только для тех, кто платит"    )# Фиксируем покупку в базе данных@router.message(F.successful_payment)async def on_successful_payment(message: Message):    user_id = message.from_user.id    # await db.save_purchase(user_id)  # сохраняем в БД    await message.answer("✅ Оплата прошла! Приятного просмотра.")

Главное преимущество — Telegram сам показывает медиа размытым, сам принимает оплату, сам открывает контент после списания Stars. Твоя задача — только зафиксировать покупку в базе.

Ограничения:

  • Stars пока нельзя вывести в некоторых странах — проверяй на Fragment

  • Пользователь должен сначала купить Stars — не все к этому готовы

  • Курс конвертации устанавливает Telegram


2. Обязательная подписка на канал: быстрый рост аудитории

Один из самых эффективных приёмов для роста — пользователь не может пользоваться ботом пока не подпишется на канал. Звучит агрессивно, но работает. Особенно если бот полезный.

Схема: пользователь запускает бота → видит сообщение с просьбой подписаться → подписывается → нажимает кнопку «Проверить» → бот открывает полный доступ.

Совет: добавляй в это сообщение гифку или фото — так оно выглядит живее и не как стена текста.

from aiogram import Bot, Router, Ffrom aiogram.types import (    Message, CallbackQuery,    InlineKeyboardMarkup, InlineKeyboardButton,    FSInputFile)from aiogram.filters import CommandStartrouter = Router()CHANNEL_ID = "@your_channel"  # или числовой ID каналаasync def check_subscription(bot: Bot, user_id: int) -> bool:    member = await bot.get_chat_member(        chat_id=CHANNEL_ID,        user_id=user_id    )    return member.status not in ["left", "kicked"]def subscribe_keyboard() -> InlineKeyboardMarkup:    return InlineKeyboardMarkup(inline_keyboard=[        [InlineKeyboardButton(            text="📢 Подписаться на канал",            url=f"https://t.me/your_channel"        )],        [InlineKeyboardButton(            text="✅ Проверить подписку",            callback_data="check_sub"        )]    ])@router.message(CommandStart())async def start(message: Message, bot: Bot):    is_subscribed = await check_subscription(bot, message.from_user.id)    if not is_subscribed:        await message.answer_animation(            animation=FSInputFile("welcome.gif"),            caption=(                "👋 Привет! Чтобы пользоваться ботом,\n"                "сначала подпишись на наш канал 👇"            ),            reply_markup=subscribe_keyboard()        )        return    await message.answer("Добро пожаловать! 🎉")@router.callback_query(F.data == "check_sub")async def check_sub_callback(callback: CallbackQuery, bot: Bot):    is_subscribed = await check_subscription(bot, callback.from_user.id)    if not is_subscribed:        await callback.answer(            "❌ Ты ещё не подписался!",            show_alert=True        )        return    await callback.message.delete()    await callback.message.answer("✅ Подписка подтверждена! Добро пожаловать.")    await callback.answer()

Плюсы подхода:

  • Быстрый рост подписчиков канала

  • Канал можно дополнительно монетизировать через рекламу и платные посты

  • Пользователи уже «тёплые» — знакомы с брендом до того как начали пользоваться ботом


3. Эффекты сообщений: бот с настроением

Мало кто знает, что Telegram позволяет отправлять сообщения с анимационными эффектами — конфетти, огонь, сердечки. Задаётся через параметр message_effect_id.

Доступные эффекты:

Эффект

ID

🔥 Огонь

5104841245755180586

👍 Лайк

5107584321108051014

👎 Дизлайк

5104858069142078462

❤️ Сердце

5044134455711629726

🎉 Конфетти

5046509860389126442

💩 Какашка

5046589136895476101

from aiogram import Routerfrom aiogram.types import Messagefrom aiogram.filters import Commandrouter = Router()# ID эффектовEFFECT_FIRE = "5104841245755180586"EFFECT_CONFETTI = "5046509860389126442"EFFECT_HEART = "5044134455711629726"@router.message(Command("buy_success"))async def payment_success(message: Message):    await message.answer(        "🎉 Оплата прошла успешно!",        message_effect_id=EFFECT_CONFETTI    )@router.message(Command("like"))async def send_like(message: Message):    await message.answer(        "Спасибо за отзыв! ❤️",        message_effect_id=EFFECT_HEART    )@router.message(Command("hot_deal"))async def hot_deal(message: Message):    await message.answer(        "🔥 Горячее предложение — только сегодня!",        message_effect_id=EFFECT_FIRE    )

Где использовать:

  • Успешная оплата → конфетти

  • Приветствие нового пользователя → сердечко

  • Срочная акция → огонь

  • Ошибка → дизлайк (да, и такое бывает уместно)

Эффекты — мелочь, но именно из таких мелочей складывается ощущение «живого» бота.


4. Стриминг ответа: текст появляется в реальном времени

Вместо того чтобы ждать пока бот обработает запрос и пришлёт сообщение целиком — текст появляется прямо по мере генерации. Так работает ChatGPT, так работают все современные AI-боты.

Механика простая: отправляем пустое сообщение, потом редактируем его каждые N миллисекунд добавляя новые куски текста.

import asynciofrom aiogram import Routerfrom aiogram.types import Messagefrom aiogram.filters import Commandfrom typing import AsyncGeneratorrouter = Router()# Симуляция стриминга — замени на реальный AI (OpenAI, etc.)async def fake_stream(text: str) -> AsyncGenerator[str, None]:    for word in text.split():        yield word + " "        await asyncio.sleep(0.1)async def stream_reply(message: Message, generator: AsyncGenerator) -> None:    # Отправляем сообщение с курсором    sent = await message.answer("▌")    full_text = ""    last_edit = asyncio.get_event_loop().time()    async for chunk in generator:        full_text += chunk        now = asyncio.get_event_loop().time()        # Редактируем не чаще чем раз в 0.5 сек — иначе flood limit        if now - last_edit >= 0.5:            await sent.edit_text(full_text + "▌")            last_edit = now    # Финальное сообщение без курсора    await sent.edit_text(full_text)@router.message(Command("ask"))async def handle_ask(message: Message) -> None:    response_text = "Это пример стриминга в Telegram боте. Текст появляется постепенно прямо как в ChatGPT."    await stream_reply(message, fake_stream(response_text))

С реальным OpenAI стримингом:

from openai import AsyncOpenAIclient = AsyncOpenAI(api_key="your_key")async def openai_stream(user_message: str) -> AsyncGenerator[str, None]:    stream = await client.chat.completions.create(        model="gpt-4o",        messages=[{"role": "user", "content": user_message}],        stream=True    )    async for chunk in stream:        delta = chunk.choices[0].delta.content        if delta:            yield delta@router.message(Command("gpt"))async def handle_gpt(message: Message) -> None:    await stream_reply(message, openai_stream(message.text))

Главное про flood limit: Telegram не даёт редактировать сообщение чаще чем раз в секунду. Поэтому копим чанки и редактируем с задержкой 0.5s — так и плавно и без бана.

Если хочешь попробовать как это ощущается на практике, то у меня есть бесплатный телеграм-бот. Это бот с четырьмя моделями внутри: ChatGPT, Gemini, Grok и DeepSeek. Отвечает в режиме реального времени — текст появляется прямо по мере генерации.


5. Онбординг с прогресс-баром: снижаем drop-off при регистрации

Когда пользователь регистрируется в боте и не видит прогресса — он бросает на середине. Прогресс-бар [■■■□□] решает это. Человек видит что осталось 2 шага из 5 и продолжает.

from aiogram import Router, Ffrom aiogram.types import Message, ReplyKeyboardMarkup, KeyboardButton, ReplyKeyboardRemovefrom aiogram.filters import CommandStartfrom aiogram.fsm.context import FSMContextfrom aiogram.fsm.state import State, StatesGrouprouter = Router()class Registration(StatesGroup):    name = State()    age = State()    city = State()    phone = State()def progress_bar(current: int, total: int) -> str:    filled = "■" * current    empty = "□" * (total - current)    return f"[{filled}{empty}] Шаг {current} из {total}"@router.message(CommandStart())async def start_registration(message: Message, state: FSMContext):    await state.set_state(Registration.name)    await message.answer(        f"{progress_bar(1, 4)}\n\n"        f"👋 Привет! Давай познакомимся.\n"        f"Как тебя зовут?",        reply_markup=ReplyKeyboardRemove()    )@router.message(Registration.name)async def get_name(message: Message, state: FSMContext):    await state.update_data(name=message.text)    await state.set_state(Registration.age)    await message.answer(        f"{progress_bar(2, 4)}\n\n"        f"Отлично, {message.text}! Сколько тебе лет?"    )@router.message(Registration.age)async def get_age(message: Message, state: FSMContext):    await state.update_data(age=message.text)    await state.set_state(Registration.city)    skip_keyboard = ReplyKeyboardMarkup(        keyboard=[[KeyboardButton(text="Пропустить")]],        resize_keyboard=True    )    await message.answer(        f"{progress_bar(3, 4)}\n\n"        f"Из какого ты города?",        reply_markup=skip_keyboard    )@router.message(Registration.city)async def get_city(message: Message, state: FSMContext):    city = None if message.text == "Пропустить" else message.text    await state.update_data(city=city)    await state.set_state(Registration.phone)    phone_keyboard = ReplyKeyboardMarkup(        keyboard=[[KeyboardButton(text="📱 Поделиться номером", request_contact=True)]],        resize_keyboard=True    )    await message.answer(        f"{progress_bar(4, 4)}\n\n"        f"Последний шаг — поделись номером телефона:",        reply_markup=phone_keyboard    )@router.message(Registration.phone, F.contact)async def get_phone(message: Message, state: FSMContext):    data = await state.get_data()    await state.clear()    await message.answer(        f"✅ Готово! Регистрация завершена.\n\n"        f"Имя: {data['name']}\n"        f"Возраст: {data['age']}\n"        f"Город: {data.get('city', 'не указан')}",        reply_markup=ReplyKeyboardRemove()    )

Прогресс-бар — психологический трюк. Люди не любят бросать незаконченное дело. Особенно когда видят что осталось чуть-чуть.


6. Персонализация: бот который тебя помнит

Персонализация — это не просто «Привет, Иван». Это когда бот помнит что ты делал, предлагает то что тебе актуально и не заставляет вводить одно и то же по второму кругу.

from aiogram import Router, Ffrom aiogram.types import Messagefrom aiogram.filters import CommandStartfrom datetime import datetimerouter = Router()# Простой in-memory стор (в продакшене заменяй на БД)user_storage: dict = {}def get_user(user_id: int) -> dict:    if user_id not in user_storage:        user_storage[user_id] = {            "name": None,            "visits": 0,            "last_visit": None,            "last_action": None        }    return user_storage[user_id]@router.message(CommandStart())async def start(message: Message):    user_id = message.from_user.id    first_name = message.from_user.first_name    user = get_user(user_id)    user["visits"] += 1    user["name"] = first_name    last_visit = user["last_visit"]    user["last_visit"] = datetime.now()    # Первый визит    if user["visits"] == 1:        await message.answer(            f"👋 Привет, {first_name}! Рад познакомиться.\n"            f"Вот что я умею: ..."        )    # Возвращающийся пользователь    elif user["last_action"]:        await message.answer(            f"С возвращением, {first_name}! 👋\n"            f"В прошлый раз ты {user['last_action']}.\n"            f"Продолжим?"        )    # Постоянный пользователь    else:        await message.answer(            f"О, {first_name}! Уже {user['visits']}-й визит 🔥\n"            f"Рад видеть постоянного гостя!"        )@router.message(F.text == "🔍 Поиск")async def search(message: Message):    user = get_user(message.from_user.id)    user["last_action"] = "использовал поиск"    # ... логика поиска

Что ещё можно персонализировать:

  • Язык интерфейса по language_code из message.from_user

  • Рекомендации на основе истории действий

  • Напоминания с упоминанием имени

  • Разные онбординги для разных сегментов пользователей


7. Кастомный плейсхолдер: подсказка прямо в строке ввода

Когда пользователь открывает бота, в строке ввода написано «Сообщение». Нейтрально, но не информативно. Параметр input_field_placeholder позволяет заменить этот текст на свой.

Обычный плейсхолдер:

Кастомный плейсхолдер:

from aiogram import Routerfrom aiogram.types import Message, ReplyKeyboardMarkup, KeyboardButtonfrom aiogram.filters import CommandStartrouter = Router()def main_keyboard() -> ReplyKeyboardMarkup:    return ReplyKeyboardMarkup(        keyboard=[[KeyboardButton(text="🏠 Главное меню")]],        resize_keyboard=True,        is_persistent=True,  # клавиатура не скрывается после нажатия        input_field_placeholder="Введите название города...",    )@router.message(CommandStart())async def start(message: Message):    await message.answer(        text="Привет! Введи город и я покажу погоду.",        reply_markup=main_keyboard(),    )

Параметр is_persistent=True нужен чтобы клавиатура не скрывалась после нажатия — иначе плейсхолдер пропадёт вместе с ней.

Примеры под разные боты:

# Бот погодыinput_field_placeholder="Введите название города..."# Бот поддержкиinput_field_placeholder="Опишите вашу проблему..."# Финансовый ботinput_field_placeholder="Введите сумму в рублях..."# Поиск по каталогуinput_field_placeholder="Начните вводить название товара..."

Ограничения:

  • Максимум 64 символа

  • Поддерживает эмодзи

  • Работает только с ReplyKeyboardMarkup — для инлайн-клавиатур недоступно

  • Чисто визуальная штука, на логику бота не влияет


Если после этой статьи захотелось сделать своего бота — попробуй @GenerateYourAIBot. Пишешь промпт с описанием что должен делать бот, он генерирует готовую архитектуру: структуру файлов, хэндлеры, FSM, логику. Не заменяет руки, но сильно ускоряет старт — особенно когда проект нестандартный и не хочется думать с чего начать.

Итого

Вот что у нас получилось:

Фича

Сложность

Что даёт

Stars / платный контент

⭐⭐⭐

Монетизация без эквайринга

Обязательная подписка

⭐⭐

Быстрый рост канала

Эффекты сообщений

Ощущение живого бота

Typing imitation

Естественный диалог

Прогресс-бар онбординга

⭐⭐

Снижение drop-off

Персонализация

⭐⭐

Лояльность пользователей

Кастомный плейсхолдер

Чище UX без лишнего текста

Если времени мало — начни с эффектов сообщений и typing imitation. Пять минут кода, а разница заметна сразу. Потом добавляй прогресс-бар и персонализацию — это уже влияет на удержание.

ссылка на оригинал статьи https://habr.com/ru/articles/1043608/