feat(keyboard): Add persistent and placeholder options

Introduce `persistent` and `placeholder` parameters for `ReplyKeyboardMarkup`.
These new options control keyboard visibility and input field text.
Available for text, location, phone, and poll buttons.

Remove vestigial `inline_only` from `build_reply_markup` as its functionality is
implicitly covered by the general rule against mixing inline and
normal buttons.
This commit is contained in:
Darskiy 2025-05-28 02:19:03 +03:00
parent 69e4493c04
commit 863365983d
2 changed files with 82 additions and 41 deletions

View File

@ -7,8 +7,8 @@ from ..tl import types, custom
class ButtonMethods: class ButtonMethods:
@staticmethod @staticmethod
def build_reply_markup( def build_reply_markup(
buttons: 'typing.Optional[hints.MarkupLike]', buttons: 'typing.Optional[hints.MarkupLike]'
inline_only: bool = False) -> 'typing.Optional[types.TypeReplyMarkup]': ) -> 'typing.Optional[types.TypeReplyMarkup]':
""" """
Builds a :tl:`ReplyInlineMarkup` or :tl:`ReplyKeyboardMarkup` for Builds a :tl:`ReplyInlineMarkup` or :tl:`ReplyKeyboardMarkup` for
the given buttons. the given buttons.
@ -26,9 +26,6 @@ class ButtonMethods:
The button, list of buttons, array of buttons or markup The button, list of buttons, array of buttons or markup
to convert into a markup. to convert into a markup.
inline_only (`bool`, optional):
Whether the buttons **must** be inline buttons only or not.
Example Example
.. code-block:: python .. code-block:: python
@ -42,8 +39,8 @@ class ButtonMethods:
return None return None
try: try:
if buttons.SUBCLASS_OF_ID == 0xe2e10ef2: if buttons.SUBCLASS_OF_ID == 0xe2e10ef2: # crc32(b'ReplyMarkup'):
return buttons # crc32(b'ReplyMarkup'): return buttons
except AttributeError: except AttributeError:
pass pass
@ -57,6 +54,8 @@ class ButtonMethods:
resize = None resize = None
single_use = None single_use = None
selective = None selective = None
persistent = None
placeholder = None
rows = [] rows = []
for row in buttons: for row in buttons:
@ -69,6 +68,10 @@ class ButtonMethods:
single_use = button.single_use single_use = button.single_use
if button.selective is not None: if button.selective is not None:
selective = button.selective selective = button.selective
if button.persistent is not None:
persistent = button.persistent
if button.placeholder is not None:
placeholder = button.placeholder
button = button.button button = button.button
elif isinstance(button, custom.MessageButton): elif isinstance(button, custom.MessageButton):
@ -78,19 +81,21 @@ class ButtonMethods:
is_inline |= inline is_inline |= inline
is_normal |= not inline is_normal |= not inline
if button.SUBCLASS_OF_ID == 0xbad74a3: if button.SUBCLASS_OF_ID == 0xbad74a3: # crc32(b'KeyboardButton')
# 0xbad74a3 == crc32(b'KeyboardButton')
current.append(button) current.append(button)
if current: if current:
rows.append(types.KeyboardButtonRow(current)) rows.append(types.KeyboardButtonRow(current))
if inline_only and is_normal: if is_inline and is_normal:
raise ValueError('You cannot use non-inline buttons here')
elif is_inline == is_normal and is_normal:
raise ValueError('You cannot mix inline with normal buttons') raise ValueError('You cannot mix inline with normal buttons')
elif is_inline: elif is_inline:
return types.ReplyInlineMarkup(rows) return types.ReplyInlineMarkup(rows)
# elif is_normal:
return types.ReplyKeyboardMarkup( return types.ReplyKeyboardMarkup(
rows, resize=resize, single_use=single_use, selective=selective) rows=rows,
resize=resize,
single_use=single_use,
selective=selective,
persistent=persistent,
placeholder=placeholder
)

View File

