Inline mode
Users can interact with bots using inline queries, by starting a message with bot's username and then typing their query.
You can learn more about them in Bot API docs
Example of an inline bot
Handling inline queries
To handle inline queries to your bot, simply use onInlineQuery
:
dp.onInlineQuery(async (query) => {
// ...
})
dp.onInlineQuery(async (query) => {
// ...
})
You can also use filters.regex
to filter based on the query text:
dp.onInlineQuery(
filters.regex(/^cats /),
async (query) => {
// ...
}
)
dp.onInlineQuery(
filters.regex(/^cats /),
async (query) => {
// ...
}
)
Answering to inline queries
Answering an inline query means sending results of the query to Telegram. The results may contain any media, including articles, photos, videos, stickers, etc, and can also be heterogeneous, which means that you can use different media types in results for the same query.
To answer a query, simply use .answer()
and pass an array that contains results:
dp.onInlineQuery(async (query) => {
query.answer([...])
})
dp.onInlineQuery(async (query) => {
query.answer([...])
})
TIP
Clients wait about 10 seconds for results, after which they assume the bot timed out. Make sure not to do heavy computations there!
Results
Every inline result must contain a unique result ID, which can be later used in chosen inline result updates updates to determine which one was chosen.
Media inside inline results must be provided via either HTTP URL, or re-used from Telegram (e.g. using File IDs). Telegram does not allow uploading new files directly to inline results.
When choosing a result, by default, a message containing the respective result is sent, but the message contents can be customized using message
field.
Telegram supports a bunch of result types, and mtcute supports sending all of them as an inline result. In the below examples we'll be using direct URLs to content, but you can also use File IDs instead.
Article
An article is a result that contains a title, description and optionally a thumbnail and a URL:
Article result example
dp.onInlineQuery(async (query) => {
query.answer([
BotInline.article(
'RESULT_ID',
{
title: 'Article title',
description: 'Article description',
thumb: 'https://example.com/image.jpg',
url: 'https://example.com/some-article.html'
}
)
])
})
dp.onInlineQuery(async (query) => {
query.answer([
BotInline.article(
'RESULT_ID',
{
title: 'Article title',
description: 'Article description',
thumb: 'https://example.com/image.jpg',
url: 'https://example.com/some-article.html'
}
)
])
})
When choosing this result, by default, a message with the following text is sent (Handlebars syntax is used here):
For the above example, this would result in the following message:
GIF
You can send an animated GIF (either real GIF, or an MP4 without sound) as a result.
GIF result example
dp.onInlineQuery(async (query) => {
query.answer([
BotInline.gif(
'RESULT_ID',
'https://media.tenor.com/videos/98bf1db10cb172aae086b09ae88ebf22/mp4'
)
])
})
dp.onInlineQuery(async (query) => {
query.answer([
BotInline.gif(
'RESULT_ID',
'https://media.tenor.com/videos/98bf1db10cb172aae086b09ae88ebf22/mp4'
)
])
})
You can also add title and description, however only some clients display them (e.g. Telegram Desktop doesn't, screenshot below is from Telegram for Android)
GIF result with title
dp.onInlineQuery(async (query) => {
query.answer([
BotInline.gif(
'RESULT_ID',
'https://media.tenor.com/videos/98bf1db10cb172aae086b09ae88ebf22/mp4',
{
title: 'GIF title',
description: 'GIF description',
}
)
])
})
dp.onInlineQuery(async (query) => {
query.answer([
BotInline.gif(
'RESULT_ID',
'https://media.tenor.com/videos/98bf1db10cb172aae086b09ae88ebf22/mp4',
{
title: 'GIF title',
description: 'GIF description',
}
)
])
})
Video
You can send an MP4 video as an inline result.
When sending by direct URL, there's a file size limit of 20 MB, and you must provide a thumbnail, otherwise the result will be ignored. Thumbnail is only used until the video file is cached by Telegram, and is then overridden by Telegram-generated video thumbnail.
Video result example
dp.onInlineQuery(async (query) => {
query.answer([
BotInline.video(
'RESULT_ID',
'https://amvnews.ru/index.php?go=Files&file=down&id=1858&alt=4',
{
thumb:
'https://amvnews.ru/images/news098/1257019986-Bad-Apple21_5.jpg',
title: 'Video title',
description: 'Video description',
}
)
])
})
dp.onInlineQuery(async (query) => {
query.answer([
BotInline.video(
'RESULT_ID',
'https://amvnews.ru/index.php?go=Files&file=down&id=1858&alt=4',
{
thumb:
'https://amvnews.ru/images/news098/1257019986-Bad-Apple21_5.jpg',
title: 'Video title',
description: 'Video description',
}
)
])
})
Alternatively, you can send a video by its URL (e.g. from YouTube) using isEmbed: true
:
Video page result example
dp.onInlineQuery(async (query) => {
query.answer([
BotInline.video(
'RESULT_ID',
'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
{
isEmbed: true,
thumb: 'https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg',
title: 'Video title',
description: 'Video description',
}
)
])
})
dp.onInlineQuery(async (query) => {
query.answer([
BotInline.video(
'RESULT_ID',
'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
{
isEmbed: true,
thumb: 'https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg',
title: 'Video title',
description: 'Video description',
}
)
])
})
Choosing such result would send a message containing URL to that video:
Audio
You can send an MPEG audio file as an inline result.
When sending by direct URL, there's a file size limit of 20 MB.
Audio result example
dp.onInlineQuery(async (query) => {
query.answer([
BotInline.audio(
'RESULT_ID',
'https://vk.com/mp3/cc_ice_melts.mp3',
{
performer: 'Griby',
title: 'Tayet Lyod',
}
)
])
})
dp.onInlineQuery(async (query) => {
query.answer([
BotInline.audio(
'RESULT_ID',
'https://vk.com/mp3/cc_ice_melts.mp3',
{
performer: 'Griby',
title: 'Tayet Lyod',
}
)
])
})
NOTE
Performer, title and other meta can't be changed once the file is cached by Telegram (they will still be displayed in the results, but not in the message). To avoid caching when sending by URL, add a random query parameter (e.g. ?notgcache
), which will make Telegram think this is a new file.
Voice
You can send an OGG file as a voice note inline result.
Waveform seems to only be generated for OGG files encoded with OPUS, so if it is not generated, try re-encoding your file with OPUS.
Voice result example
dp.onInlineQuery(async (query) => {
query.answer([
BotInline.voice(
'RESULT_ID',
'https://tei.su/test_voice.ogg',
{
title: 'Voice title',
}
)
])
})
dp.onInlineQuery(async (query) => {
query.answer([
BotInline.voice(
'RESULT_ID',
'https://tei.su/test_voice.ogg',
{
title: 'Voice title',
}
)
])
})
Photo
You can send an image as an inline result.
Photo result example
dp.onInlineQuery(async (query) => {
query.answer([
BotInline.photo(
'RESULT_ID',
'https://nyanpa.su/renge.jpg'
)
])
})
dp.onInlineQuery(async (query) => {
query.answer([
BotInline.photo(
'RESULT_ID',
'https://nyanpa.su/renge.jpg'
)
])
})
You can also add title and description, however only some clients display them (e.g. Telegram Desktop doesn't, screenshot below is from Telegram for Android)
Photo result with title
dp.onInlineQuery(async (query) => {
query.answer([
BotInline.photo(
'RESULT_ID',
'https://nyanpa.su/renge.jpg',
{
title: 'Photo title',
description: 'Photo description',
}
)
])
})
dp.onInlineQuery(async (query) => {
query.answer([
BotInline.photo(
'RESULT_ID',
'https://nyanpa.su/renge.jpg',
{
title: 'Photo title',
description: 'Photo description',
}
)
])
})
Sticker
You can send a sticker as an inline result. You can't send a sticker by URL (Telegram limitation), only by File ID.
Sticker result example
dp.onInlineQuery(async (query) => {
query.answer([
BotInline.sticker(
'RESULT_ID',
'CAACAgIAAxk...JtzysqiUK3IAQ'
)
])
})
dp.onInlineQuery(async (query) => {
query.answer([
BotInline.sticker(
'RESULT_ID',
'CAACAgIAAxk...JtzysqiUK3IAQ'
)
])
})
File
You can send a file as an inline result.
Due to Telegram limitations, when using URLs, you can only send PDF and ZIP files, and must set mime
accordingly (application/pdf
and application/zip
MIMEs respectively). With File IDs, you can send any file.
File result example
dp.onInlineQuery(async (query) => {
query.answer([
BotInline.file(
'RESULT_ID',
'https://file-examples-com.github.io/uploads/2017/10/file-sample_150kB.pdf',
{
mime: 'application/pdf',
title: 'File title',
description: 'File description'
}
)
])
})
dp.onInlineQuery(async (query) => {
query.answer([
BotInline.file(
'RESULT_ID',
'https://file-examples-com.github.io/uploads/2017/10/file-sample_150kB.pdf',
{
mime: 'application/pdf',
title: 'File title',
description: 'File description'
}
)
])
})
Geolocation
You can send a geolocation as an inline result.
By default, Telegram generates thumb
for result based on the location provided.
Geo result example
dp.onInlineQuery(async (query) => {
query.answer([
BotInline.geo(
'RESULT_ID',
{
latitude: 55.751999,
longitude: 37.617734,
title: 'Kremlin'
}
),
])
})
dp.onInlineQuery(async (query) => {
query.answer([
BotInline.geo(
'RESULT_ID',
{
latitude: 55.751999,
longitude: 37.617734,
title: 'Kremlin'
}
),
])
})
Venue
You can send a venue as an inline result.
By default, Telegram generates thumb
for result based on the location provided.
Venue result example
dp.onInlineQuery(async (query) => {
query.answer([
BotInline.venue(
'RESULT_ID',
{
latitude: 55.751999,
longitude: 37.617734,
title: 'Kremlin',
address: 'Red Square'
}
),
])
})
dp.onInlineQuery(async (query) => {
query.answer([
BotInline.venue(
'RESULT_ID',
{
latitude: 55.751999,
longitude: 37.617734,
title: 'Kremlin',
address: 'Red Square'
}
),
])
})
Contact
You can send a contact as an inline result.
Contact result example
dp.onInlineQuery(async (query) => {
query.answer([
BotInline.contact(
'RESULT_ID',
{
firstName: 'Alice',
phone: '+79001234567',
thumb: 'https://avatars.githubusercontent.com/u/86301490'
}
),
])
})
dp.onInlineQuery(async (query) => {
query.answer([
BotInline.contact(
'RESULT_ID',
{
firstName: 'Alice',
phone: '+79001234567',
thumb: 'https://avatars.githubusercontent.com/u/86301490'
}
),
])
})
Games
Finally, you can send a game as an inline result.
Game result example
dp.onInlineQuery(async (query) => {
query.answer([
BotInline.game('RESULT_ID', 'game_short_name'),
])
})
dp.onInlineQuery(async (query) => {
query.answer([
BotInline.game('RESULT_ID', 'game_short_name'),
])
})
Custom message
By default, mtcute generates a message based on result contents. However, you can override the message that will be sent using message
Text
Instead of media or default article message, you may want to send a custom text message:
dp.onInlineQuery(async (query) => {
query.answer([
BotInline.photo(
'RESULT_ID',
'https://nyanpa.su/renge.jpg',
{
message: BotInlineMessage.text(
'Ha-ha, just kidding. No Renge for you :p'
)
}
)
])
})
dp.onInlineQuery(async (query) => {
query.answer([
BotInline.photo(
'RESULT_ID',
'https://nyanpa.su/renge.jpg',
{
message: BotInlineMessage.text(
'Ha-ha, just kidding. No Renge for you :p'
)
}
)
])
})
Media
You can customize media message (for photos, videos, voices, documents, etc.) with custom caption, keyboard, etc:
dp.onInlineQuery(async (query) => {
query.answer([
BotInline.photo(
'RESULT_ID',
'https://nyanpa.su/renge.jpg',
{
message: BotInlineMessage.media({ text: 'Nyanpasu!' }),
}
)
])
})
dp.onInlineQuery(async (query) => {
query.answer([
BotInline.photo(
'RESULT_ID',
'https://nyanpa.su/renge.jpg',
{
message: BotInlineMessage.media({ text: 'Nyanpasu!' }),
}
)
])
})
Sadly, Telegram does not allow sending another media instead of the one you provided in the result (however, you could use web previews to accomplish similar results)
Geolocation
Instead of sending the default message, you can send geolocation, or even live geolocation:
dp.onInlineQuery(async (query) => {
query.answer([
BotInline.photo(
'RESULT_ID',
'https://nyanpa.su/renge.jpg',
{
// or BotInlineMessage.geoLive
message: BotInlineMessage.geo({
latitude: 55.751999,
longitude: 37.617734,
}),
}
)
])
})
dp.onInlineQuery(async (query) => {
query.answer([
BotInline.photo(
'RESULT_ID',
'https://nyanpa.su/renge.jpg',
{
// or BotInlineMessage.geoLive
message: BotInlineMessage.geo({
latitude: 55.751999,
longitude: 37.617734,
}),
}
)
])
})
Venue
Instead of sending the default message, you can send a venue
dp.onInlineQuery(async (query) => {
query.answer([
BotInline.photo(
'RESULT_ID',
'https://nyanpa.su/renge.jpg',
{
message: BotInlineMessage.venue({
latitude: 55.751999,
longitude: 37.617734,
title: 'Kremlin',
address: 'Red Square'
}),
}
)
])
})
dp.onInlineQuery(async (query) => {
query.answer([
BotInline.photo(
'RESULT_ID',
'https://nyanpa.su/renge.jpg',
{
message: BotInlineMessage.venue({
latitude: 55.751999,
longitude: 37.617734,
title: 'Kremlin',
address: 'Red Square'
}),
}
)
])
})
Contact
Instead of sending the default message, you can send a contact
dp.onInlineQuery(async (query) => {
query.answer([
BotInline.photo(
'RESULT_ID',
'https://nyanpa.su/renge.jpg',
{
message: BotInlineMessage.contact({
firstName: 'Alice',
phone: '+79001234567',
}),
}
)
])
})
dp.onInlineQuery(async (query) => {
query.answer([
BotInline.photo(
'RESULT_ID',
'https://nyanpa.su/renge.jpg',
{
message: BotInlineMessage.contact({
firstName: 'Alice',
phone: '+79001234567',
}),
}
)
])
})
Game
For inline results containing a game, you can customize keyboard under the message (learn more):
dp.onInlineQuery(async (query) => {
query.answer([
BotInline.game('RESULT_ID', 'test', {
message: BotInlineMessage.game({
replyMarkup: ...
})
}),
])
})
dp.onInlineQuery(async (query) => {
query.answer([
BotInline.game('RESULT_ID', 'test', {
message: BotInlineMessage.game({
replyMarkup: ...
})
}),
])
})
Switch to PM
Some bots may benefit from switching to PM with the bot for some action (e.g. logging in with your account). For that, you can use switchPm
button along with your results:
Switch to PM example
dp.onInlineQuery(async (query) => {
query.answer([], {
switchPm: {
text: 'Log in',
parameter: 'login_inline'
}
})
})
dp.onNewMessage(filters.deeplink('login_inline'), async (msg) => {
await msg.answerText('Thanks for logging in!')
})
dp.onInlineQuery(async (query) => {
query.answer([], {
switchPm: {
text: 'Log in',
parameter: 'login_inline'
}
})
})
dp.onNewMessage(filters.deeplink('login_inline'), async (msg) => {
await msg.answerText('Thanks for logging in!')
})
Chosen inline results
When a user selects an inline result, and assuming that you have inline feedback feature enabled, an update is received, which can be handled:
dp.onChosenInlineResult(async (result) => {
await result.editMessage({
text: `${result.user.displayName}, thanks for using inline!`
})
})
dp.onChosenInlineResult(async (result) => {
await result.editMessage({
text: `${result.user.displayName}, thanks for using inline!`
})
})
You can use filters.regex
to filter by chosen result ID:
dp.onChosenInlineResult(
filters.regex(/^CATS_/),
async (result) => {
// ...
}
)
dp.onChosenInlineResult(
filters.regex(/^CATS_/),
async (result) => {
// ...
}
)
These updates are only sent by Telegram when you have enabled inline feedback feature. You can enable it in @BotFather.
It is however noted by Telegram that this should only be used for statistical purposes, and even if probability setting is 100%, not all chosen inline results may be reported (source).
Editing inline message
You can edit an inline message in Chosen inline query update handlers, and in Callback query updates:
NOTE
In the below examples, it is assumed that callback query originates from an inline message.
dp.onChosenInlineResult(async (result) => {
await result.editMessage({...})
})
dp.onCallbackQuery(async (query) => {
await query.editMessage({...})
})
dp.onChosenInlineResult(async (result) => {
await result.editMessage({...})
})
dp.onCallbackQuery(async (query) => {
await query.editMessage({...})
})
You can also save inline message ID and edit the message later:
function updateMessageLater(msgId: string) {
setTimeout(() => {
tg.editInlineMessage(msgId, {...})
.catch(console.error)
}, 5000)
}
dp.onChosenInlineResult(async (result) => {
updateMessageLater(result.messageIdStr)
})
dp.onCallbackQuery(async (query) => {
updateMessageLater(query.inlineMessageIdStr)
})
function updateMessageLater(msgId: string) {
setTimeout(() => {
tg.editInlineMessage(msgId, {...})
.catch(console.error)
}, 5000)
}
dp.onChosenInlineResult(async (result) => {
updateMessageLater(result.messageIdStr)
})
dp.onCallbackQuery(async (query) => {
updateMessageLater(query.inlineMessageIdStr)
})
TIP
messageIdStr
and inlineMessageIdStr
contain string representation of the inline message ID, to simplify its storage.
You can also use the messageId
and inlineMessageId
respectively, which contain a TL object, in case you need to use Raw API. In mtcute, they are interchangeable.
You can also edit media inside inline messages, and even upload new media directly to them (unlike Bot API):
dp.onChosenInlineResult(async (result) => {
const link = await getDownloadLink(result.id)
await result.editMessage({
media: InputMedia.audio(await fetch(link))
})
})
dp.onChosenInlineResult(async (result) => {
const link = await getDownloadLink(result.id)
await result.editMessage({
media: InputMedia.audio(await fetch(link))
})
})