MAATRIX / Блог / Telegram-бот с inline-режимом: настройка

Telegram-бот с inline-режимом: настройка

Telegram-бот с inline-режимом: настройка

MAATRIX

Обычный бот требует, чтобы пользователь открыл с ним диалог и написал команду или нажал кнопку. Inline-режим убирает этот шаг: человек набирает @имя_бота запрос прямо в поле ввода любого чата — личного, группового, канала — и бот присылает список результатов, которые вставляются в сообщение без перехода в другой диалог. Ниже — как включить режим у BotFather, написать обработчик на aiogram 3, собрать результаты разных типов, закэшировать их и не упереться в ограничения, которые Telegram накладывает на inline.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →

Что такое inline-режим и когда он выигрывает у обычного диалога

Технически inline-режим — это отдельный тип апдейта inline_query, который прилетает боту, когда пользователь набрал в любом поле ввода @username и что-то после пробела. Бот отвечает списком результатов (answerInlineQuery), пользователь тапает по одному — и в текущий чат от его имени отправляется готовое сообщение. Диалог с ботом при этом может вообще не существовать: достаточно, чтобы у человека была открыта Telegram-переписка с кем угодно.

Режим подходит для сценариев «вставить готовый кусок контента в чужой разговор»: поиск стикеров и GIF (так устроены официальные @gif и @pic), быстрый перевод фразы, подстановка курса валюты или котировки, вставка ссылки на товар из каталога, генерация цитаты или мема. Не подходит там, где нужен полноценный диалог с состоянием — многошаговые формы, оплата, авторизация с несколькими вопросами: inline отдаёт один запрос и один список ответов, без памяти о предыдущем шаге (если не городить это вручную через query.from.id).

Второй момент — сам inline-запрос видит только пользователь, который его набрал, а итоговое сообщение в чате видят уже все участники. Поэтому контент результата должен быть сам по себе презентабельным сообщением, а не служебным «ок, обрабатываю».

Включение inline-режима через BotFather

По умолчанию у нового бота inline выключен. Включается тремя командами в диалоге с @BotFather:

/setinline
(выбрать бота из списка)
(ввести placeholder — текст-подсказку в поле ввода, например: "Поиск товара...")

Placeholder — это то, что пользователь видит серым текстом в поле ввода сразу после @username, до того как начал печатать запрос. Ограничение на длину небольшое (в районе десятков символов), так что делайте его коротким и по делу.

Дополнительно полезны:

/setinlinefeedback
(выбрать бота)
(Enabled / Disabled / Some — как часто присылать боту апдейт chosen_inline_result)

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

После /setinline бот начинает получать апдейты inline_query — но только если ваш код их вообще обрабатывает. Дальше — код.

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

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

Арендовать VPS

Обработчик InlineQuery в aiogram

В aiogram 3.x обработчик регистрируется как отдельный тип апдейта через роутер:

from aiogram import Router
from aiogram.types import InlineQuery

router = Router()

@router.inline_query()
async def handle_inline_query(query: InlineQuery):
    text = query.query.strip()
    user_id = query.from_user.id

    if not text:
        # пустой запрос — можно вернуть подсказки по умолчанию
        await query.answer([], cache_time=1)
        return

    results = build_results(text)
    await query.answer(results, cache_time=60, is_personal=True)

query.query — то, что пользователь напечатал после @username. query.offset — строка для пагинации (о ней ниже), query.from_user — тот, кто набирает запрос, а не тот, кто его в итоге увидит. answer() — единственный обязательный вызов: не ответить в течение примерно 10 секунд означает, что Telegram покажет пользователю пустой список.

Роутер с inline-обработчиком подключается к диспетчеру так же, как обычные роутеры с командами:

from aiogram import Bot, Dispatcher

bot = Bot(token=TOKEN)
dp = Dispatcher()
dp.include_router(router)

Если бот уже обрабатывает обычные сообщения и команды, @router.inline_query() просто добавляется рядом — это независимый поток апдейтов, конфликтов с @router.message() не будет.

Формирование результатов: текст, статьи, картинки

Результат inline-запроса — это список объектов InlineQueryResult*, у каждого свой набор полей. Самый частый — статья с текстовым содержимым:

import hashlib
from aiogram.types import InlineQueryResultArticle, InputTextMessageContent

def build_results(text: str) -> list:
    result_id = hashlib.md5(text.encode()).hexdigest()
    return [
        InlineQueryResultArticle(
            id=result_id,
            title=f"Курс: {text}",
            description="Нажмите, чтобы вставить в чат",
            input_message_content=InputTextMessageContent(
                message_text=f"Курс {text}: уточняется на сервере"
            ),
        )
    ]

id должен быть уникальным в пределах одного ответа и не длиннее 64 байт — обычно берут хэш от текста запроса или от идентификатора сущности в базе. input_message_content — то, что реально уйдёт в чат при выборе; оно не обязано совпадать с title/description, которые видны только в выпадающем списке до выбора.

Для картинок используется InlineQueryResultPhoto — но с важной оговоркой: файл должен быть доступен по прямой публичной ссылке (или уже иметь file_id, полученный ранее от Telegram), локальный путь на диске сервера не подходит:

from aiogram.types import InlineQueryResultPhoto

InlineQueryResultPhoto(
    id=result_id,
    photo_url="https://cdn.example.com/preview.jpg",
    thumbnail_url="https://cdn.example.com/thumb.jpg",
    caption="Товар со склада",
)

Если ваша версия aiogram старше и падает с ошибкой на незнакомом аргументе thumbnail_url — в части релизов 3.x он назывался thumb_url; проверьте pip show aiogram и при необходимости обновитесь (pip install -U aiogram) либо используйте старое имя параметра.

