Keyboards
You probably already know what a keyboard is, and if you don't, check the Bots documentation by Telegram.
Sending a keyboard
When developing bots, a common feature that many developers use is sending custom keyboards to their users - be it inline or reply.
In both cases, this is done by providing replyMarkup
parameter when using sendText
or similar methods. It accepts plain JavaScript object, but you can also use builder functions from BotKeyboard
namespace.
In mtcute, buttons are represented as a two-dimensional array.
Reply keyboards
Reply keyboard is a keyboard that is shown under user's writebar. When user taps on some button, a message is sent containing this button's text.
Example of a reply keyboard
await tg.sendText('username', 'Awesome keyboard!', {
replyMarkup: BotKeyboard.reply([
[BotKeyboard.text('First button')],
[BotKeyboard.text('Second button')],
])
})
await tg.sendText('username', 'Awesome keyboard!', {
replyMarkup: BotKeyboard.reply([
[BotKeyboard.text('First button')],
[BotKeyboard.text('Second button')],
])
})
You can only use the following button types with reply keyboards:
Name | Type | Notes |
---|---|---|
Text-only | BotKeyboard.text | |
Request contact | BotKeyboard.requestContact | only for private chats. |
Request geolocation | BotKeyboard.requestGeo | only for private chats. |
Request poll | BotKeyboard.requestPoll | only for private chats. |
Using any other will result in an error by Telegram.
You can also instruct the client to hide a previously sent reply keyboard:
await tg.sendText('username', 'No more keyboard :p', {
replyMarkup: BotKeyboard.hideReply()
})
await tg.sendText('username', 'No more keyboard :p', {
replyMarkup: BotKeyboard.hideReply()
})
Or, ask the user to reply to this message with custom text:
await tg.sendText('username', 'What is your name?', {
replyMarkup: BotKeyboard.forceReply()
})
await tg.sendText('username', 'What is your name?', {
replyMarkup: BotKeyboard.forceReply()
})
Inline keyboards
Inline keyboard is a keyboard that is shown under the message. When user taps on some button, a client does some action that particular button instructs it to do.
Example of an inline keyboard
await tg.sendText('username', 'Awesome keyboard!', {
replyMarkup: BotKeyboard.inline([
[BotKeyboard.callback('First button', 'btn:1')],
[BotKeyboard.callback('Second button', 'btn:2')],
])
})
await tg.sendText('username', 'Awesome keyboard!', {
replyMarkup: BotKeyboard.inline([
[BotKeyboard.callback('First button', 'btn:1')],
[BotKeyboard.callback('Second button', 'btn:2')],
])
})
You can only use the following button types with inline keyboards:
Name | Type | Notes |
---|---|---|
Callback | BotKeyboard.callback | When clicked a callback query will be sent to the bot. |
URL | BotKeyboard.url | When clicked the client will open the given URL. |
Switch inline | BotKeyboard.switchInline | When clicked the client will open an inline query to this bot with the given query. |
"Play game" | BotKeyboard.game | Must be the first one, must be used with InputMedia.game as the media. |
"Pay" | BotKeyboard.pay | Must be the first one, must be used with InputMedia.invoice as the media. |
Seamless login | BotKeyboard.urlAuth | Learn more |
WebView | BotKeyboard.webView | Learn more |
Open user | BotKeyboard.userProfile | When clicked the client will open the given user's profile |
Request peer | BotKeyboard.requestPeer | When clicked the client will ask the user to choose a peer and will send a message with ActionPeerChosen |
Using any other will result in an error by Telegram.
Keyboard builder
Sometimes 2D array is a bit too low-level, and thus mtcute provides an easy-to-use builder for the keyboards.
Once created using BotKeyboard.builder()
, you can push
buttons there, and then get it either asInline
or asReply
:
const markup = BotKeyboard.builder()
.push(BotKeyboard.text('Button 1'))
.push(BotKeyboard.text('Button 2'))
.asReply()
// Result:
// [ Button 1 ]
// [ Button 2 ]
const markup = BotKeyboard.builder()
.push(BotKeyboard.text('Button 1'))
.push(BotKeyboard.text('Button 2'))
.asReply()
// Result:
// [ Button 1 ]
// [ Button 2 ]
You can also push a button conditionally, or even use a function:
const markup = BotKeyboard.builder()
.push(BotKeyboard.text('Button 1'))
.push(isAdmin && BotKeyboard.text('Button 2'))
.push(() => BotKeyboard.text('Button 3'))
.asReply()
// Result:
// [ Button 1 ]
// [ Button 2 ] (only if admin)
// [ Button 3 ]
const markup = BotKeyboard.builder()
.push(BotKeyboard.text('Button 1'))
.push(isAdmin && BotKeyboard.text('Button 2'))
.push(() => BotKeyboard.text('Button 3'))
.asReply()
// Result:
// [ Button 1 ]
// [ Button 2 ] (only if admin)
// [ Button 3 ]
When push
-ing multiple buttons at once, they will be wrapped after a certain number of buttons added (default: 3):
const markup = BotKeyboard.builder()
.push(
BotKeyboard.text('Button 1'),
BotKeyboard.text('Button 2'),
BotKeyboard.text('Button 3'),
BotKeyboard.text('Button 4'),
)
.asReply()
// Result:
// [ Button 1 ] [ Button 2 ] [ Button 3 ]
// [ Button 4 ]
const markup = BotKeyboard.builder()
.push(
BotKeyboard.text('Button 1'),
BotKeyboard.text('Button 2'),
BotKeyboard.text('Button 3'),
BotKeyboard.text('Button 4'),
)
.asReply()
// Result:
// [ Button 1 ] [ Button 2 ] [ Button 3 ]
// [ Button 4 ]
Or, you can add entire rows at once without them getting wrapped (and even populate them from a function!):
const markup = BotKeyboard.builder()
.row(
BotKeyboard.text('1'),
BotKeyboard.text('2'),
BotKeyboard.text('3'),
BotKeyboard.text('4'),
)
.row((row) => {
for (let i = 5; i <= 8; i++ ) {
row.push(BotKeyboard.text(`${i}`))
}
})
.asReply()
// Result:
// [ 1 ] [ 2 ] [ 3 ] [ 4 ]
// [ 5 ] [ 6 ] [ 7 ] [ 8 ]
const markup = BotKeyboard.builder()
.row(
BotKeyboard.text('1'),
BotKeyboard.text('2'),
BotKeyboard.text('3'),
BotKeyboard.text('4'),
)
.row((row) => {
for (let i = 5; i <= 8; i++ ) {
row.push(BotKeyboard.text(`${i}`))
}
})
.asReply()
// Result:
// [ 1 ] [ 2 ] [ 3 ] [ 4 ]
// [ 5 ] [ 6 ] [ 7 ] [ 8 ]
Callback data builders
Writing, parsing and checking callback data manually gets tiring quite fast. Luckily, mtcute provides a tool that does the heavy stuff for you, called Callback data builder.
Creating a builder
Consider a simple bot that has some posts to display to user, and the user can switch between them using inline buttons.
First, let's declare a builder for the button:
const PostButton = new CallbackDataBuilder('post', 'id', 'action')
const PostButton = new CallbackDataBuilder('post', 'id', 'action')
Here, post
is the prefix, which will be prepended to all callback data strings generated by this builder to disambiguate. Make sure to use something unique!
id
and action
are fields which will be parsed/serialized to the callback data string in that particular order. Only include important stuff there, since callback data is limited to 64 characters!
TIP
Callback data builders are meant to be static, so it is best to declare them in a separate file and import from other files.
Creating buttons
Now that we have the builder, we can use .build
method to add buttons to the messages:
await msg.answerText('...', {
replyMarkup: BotKeyboard.inline([
[
BotKeyboard.callback(
'Post title',
PostButton.build({ id: 1, action: 'view' })
)
]
])
})
await msg.answerText('...', {
replyMarkup: BotKeyboard.inline([
[
BotKeyboard.callback(
'Post title',
PostButton.build({ id: 1, action: 'view' })
)
]
])
})
The above code will produce the following callback data in that button:
post:1:view
post:1:view
Handling clicks
Our button is currently rather useless, since we haven't registered a handler for it just yet. We can use .filter
method of our builder to create a filter to suit our needs:
dp.onCallbackQuery(PostButton.filter({ action: 'view' }), async (upd) => {
const post = await getPostById(upd.match.id)
if (!post) {
await upd.answer({ text: 'Not found!' })
return
}
await upd.editMessage({
text: post.text
})
})
dp.onCallbackQuery(PostButton.filter({ action: 'view' }), async (upd) => {
const post = await getPostById(upd.match.id)
if (!post) {
await upd.answer({ text: 'Not found!' })
return
}
await upd.editMessage({
text: post.text
})
})
.filter
not only handles parsing and checking, but also provides .match
extension field that contains the parsed data, and you can use it inside your code.
Using a keyboard
When using mtcute as a client, you may want to use some keyboard that was attached to some message.
dp.onNewMessage(async (msg: Message) => {
const markup = msg.markup
switch (markup.type) {
// see below
}
})
dp.onNewMessage(async (msg: Message) => {
const markup = msg.markup
switch (markup.type) {
// see below
}
})
If type is hide_reply
, there is (obviously) nothing to do except to hide the current reply keyboard from the UI (if applicable).
If type is force_reply
, just send a message in reply to this message:
await msg.replyText('Some text')
await msg.replyText('Some text')
If type is reply
or inline
, then there are some buttons available. You can find the one you need, and then act accordingly:
const buttons = markup.buttons
const buttonINeed = BotKeyboard.findButton(buttons, 'Button text')
switch (buttonINeed._) {
// see below
}
const buttons = markup.buttons
const buttonINeed = BotKeyboard.findButton(buttons, 'Button text')
switch (buttonINeed._) {
// see below
}
buttonINeed
will be a plain TL object of type KeyboardButton.
Emulating a click
See Telegram docs on this topic.