Skip to content

Commit

Permalink
feat: link in markup
Browse files Browse the repository at this point in the history
  • Loading branch information
kutuzov13 committed Apr 12, 2024
1 parent 6b27924 commit f6e4a67
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 3 deletions.
9 changes: 9 additions & 0 deletions README.md
Expand Up @@ -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,
Expand Down
61 changes: 59 additions & 2 deletions pybotx/models/message/markup.py
Expand Up @@ -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
Expand All @@ -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]
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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)

Expand Down Expand Up @@ -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):
Expand All @@ -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,
Expand All @@ -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,
),
)

Expand Down
125 changes: 125 additions & 0 deletions tests/client/notifications_api/test_markup.py
Expand Up @@ -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")
Expand Down
2 changes: 1 addition & 1 deletion tests/models/test_markup.py
Expand Up @@ -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)"
)

0 comments on commit f6e4a67

Please sign in to comment.