Keyboards

Buttons under a message. The guide is Commands and buttons.

Warning

Buttons are never encrypted, not even in encrypted chats: their text and their callback_data travel in clear, because the server has to know which button was pressed.

class Button(text, callback_data=None, url=None)

One button under a message: a callback button or a link button.

A callback button sends its callback_data back to the bot when pressed, as a CallbackQuery. A link button offers to open its url, and the phone shows the address before opening it. Make one with callback or link; the fields are also the constructor’s arguments, and exactly one of callback_data and url must be set.

Nothing is checked when a button is made. When its keyboard is sent, a broken rule raises ValidationError (with field set to "markup"):

  • text is not blank, has at most 64 characters (UTF-16 units) and no control characters. Spaces at both ends are trimmed when it is sent.

  • callback_data has 1 to 64 bytes in UTF-8 and no control characters, and no other button of the keyboard has the same.

  • url is an absolute http or https address with no user name or password, at most 1024 characters long.

text

The label on the button.

Type:

str

callback_data

The data a press sends back to the bot. None for a link button.

Type:

str | None

url

The address a link button opens. None for a callback button.

Type:

str | None

Example

>>> from pgpcbot import Button
>>> Button.callback("Yes", "vote:yes")
Button(text='Yes', callback_data='vote:yes', url=None)
>>> Button.link("Rules", "https://example.org/rules").to_json()
{'text': 'Rules', 'url': 'https://example.org/rules'}

Warning

Buttons are never encrypted, not even in an encrypted chat: the server has to know which button was pressed. Keep personal data out of text and callback_data. Use an ID that means something only to your bot (order:8812), not the content (order:Anna Rossi, 3 pizzas).

classmethod callback(text, data)

Makes a callback button, which sends data back to the bot when pressed.

The press reaches the handlers of Router.on_callback_query as a CallbackQuery whose data is this string.

Parameters:
  • text (str) – The label on the button.

  • data (str) – 1 to 64 bytes in UTF-8 that tell the bot which button it was, such as "vote:yes". Never encrypted.

Returns:

The new button.

Return type:

Button

Makes a link button, which offers to open url.

The phone shows the address before opening it.

Parameters:
  • text (str) – The label on the button.

  • url (str) – An absolute http or https address, up to 1024 characters, with no user name or password in it.

Returns:

The new button.

Return type:

Button

to_json()

Converts the button to the API’s JSON form.

Nothing is checked or trimmed here.

Returns:

A dict, {"text": ..., "callbackData": ...} or {"text": ..., "url": ...}.

Return type:

dict[str, str]

classmethod from_json(data)

Makes a button from the API’s JSON form, the reverse of to_json.

Nothing is checked. A callbackData or url that is not a string is left out.

Parameters:

data (Mapping[str, Any]) – One button as the API writes it, such as {"text": "Yes", "callbackData": "vote:yes"}.

Returns:

The button.

Return type:

Button

class InlineKeyboard(rows=<factory>)

The rows of buttons under a message.

Make one with build, one argument per row, or with a KeyboardBuilder. The constructor takes the rows as a tuple of tuples. Pass the keyboard as markup= when sending or editing a message (Message.reply, Bot.send_message, CallbackQuery.edit_message…). Wherever a keyboard is accepted, the API’s JSON form works too, as a dict. len(keyboard) is the number of rows.

Nothing is checked while the keyboard is built. When it is sent, a broken rule raises ValidationError (with field set to "markup") before anything leaves the process:

  • up to 20 rows, 8 buttons in a row and 100 buttons in all;

  • each button follows the rules listed in Button, and no two buttons have the same callback_data;

  • the whole keyboard is at most 8 KiB (8192 bytes) of JSON, measured the way the server measures it.

Empty rows are dropped, and a keyboard with no buttons is the same as no keyboard. Its buttons are never encrypted (see Button).

rows

The rows, top to bottom, each a tuple of buttons from left to right.

