From c4c5d6402fb34050478595530bab7241affc9c72 Mon Sep 17 00:00:00 2001 From: Yegor Kutuzov Date: Fri, 12 Apr 2024 10:30:41 +0300 Subject: [PATCH] feat: link in markup (#463) --- README.md | 9 ++ pybotx/models/message/markup.py | 61 ++++++++- tests/client/notifications_api/test_markup.py | 125 ++++++++++++++++++ tests/models/test_markup.py | 2 +- 4 files changed, 194 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 53a7b653..520310ff 100644 --- a/README.md +++ b/README.md @@ -390,6 +390,15 @@ async def bubbles_handler(message: IncomingMessage, bot: Bot) -> None: new_row=False, ) + # В кнопку можно добавит ссылку на ресурс, + # для этого нужно добавить url в аргумент `link`, а `command` оставить пустым, + # `alert` добавляется в окно подтверждения при переходе по ссылке. + bubbles.add_button( + label="Bubble with link", + alert="alert text", + link="https://example.com", + ) + await bot.answer_message( "The time has come to make a choice, Mr. Anderson:", bubbles=bubbles, diff --git a/pybotx/models/message/markup.py b/pybotx/models/message/markup.py index f3ab683c..644f5a10 100644 --- a/pybotx/models/message/markup.py +++ b/pybotx/models/message/markup.py @@ -14,8 +14,29 @@ class ButtonTextAlign(Enum): @dataclass class Button: - command: str + """ + Button object. + + :param label: Button name. + :param command: Button command (required if no `link` is undefined). + :param data: Button body that will be sent as command parameters + when the button is clicked. + :param text_color: Button text color. + :param background_color: Bubbles background color. + :param align (default CENTER): Text alignment left | center | right + :param silent: If true, then when the button is pressed + the message will not be sent to the chat, it will be sent in the background. + :param width_ratio: Horizontal button size. + :param alert: Button notification text. + :param process_on_client: Execute process on client. + :param link: URL to resource. + + :raises ValueError: If `command` is missing. + `command` is optional only if `link` is not undefined. + """ + label: str + command: Missing[str] = Undefined data: Dict[str, Any] = field(default_factory=dict) text_color: Missing[str] = Undefined background_color: Missing[str] = Undefined @@ -25,6 +46,11 @@ class Button: width_ratio: Missing[int] = Undefined alert: Missing[str] = Undefined process_on_client: Missing[bool] = Undefined + link: Missing[str] = Undefined + + def __post_init__(self) -> None: + if self.command is Undefined and self.link is Undefined: + raise ValueError("Either 'command' or 'link' must be provided") ButtonRow = List[Button] @@ -68,8 +94,8 @@ def add_built_button(self, button: Button, new_row: bool = True) -> None: def add_button( self, - command: str, label: str, + command: Missing[str] = Undefined, data: Optional[Dict[str, Any]] = None, text_color: Missing[str] = Undefined, background_color: Missing[str] = Undefined, @@ -78,8 +104,33 @@ def add_button( width_ratio: Missing[int] = Undefined, alert: Missing[str] = Undefined, process_on_client: Missing[bool] = Undefined, + link: Missing[str] = Undefined, new_row: bool = True, ) -> None: + """Add button. + + :param label: Button name. + :param command: Button command (required if no `link` is undefined). + :param data: Button body that will be sent as command parameters + when the button is clicked. + :param text_color: Button text color. + :param background_color: Bubbles background color. + :param align: Text alignment left | center | right + :param silent: If true, then when the button is pressed + the message will not be sent to the chat, it will be sent in the background. + :param width_ratio: Horizontal button size. + :param alert: Button notification text. + :param process_on_client: Execute process on client. + :param link: URL to resource. + :param new_row: Move the next button to a new row. + + :raises ValueError: If `command` is missing. + `command` is optional only if `link` is undefined. + """ + + if link is Undefined and command is Undefined: + raise ValueError("Command arg is required if link is undefined.") + button = Button( command=command, label=label, @@ -91,6 +142,7 @@ def add_button( width_ratio=width_ratio, alert=alert, process_on_client=process_on_client, + link=link, ) self.add_built_button(button, new_row=new_row) @@ -118,6 +170,7 @@ class BotXAPIButtonOptions(UnverifiedPayloadBaseModel): show_alert: Missing[Literal[True]] alert_text: Missing[str] handler: Missing[Literal["client"]] + link: Missing[str] class BotXAPIButton(UnverifiedPayloadBaseModel): @@ -140,6 +193,9 @@ def api_button_from_domain(button: Button) -> BotXAPIButton: if button.process_on_client: handler = "client" + if button.link is not Undefined: + handler = "client" + return BotXAPIButton( command=button.command, label=button.label, @@ -153,6 +209,7 @@ def api_button_from_domain(button: Button) -> BotXAPIButton: alert_text=button.alert, show_alert=show_alert, handler=handler, + link=button.link, ), ) diff --git a/tests/client/notifications_api/test_markup.py b/tests/client/notifications_api/test_markup.py index 25ea974e..eac8860c 100644 --- a/tests/client/notifications_api/test_markup.py +++ b/tests/client/notifications_api/test_markup.py @@ -374,6 +374,131 @@ async def test__markup__color_and_align( assert endpoint.called +async def test__markup__link( + respx_mock: MockRouter, + host: str, + bot_id: UUID, + bot_account: BotAccountWithSecret, +) -> None: + # - Arrange - + endpoint = respx_mock.post( + f"https://{host}/api/v4/botx/notifications/direct", + headers={"Authorization": "Bearer token", "Content-Type": "application/json"}, + json={ + "group_chat_id": "054af49e-5e18-4dca-ad73-4f96b6de63fa", + "notification": { + "body": "Buttons links:", + "bubble": [ + [ + { + "data": {}, + "label": "Open me", + "opts": { + "silent": True, + "align": "center", + "handler": "client", + "link": "https://example.com", + }, + }, + ], + ], + "keyboard": [ + [ + { + "data": {}, + "label": "Open me", + "opts": { + "silent": True, + "align": "center", + "handler": "client", + "link": "https://example.com", + }, + }, + ], + ], + "status": "ok", + }, + }, + ).mock( + return_value=httpx.Response( + HTTPStatus.ACCEPTED, + json={ + "status": "ok", + "result": {"sync_id": "21a9ec9e-f21f-4406-ac44-1a78d2ccf9e3"}, + }, + ), + ) + + bubbles = BubbleMarkup() + bubbles.add_button( + label="Open me", + silent=True, + link="https://example.com", + ) + + keyboard = KeyboardMarkup() + keyboard.add_button( + label="Open me", + silent=True, + link="https://example.com", + ) + + built_bot = Bot(collectors=[HandlerCollector()], bot_accounts=[bot_account]) + + # - Act - + async with lifespan_wrapper(built_bot) as bot: + task = asyncio.create_task( + bot.send_message( + body="Buttons links:", + bot_id=bot_id, + chat_id=UUID("054af49e-5e18-4dca-ad73-4f96b6de63fa"), + bubbles=bubbles, + keyboard=keyboard, + ), + ) + + await asyncio.sleep(0) # Return control to event loop + + await bot.set_raw_botx_method_result( + { + "status": "ok", + "sync_id": "21a9ec9e-f21f-4406-ac44-1a78d2ccf9e3", + "result": {}, + }, + verify_request=False, + ) + + # - Assert - + assert (await task) == UUID("21a9ec9e-f21f-4406-ac44-1a78d2ccf9e3") + assert endpoint.called + + +def test__markup__bubble_without_command_error_raised() -> None: + # - Arrange - + bubbles = BubbleMarkup() + + # - Act - + with pytest.raises(ValueError) as exc: + bubbles.add_button( + label="label", + silent=True, + ) + + # - Assert - + assert "Command arg is required" in str(exc.value) + + +def test__markup__built_button_without_command_error_raised2() -> None: + # - Arrange - + with pytest.raises(ValueError) as exc: + Button( + label="Bubble", + ) + + # - Assert - + assert "Either 'command' or 'link' must be provided" in str(exc.value) + + def test__markup__comparison() -> None: # - Arrange - button = Button("/test", "test") diff --git a/tests/models/test_markup.py b/tests/models/test_markup.py index 87ade162..8f8fb999 100644 --- a/tests/models/test_markup.py +++ b/tests/models/test_markup.py @@ -11,5 +11,5 @@ def test__mentions_list_properties__filled() -> None: # - Assert - assert ( bubbles.__repr__() - == "row 1: label1 (command1)\nrow 2: label2 (command2) | label3 (command3)" + == "row 1: command1 (label1)\nrow 2: command2 (label2) | command3 (label3)" )