Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: link in markup #463

Merged
merged 1 commit into from
Apr 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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)"
)
Loading