Type:

tuple[tuple[Button, …], …]

Example

>>> from pgpcbot import Button, InlineKeyboard
>>> keyboard = InlineKeyboard.build(
...     [Button.callback("Yes", "vote:yes"), Button.callback("No", "vote:no")],
...     [Button.link("Rules", "https://example.org/rules")],
... )
>>> len(keyboard)
2
>>> keyboard.find("vote:no").text
'No'

In a handler, the keyboard goes under the reply:

await message.reply("Do you agree?", markup=keyboard)
classmethod build(*rows)

Makes a keyboard from rows of buttons, one argument per row.

InlineKeyboard.build([yes, no], [rules]) puts yes and no side by side, and rules under them.

Parameters:

*rows (Iterable[Button]) – The rows, top to bottom. Each is a list (or any iterable) of buttons, from left to right.

Returns:

The keyboard.

Return type:

InlineKeyboard

classmethod from_json(data)

Makes a keyboard from the API’s JSON form, the reverse of to_json.

Nothing is checked. Rows that are not lists and buttons that are not objects are skipped.

Parameters:

data (Mapping[str, Any] | None) – {"inlineKeyboard": [[...], ...]}, as the API writes it, or None.

Returns:

The keyboard, or None when data is not a mapping with an inlineKeyboard list.

Return type:

InlineKeyboard | None

to_json()

Converts the keyboard to the API’s JSON form.

Nothing is checked or dropped here: the rules apply when the keyboard is sent.

Returns:

A dict, {"inlineKeyboard": [[...], ...]}, with each button as Button.to_json writes it.

Return type:

dict[str, Any]

buttons()

Lists every button of the keyboard.

Returns:

The buttons, row by row from the top, each row from left to right.

Return type:

list[Button]

find(callback_data)

Finds the button that carries callback_data.

Parameters:

callback_data (str) – The data to look for, such as the data of a CallbackQuery.

Returns:

The first button with that data, or None when no button has it.

Return type:

Button | None

class KeyboardBuilder(*, per_row=PER_ROW_MAX)

Builds a keyboard a row at a time, starting a new row when one is full.

add puts buttons in the current row and starts a new row after per_row buttons; row adds a row of its own. Both give the builder back, so calls can be chained, and build makes the InlineKeyboard. Nothing is checked until the keyboard is sent.

Example

>>> from pgpcbot import Button, KeyboardBuilder
>>> towns = ["Rome", "Milan", "Turin", "Bari"]
>>> keyboard = (
...     KeyboardBuilder(per_row=3)
...     .add(*[Button.callback(town, f"town:{town.lower()}") for town in towns])
...     .row(Button.link("All towns", "https://example.org/towns"))
...     .build()
... )
>>> [[button.text for button in row] for row in keyboard.rows]
[['Rome', 'Milan', 'Turin'], ['Bari'], ['All towns']]

Starts an empty keyboard.

Parameters:

per_row (int) – How many buttons add puts in a row before it starts the next one, from 1 to 8 (the server’s limit). The default is 8.

Raises:

ValueError – When per_row is not between 1 and 8.

add(*buttons)

Adds buttons to the current row, starting a new row when it is full.

A row is full when it has per_row buttons. After row, the next button starts a new row.

Parameters:

*buttons (Button) – The buttons, from left to right.

Returns:

This builder, so calls can be chained.

Return type:

KeyboardBuilder

row(*buttons)

Adds a row of its own with these buttons; the next add starts a new row.

per_row does not apply to this row, but the server’s limit of 8 buttons a row does. With no buttons, it only ends the current row.

Parameters:

*buttons (Button) – The buttons of the row, from left to right.

Returns:

This builder, so calls can be chained.

Return type:

KeyboardBuilder

build()

Makes the keyboard from the rows added so far. Empty rows are left out.

Returns:

The InlineKeyboard.

Return type:

InlineKeyboard