inlineQuery
Метод inlineQuery в GramIO позволяет вашему боту отвечать на инлайн-запросы, отправленные пользователями. Инлайн-запрос - это особый тип сообщения, когда пользователи могут искать контент от вашего бота, введя запрос в поле ввода сообщения, не взаимодействуя напрямую с ботом.
WARNING
Вам необходимо включить эту опцию через @BotFather. Отправьте команду /setinline, выберите бота и укажите текст-заполнитель, который пользователь увидит в поле ввода после ввода имени вашего бота.
Основное использование
Ответ на инлайн-запрос с использованием регулярного выражения
Вы можете настроить бота на прослушивание определенных инлайн-запросов, соответствующих регулярному выражению, и ответить результатами. Вот пример:
bot.inlineQuery(/search (.*)/i, async (context) => {
if (context.args) {
await context.answer([
InlineQueryResult.article(
"id-1",
`Результат для ${context.args.at(1)}`,
InputMessageContent.text(
`Это результат сообщения для запроса ${context.args.at(1)}`
)
),
]);
}
});В этом примере:
- Бот прослушивает инлайн-запросы, соответствующие шаблону
search (.*). - Если соответствие найдено, бот извлекает поисковый запрос и возвращает список результатов инлайн-запроса.
TIP
context.answer() принимает параметры answerInlineQuery вторым аргументом. Наиболее важный из них — cache_time: как долго Telegram кэширует результаты на своих серверах (по умолчанию 300 с). Передайте cache_time: 0 для динамических результатов, которые меняются с каждым нажатием клавиши — и особенно во время разработки. Все полезные параметры разобраны ниже в разделе опции context.answer().
Ограничения для превью
thumbnail_url у любого варианта InlineQueryResult* (article, photo, video, document, gif и т.д.) должен вести на JPEG — PNG и WebP молча не отрендерятся в части Telegram-клиентов, и превью просто исчезнет, не откатившись к дефолту. Держите картинку ≤320×320 px и ≤200 KB; слишком большой JPEG ломается так же. Если превью не показывается, а URL доступен — начните проверку с формата и размеров.
Параметры
trigger: Условие, которому должен соответствовать инлайн-запрос. Это может быть регулярное выражение, строка или пользовательская функция.- Регулярное выражение: Соответствует запросам, которые соответствуют определенному шаблону.
- Строка: Соответствует точной строке запроса.
- Функция: Оценивает запрос на основе пользовательской логики. Должна возвращать true для соответствия.
handler: Функция, которая обрабатывает инлайн-запрос. Она получает объектcontext, который включает детали о запросе и предоставляет методы для ответа.options: Дополнительные опции для обработки выбора результата.onResult: Функция, которая вызывается, когда пользователь выбирает результат инлайн-запроса. Она может использоваться для изменения или обновления сообщения после выбора. Использует ChosenInlineResult под капотом.
IMPORTANT
Вы можете изменять только сообщения, которые содержат InlineKeyboard. Подробнее о ChosenInlineResult.
Как работает inlineQuery
Сопоставление запроса: Когда пользователь вводит инлайн-запрос, метод
inlineQueryпроверяет, соответствует ли запрос предоставленномуtrigger. Это может быть прямое соответствие (строка), соответствие шаблону (регулярное выражение) или проверка условия (функция).Обработка запроса: Если запрос соответствует, вызывается функция
handler. Внутри этой функции вы можете получить доступ к параметрам запроса, сгенерировать результаты и отправить их обратно пользователю, используяcontext.answer().Реагирование на выбор результата: Если предоставлена опция
onResult, бот прослушивает, когда пользователь выбирает один из результатов инлайн-запроса. Затем предоставленная функция может, например, изменить текст сообщения.
Пример: Пользовательский обработчик инлайн-запросов
Вот более подробный пример, демонстрирующий использование как триггера инлайн-запроса, так и обработки выбора результата:
bot.inlineQuery(
/search (.*)/i,
async (context) => {
if (context.args) {
await context.answer(
[
InlineQueryResult.article(
"result-1",
`Результат для ${context.args[1]}`,
InputMessageContent.text(
`Вы искали: ${context.args[1]}`
),
{
reply_markup: new InlineKeyboard().text(
"Получить подробности",
"details-callback"
),
}
),
],
{
cache_time: 0,
}
);
}
},
{
onResult: (context) => context.editText("Вы выбрали результат!"),
}
);В этом примере:
- Бот прослушивает инлайн-запросы, которые начинаются с
search, за которыми следует поисковый термин. - Бот отвечает результатом инлайн-запроса, который включает кнопку с надписью "Получить подробности."
- Когда пользователь выбирает этот результат, бот редактирует сообщение, чтобы указать, что был сделан выбор.
Связка с кнопками switchTo* — лучший UX для инлайн-режима
Большинство пользователей никогда не наберут @YourBot <запрос> сами — это трение, о котором они попросту не знают. Решение: спрятать инлайн-запрос за инлайн-кнопку. Три кнопки switchTo* телепортируют пользователя прямо в инлайн-режим с предзаполненным запросом, а ваш inlineQuery-хендлер отдаёт курированный пикер. Один тап, выбор из списка, Telegram сам постит результат — без ввода, без команд.
switchToCurrentChat— открывает инлайн-режим в этом же чате. Идеально для «выбери одно из вариантов» внутри приватного UI бота.switchToChat— предлагает выбрать любой чат и пошарить туда. Канонический flow «Отправить другу» / «Поделиться карточкой».switchToChosenChat— то же самое, но с фильтром по типу чата (личка / группа / канал / бот).
Пример шаринга — /share отвечает кнопкой-пикером, инлайн-хендлер отдаёт конкретную карточку по id:
bot.command("share", (ctx) =>
ctx.send("Поделиться карточкой:", {
reply_markup: new InlineKeyboard().switchToChosenChat("📤 Отправить в…", {
query: "card-42",
allow_user_chats: true,
allow_group_chats: true,
}),
}),
);
bot.inlineQuery(/^card-(\d+)$/, async (ctx) => {
const card = await loadCard(ctx.args![1]);
return ctx.answer(
[
InlineQueryResult.article(
card.id,
card.title,
InputMessageContent.text(card.body),
),
],
{ cache_time: 0, is_personal: true },
);
});Почему связка настолько сильная:
- Ноль ввода.
queryпредзаполнен — пользователь только выбирает результат. - Нативный UI пикера. Диалог инлайн-результатов пользователи уже знают по
@gif,@stickers,@vote— узнаваемость в подарок. - Не выпадаешь из потока.
switchToCurrentChatне уводит из чата;switchToChatпревращает шаринг в один тап. - Без мини-аппов, без диплинков, без редиректов на бэкенд. Весь round-trip — нативный Telegram.
Любая фича, которая иначе превратилась бы в «введите команду, потом ответьте опцией» — голосование, пикеры GIF/стикеров, выбор языка или таймзоны, шаринг цитат/карточек, пикер контактов — почти всегда лучше делать через switchTo* + инлайн-хендлер.
Опции context.answer()
Второй аргумент context.answer() принимает все параметры answerInlineQuery. Четыре, которые реально пригодятся:
button — пригласить пользователя войти, пройти онбординг или открыть Mini App
Инлайн-режим работает в любом чате — он не может показывать приватный UI вашего бота, принимать загрузку файлов или вести многошаговые сценарии. Если для выдачи результатов нужно что-то такое (авторизация, подключение стороннего аккаунта, согласие на условия, настройка параметров), верните пустой массив results плюс верхнюю button, которая уведёт пользователя туда, где ваш бот эти сценарии может выполнить.
InlineQueryResultsButton — это discriminated union: укажите ровно одно из:
start_parameter— открывает приватный чат и вызывает/start <parameter>, который попадает в вашbot.command("start")какctx.args. 1-64 символа, толькоA-Z,a-z,0-9,_,-.web_app— запускает Mini App. См. гайд по TMA.
bot.inlineQuery(async (ctx) => {
if (!(await isAuthenticated(ctx.from.id))) {
return ctx.answer([], {
cache_time: 0, // не кэшируем приглашение к логину
is_personal: true, // оно персонально для каждого пользователя
button: {
text: "Войти, чтобы продолжить",
start_parameter: "login-inline",
},
});
}
// ...обычные результаты после авторизации
});
// В обработчике /start — принять deep-link payload
bot.command("start", (ctx) => {
if (ctx.args === "login-inline") {
return ctx.send("Давайте вас авторизуем…");
}
});Это канонический паттерн «пользователь не авторизован → уводим в ЛС» для инлайн-ботов — из инлайн-режима сам бот никак пользователя в приватный чат не вытянет. button — единственный путь.
is_personal — персональные результаты (отключает общий кэш)
Передавайте is_personal: true, когда результаты зависят от конкретного пользователя (его подписки, избранное, права). Без этого флага персональная выдача одного пользователя может утечь другому с тем же запросом — Telegram по умолчанию шарит кэш между пользователями.
next_offset — пагинация
Передайте короткий токен (максимум 64 байта) — это сигнал «есть ещё результаты». Когда пользователь долистает до конца вашей выдачи, Telegram повторит запрос, и токен придёт в ctx.inlineQuery.offset. Передайте пустую строку, чтобы сказать «больше нет».
bot.inlineQuery(async (ctx) => {
const page = Number(ctx.inlineQuery.offset) || 0;
const { items, hasMore } = await fetchPage(page);
return ctx.answer(
items.map((i) =>
InlineQueryResult.article(
i.id,
i.title,
InputMessageContent.text(i.body)
)
),
{ next_offset: hasMore ? String(page + 1) : "" }
);
});cache_time — длительность общего кэша
Telegram кэширует вашу выдачу для одинаковых запросов у всех пользователей на указанное число секунд (по умолчанию 300). Ставьте 0 во время разработки и для динамического контента. Комбинируйте с is_personal: true, если персональные результаты всё же должны коротко кэшироваться для самого пользователя.