Помимо статей и фото есть InlineQueryResultGif, InlineQueryResultVideo, InlineQueryResultDocument, InlineQueryResultAudio — все требуют внешний URL на контент, а не бинарные данные в теле ответа. К любому результату можно добавить reply_markup с инлайн-клавиатурой — кнопки под уже отправленным сообщением работают как обычно.

Кэширование результатов

Кэширование в inline-режиме двухуровневое. Первый уровень — на стороне Telegram, через параметр cache_time в answer():

await query.answer(results, cache_time=300, is_personal=False)

cache_time — сколько секунд серверы Telegram могут отдавать этот же ответ повторно без нового обращения к вашему боту, если кто-то введёт идентичный запрос. Если не передать параметр, применяется значение по умолчанию (около 300 секунд по документации Bot API — но полагаться на дефолт не стоит, лучше указывать явно). is_personal=True говорит Telegram кэшировать ответ отдельно для каждого пользователя (нужен, если результат зависит от user_id — например, показывает избранное конкретного человека); is_personal=False — общий кэш на всех, что снижает нагрузку на бота при популярных общих запросах вроде курсов валют или погоды.

Второй уровень — кэш на стороне самого бота, до похода во внешний источник данных. Если результаты собираются из БД или внешнего API с задержкой, есть смысл держать TTL-кэш в Redis или в памяти процесса:

import time

_cache: dict[str, tuple[float, list]] = {}
TTL = 30

def get_cached(query_text: str):
    entry = _cache.get(query_text)
    if entry and time.time() - entry[0] < TTL:
        return entry[1]
    return None

def set_cached(query_text: str, results: list):
    _cache[query_text] = (time.time(), results)

На VPS с несколькими воркерами процесс-локальный словарь не годится — нужен общий Redis, иначе кэш «размазывается» между процессами. Держать Redis рядом с ботом на одном сервере обычно проще, чем выносить его в отдельный managed-сервис: меньше сетевых задержек, а конфигурация — пара строк в docker-compose.yml или systemd-юнит redis-server.

Ограничения inline-режима

Прежде чем проектировать функциональность вокруг inline, стоит знать, чего он не умеет:

  • Нельзя отправить произвольный файл с диска. Контент результатов (фото, видео, документы) отдаётся только по публичному URL или через уже известный Telegram file_id — то есть файл сначала нужно один раз загрузить боту в обычном диалоге или в служебный канал, получить file_id, и уже его переиспользовать в InlineQueryResult*.
  • Ограничена длина самого запроса. Строка после @username обрезается Telegram на стороне клиента — рассчитывайте, что пользователь физически не наберёт длинный запрос, и не проектируйте синтаксис с множеством параметров в одну строку.
  • Ограничено число результатов в одном ответе. Bot API принимает список результатов, но не бесконечный — на практике за один answer() разумно отдавать не больше нескольких десятков вариантов, а для длинных списков использовать пагинацию через next_offset.
  • Пагинация — забота бота, не Telegram. Клиент присылает query.offset таким, каким вы сами вернули его в предыдущем ответе через next_offset; если не реализовать этот цикл, пользователь увидит только первую порцию результатов и не сможет долистать до остальных.
  • Нет промежуточного шага между запросом и ответом. Пользователь либо получает готовый список сразу, либо ничего — уточняющий вопрос в духе «а какой город?» внутри inline не задать, для многошаговых сценариев всё равно понадобится обычный диалог с ботом (можно подтолкнуть к нему через switch_pm_text/switch_pm_parameter в answer(), которые показывают кнопку «Открыть бота» над списком результатов).
  • Ответ ограничен по времени. Если обработчик не уложился в несколько секунд (поход в медленный внешний API, тяжёлый запрос к БД), Telegram покажет пользователю пустой список — асинхронные тяжёлые операции лучше выносить в фон и отвечать из уже прогретого кэша.

Все числовые лимиты (длина запроса, число результатов, тайминги) стоит сверять с актуальной документацией Bot API на момент разработки — Telegram их иногда пересматривает, и в статью закладывать точные цифры как гарантию не стоит.

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

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

Арендовать VPS

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

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

Нужен ли webhook для inline-режима, или подойдёт polling?

Работает и то, и другое — тип получаемых апдейтов (inline_query, chosen_inline_result) не зависит от способа доставки. Разница между webhook и polling для inline та же, что и для обычных сообщений: на старте проще запускать polling, при заметном трафике выгоднее переходить на webhook.

Почему бот не отвечает на inline-запрос, хотя код без ошибок?

Первая проверка — включён ли режим у BotFather (/setinline); вторая — подключён ли router с обработчиком inline_query в диспетчер; третья — не превышает ли обработка времени, после которого Telegram отдаёт пустой список.

Можно ли использовать inline-режим в группах, где бот не добавлен?

Да, это и есть основной сценарий: пользователь набирает @username в любом чате, бот в этот чат добавлять не нужно — сообщение придёт от имени пользователя, а не от бота.

Как хранить состояние между inline-запросом и выбором результата?

Через chosen_inline_result (включается /setinlinefeedback) и уникальный id результата — по нему можно подтянуть полные данные из БД или кэша, не запихивая всё в input_message_content заранее.

Сколько ресурсов сервера нужно под inline-бота?

Прикидки по памяти и CPU для типового Telegram-бота на VPS собраны в отдельной статье про нужный объём RAM; inline добавляет нагрузку в основном на кэш-слой (Redis) при высокой частоте запросов, сам обработчик лёгкий.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →