Skip to content

SOURCE CODE tgintegration.containers.reply_keyboard DOCS

"""

"""
import re
from typing import List
from typing import Pattern
from typing import TYPE_CHECKING
from typing import Union

from pyrogram import filters as f
from pyrogram.filters import Filter
from pyrogram.types import KeyboardButton
from pyrogram.types import Message

from tgintegration.containers import NoButtonFound

if TYPE_CHECKING:
    from tgintegration.botcontroller import BotController
    from tgintegration.containers.responses import Response


class ReplyKeyboard:DOCS
    """
    Represents a regular keyboard in the Telegram UI and allows to click buttons in the menu.

    See Also:
        [InlineKeyboard](tgintegration.InlineKeyboard)
    """

    def __init__(
        self,
        controller: "BotController",
        chat_id: Union[int, str],
        message_id: int,
        button_rows: List[List[KeyboardButton]],
    ):
        self._controller: BotController = controller
        self._message_id = message_id
        self._peer_id = chat_id
        self.rows = button_rows

    def find_button(self, pattern: Pattern) -> KeyboardButton:DOCS
        """
        Attempts to retrieve a clickable button anywhere in the underlying `rows` by matching the button captions with
        the given `pattern`. If no button could be found, **this method raises** `NoButtonFound`.

        Args:
            pattern: The button caption to look for (by `re.match`).

        Returns:
            The `KeyboardButton` if found.
        """
        compiled = re.compile(pattern)
        for row in self.rows:
            for button in row:
                # TODO: Investigate why sometimes it's a button and other times a string
                if compiled.match(button.text if hasattr(button, "text") else button):
                    return button
        raise NoButtonFound(f"No clickable entity found for pattern r'{pattern}'")

    async def _click_nowait(self, pattern, quote=False) -> Message:
        button = self.find_button(pattern)

        return await self._controller.client.send_message(
            self._peer_id,
            button.text,
            reply_to_message_id=self._message_id if quote else None,
        )

    @propertyDOCS
    def num_buttons(self) -> int:
        """
        Returns the total number of buttons in all underlying rows.
        """
        return sum(len(row) for row in self.rows)

    async def click(DOCS
        self, pattern: Pattern, filters: Filter = None, quote: bool = False
    ) -> "Response":
        """
        Uses `find_button` with the given `pattern`, clicks the button if found, and waits for the bot to react. For
        a `ReplyKeyboard`, this means that a message with the button's caption will be sent to the same chat.

        If not button could be found, `NoButtonFound` will be raised.

        Args:
            pattern: The button caption to look for (by `re.match`).
            filters: Additional filters to be given to `collect`. Will be merged with a "same chat" filter and
                `filters.text | filters.edited`.
            quote: Whether to reply to the message containing the buttons.

        Returns:
            The bot's `Response`.
        """
        button = self.find_button(pattern)

        filters = (
            filters & f.chat(self._peer_id) if filters else f.chat(self._peer_id)
        ) & (f.text | f.edited)

        async with self._controller.collect(filters=filters) as res:  # type: Response
            await self._controller.client.send_message(
                self._controller.peer,
                button.text if hasattr(button, "text") else button,
                reply_to_message_id=self._message_id if quote else None,
            )

        return res