@ -37,11 +37,14 @@ class Button:
to 128 characters and add the ellipsis () character as to 128 characters and add the ellipsis () character as
the 129. the 129.
""" """
def __init__(self, button, *, resize, single_use, selective): def __init__(self, button, *, resize, single_use, selective,
persistent, placeholder):
self.button = button self.button = button
self.resize = resize self.resize = resize
self.single_use = single_use self.single_use = single_use
self.selective = selective self.selective = selective
self.persistent = persistent
self.placeholder = placeholder
@staticmethod @staticmethod
def _is_inline(button): def _is_inline(button):
@ -168,11 +171,15 @@ class Button:
) )
@classmethod @classmethod
def text(cls, text, *, resize=None, single_use=None, selective=None): def text(cls, text, *, resize=None, single_use=None, selective=None,
persistent=None, placeholder=None):
""" """
Creates a new keyboard button with the given text. Creates a new keyboard button with the given text.
Args: Args:
text (`str`):
The title of the button.
resize (`bool`): resize (`bool`):
If present, the entire keyboard will be reconfigured to If present, the entire keyboard will be reconfigured to
be resized and be smaller if there are not many buttons. be resized and be smaller if there are not many buttons.
@ -187,48 +194,77 @@ class Button:
users. It will target users that are @mentioned in the text users. It will target users that are @mentioned in the text
of the message or to the sender of the message you reply to. of the message or to the sender of the message you reply to.
persistent (`bool`):
If present, always show the keyboard when the regular keyboard
is hidden. Defaults to false, in which case the custom keyboard
can be hidden and revealed via the keyboard icon.
placeholder (`str`):
The placeholder to be shown in the input field when the keyboard is active;
1-64 characters
When the user clicks this button, a text message with the same text When the user clicks this button, a text message with the same text
as the button will be sent, and can be handled with `events.NewMessage as the button will be sent, and can be handled with `events.NewMessage
<telethon.events.newmessage.NewMessage>`. You cannot distinguish <telethon.events.newmessage.NewMessage>`. You cannot distinguish
between a button press and the user typing and sending exactly the between a button press and the user typing and sending exactly the
same text on their own. same text on their own.
""" """
return cls(types.KeyboardButton(text), return cls(
resize=resize, single_use=single_use, selective=selective) types.KeyboardButton(text),
resize=resize,
single_use=single_use,
selective=selective,
persistent=persistent,
placeholder=placeholder
)
@classmethod @classmethod
def request_location(cls, text, *, def request_location(cls, text, *, resize=None, single_use=None, selective=None,
resize=None, single_use=None, selective=None): persistent=None, placeholder=None):
""" """
Creates a new keyboard button to request the user's location on click. Creates a new keyboard button to request the user's location on click.
``resize``, ``single_use`` and ``selective`` are documented in `text`. ``resize``, ``single_use``, ``selective``, ``persistent`` and ``placeholder``
are documented in `text`.
When the user clicks this button, a confirmation box will be shown When the user clicks this button, a confirmation box will be shown
to the user asking whether they want to share their location with the to the user asking whether they want to share their location with the
bot, and if confirmed a message with geo media will be sent. bot, and if confirmed a message with geo media will be sent.
""" """
return cls(types.KeyboardButtonRequestGeoLocation(text), return cls(
resize=resize, single_use=single_use, selective=selective) types.KeyboardButtonRequestGeoLocation(text),
resize=resize,
single_use=single_use,
selective=selective,
persistent=persistent,
placeholder=placeholder
)
@classmethod @classmethod
def request_phone(cls, text, *, def request_phone(cls, text, *, resize=None, single_use=None,
resize=None, single_use=None, selective=None): selective=None, persistent=None, placeholder=None):
""" """
Creates a new keyboard button to request the user's phone on click. Creates a new keyboard button to request the user's phone on click.
``resize``, ``single_use`` and ``selective`` are documented in `text`. ``resize``, ``single_use``, ``selective``, ``persistent`` and ``placeholder``
are documented in `text`.
When the user clicks this button, a confirmation box will be shown When the user clicks this button, a confirmation box will be shown
to the user asking whether they want to share their phone with the to the user asking whether they want to share their phone with the
bot, and if confirmed a message with contact media will be sent. bot, and if confirmed a message with contact media will be sent.
""" """
return cls(types.KeyboardButtonRequestPhone(text), return cls(
resize=resize, single_use=single_use, selective=selective) types.KeyboardButtonRequestPhone(text),
resize=resize,
single_use=single_use,
selective=selective,
placeholder=placeholder,
persistent=persistent
)
@classmethod @classmethod
def request_poll(cls, text, *, force_quiz=False, def request_poll(cls, text, *, force_quiz=False, resize=None, single_use=None,
resize=None, single_use=None, selective=None): selective=None, persistent=None, placeholder=None):
""" """
Creates a new keyboard button to request the user to create a poll. Creates a new keyboard button to request the user to create a poll.
@ -240,13 +276,20 @@ class Button:
the votes cannot be retracted. Otherwise, users can vote and retract the votes cannot be retracted. Otherwise, users can vote and retract
the vote, and the pol might be multiple choice. the vote, and the pol might be multiple choice.
``resize``, ``single_use`` and ``selective`` are documented in `text`. ``resize``, ``single_use``, ``selective``, ``persistent`` and ``placeholder``
are documented in `text`.
When the user clicks this button, a screen letting the user create a When the user clicks this button, a screen letting the user create a
poll will be shown, and if they do create one, the poll will be sent. poll will be shown, and if they do create one, the poll will be sent.
""" """
return cls(types.KeyboardButtonRequestPoll(text, quiz=force_quiz), return cls(
resize=resize, single_use=single_use, selective=selective) types.KeyboardButtonRequestPoll(text, quiz=force_quiz),
resize=resize,
single_use=single_use,
selective=selective,
persistent=persistent,
placeholder=placeholder
)
@staticmethod @staticmethod
def clear(selective=None): def clear(selective=None):
@ -265,15 +308,8 @@ class Button:
Forces a reply to the message with this markup. If used, Forces a reply to the message with this markup. If used,
no other button should be present or it will be ignored. no other button should be present or it will be ignored.
``single_use`` and ``selective`` are as documented in `text`. ``single_use``, ``selective`` and ``placeholder`` are as documented in `text`.
Args:
placeholder (str):
text to show the user at typing place of message.
If the placeholder is too long, Telegram applications will
crop the text (for example, to 64 characters and adding an
ellipsis () character as the 65th).
""" """
return types.ReplyKeyboardForceReply( return types.ReplyKeyboardForceReply(
single_use=single_use, single_use=single_use,