from __future__ import annotations import re from typing import Any _MRKDWN_SPECIAL_CHARS = re.compile(r"([*_~`<])") def escape_mrkdwn(text: str) -> str: return _MRKDWN_SPECIAL_CHARS.sub(r"\\\1", text) def build_section(text: str, *, accessory: dict | None = None) -> dict[str, Any]: block: dict[str, Any] = { "type": "section", "text": {"type": "mrkdwn", "text": text}, } if accessory: block["accessory"] = accessory return block def build_divider() -> dict[str, Any]: return {"type": "divider"} def build_button(text: str, action_id: str, *, value: str = "", style: str = "") -> dict[str, Any]: btn: dict[str, Any] = { "type": "button", "text": {"type": "plain_text", "text": text}, "action_id": action_id, } if value: btn["value"] = value if style in ("primary", "danger"): btn["style"] = style return btn def build_actions(*elements: dict[str, Any]) -> dict[str, Any]: return { "type": "actions", "elements": list(elements), } def build_approval_blocks( title: str, detail: str, confirm_action_id: str = "exec_approve", reject_action_id: str = "exec_reject", confirm_text: str = "确认执行", reject_text: str = "取消", ) -> list[dict[str, Any]]: return [ { "type": "header", "text": {"type": "plain_text", "text": title}, }, { "type": "section", "text": {"type": "mrkdwn", "text": detail}, }, {"type": "divider"}, { "type": "actions", "elements": [ { "type": "button", "text": {"type": "plain_text", "text": confirm_text}, "style": "primary", "action_id": confirm_action_id, }, { "type": "button", "text": {"type": "plain_text", "text": reject_text}, "style": "danger", "action_id": reject_action_id, }, ], }, ] def build_poll_blocks( question: str, options: list[str], poll_id: str, ) -> list[dict[str, Any]]: blocks: list[dict[str, Any]] = [ { "type": "header", "text": {"type": "plain_text", "text": "📊 投票"}, }, { "type": "section", "text": {"type": "mrkdwn", "text": f"*{escape_mrkdwn(question)}*"}, }, {"type": "divider"}, ] for i, option in enumerate(options): blocks.append( { "type": "section", "text": {"type": "mrkdwn", "text": escape_mrkdwn(option)}, "accessory": { "type": "button", "text": {"type": "plain_text", "text": "投票"}, "action_id": f"poll_{poll_id}_{i}", "value": str(i), }, } ) return blocks def build_error_notice(message: str) -> list[dict[str, Any]]: return [ { "type": "section", "text": {"type": "mrkdwn", "text": f":warning: *错误*\n>{escape_mrkdwn(message)}"}, }, { "type": "context", "elements": [ {"type": "mrkdwn", "text": "如需帮助,请联系管理员。"}, ], }, ] def build_header(text: str, *, emoji: bool = True) -> dict[str, Any]: title = text if emoji and not any(ord(c) > 127 for c in text): title = f"📌 {text}" return { "type": "header", "text": {"type": "plain_text", "text": title}, } def build_context(*elements: dict[str, Any]) -> dict[str, Any]: return { "type": "context", "elements": list(elements), } def build_image_block(image_url: str, alt_text: str, *, title: str = "") -> dict[str, Any]: block: dict[str, Any] = { "type": "image", "image_url": image_url, "alt_text": alt_text, } if title: block["title"] = {"type": "plain_text", "text": title} return block def build_option(text: str, value: str, *, description: str = "", url: str = "") -> dict[str, Any]: option: dict[str, Any] = { "text": {"type": "plain_text", "text": text}, "value": value, } if description: option["description"] = {"type": "plain_text", "text": description} if url: option["url"] = url return option def build_option_group(label: str, *options: dict[str, Any]) -> dict[str, Any]: return { "label": {"type": "plain_text", "text": label}, "options": list(options), } def build_static_select( action_id: str, *options: dict[str, Any], placeholder: str = "选择一个选项", option_groups: list[dict[str, Any]] | None = None, ) -> dict[str, Any]: select: dict[str, Any] = { "type": "static_select", "action_id": action_id, "placeholder": {"type": "plain_text", "text": placeholder}, } if option_groups: select["option_groups"] = option_groups else: select["options"] = list(options) return select def build_users_select( action_id: str, *, placeholder: str = "选择用户", initial_user: str = "", ) -> dict[str, Any]: select: dict[str, Any] = { "type": "users_select", "action_id": action_id, "placeholder": {"type": "plain_text", "text": placeholder}, } if initial_user: select["initial_user"] = initial_user return select def build_external_select( action_id: str, *, placeholder: str = "搜索...", min_query_length: int = 1, initial_option: dict[str, Any] | None = None, ) -> dict[str, Any]: select: dict[str, Any] = { "type": "external_select", "action_id": action_id, "placeholder": {"type": "plain_text", "text": placeholder}, "min_query_length": min_query_length, } if initial_option: select["initial_option"] = initial_option return select def build_conversations_select( action_id: str, *, placeholder: str = "选择频道", ) -> dict[str, Any]: return { "type": "conversations_select", "action_id": action_id, "placeholder": {"type": "plain_text", "text": placeholder}, } def build_input( label: str, element: dict[str, Any], *, block_id: str = "", hint: str = "", optional: bool = False, dispatch_action: bool = False, ) -> dict[str, Any]: block: dict[str, Any] = { "type": "input", "label": {"type": "plain_text", "text": label}, "element": element, } if block_id: block["block_id"] = block_id if hint: block["hint"] = {"type": "plain_text", "text": hint} block["optional"] = optional if dispatch_action: block["dispatch_action"] = True return block # ========== Input Elements ========== def build_datepicker( action_id: str, *, placeholder: str = "选择日期", initial_date: str = "", ) -> dict[str, Any]: element: dict[str, Any] = { "type": "datepicker", "action_id": action_id, "placeholder": {"type": "plain_text", "text": placeholder}, } if initial_date: element["initial_date"] = initial_date return element def build_checkboxes( action_id: str, options: list[dict[str, Any]], *, initial_options: list[dict[str, Any]] | None = None, ) -> dict[str, Any]: element: dict[str, Any] = { "type": "checkboxes", "action_id": action_id, "options": options, } if initial_options: element["initial_options"] = initial_options return element def build_radio_buttons( action_id: str, options: list[dict[str, Any]], *, initial_option: dict[str, Any] | None = None, ) -> dict[str, Any]: element: dict[str, Any] = { "type": "radio_buttons", "action_id": action_id, "options": options, } if initial_option: element["initial_option"] = initial_option return element def build_overflow_menu( action_id: str, options: list[dict[str, Any]], ) -> dict[str, Any]: return { "type": "overflow", "action_id": action_id, "options": options, } # ========== Multi-Select Elements ========== def build_multi_static_select( action_id: str, options: list[dict[str, Any]], *, placeholder: str = "选择选项", initial_options: list[dict[str, Any]] | None = None, max_selected_items: int | None = None, ) -> dict[str, Any]: element: dict[str, Any] = { "type": "multi_static_select", "action_id": action_id, "options": options, "placeholder": {"type": "plain_text", "text": placeholder}, } if initial_options: element["initial_options"] = initial_options if max_selected_items is not None: element["max_selected_items"] = max_selected_items return element def build_multi_users_select( action_id: str, *, placeholder: str = "选择用户", initial_users: list[str] | None = None, max_selected_items: int | None = None, ) -> dict[str, Any]: element: dict[str, Any] = { "type": "multi_users_select", "action_id": action_id, "placeholder": {"type": "plain_text", "text": placeholder}, } if initial_users: element["initial_users"] = initial_users if max_selected_items is not None: element["max_selected_items"] = max_selected_items return element def build_multi_external_select( action_id: str, *, placeholder: str = "搜索...", initial_options: list[dict[str, Any]] | None = None, min_query_length: int = 1, max_selected_items: int | None = None, ) -> dict[str, Any]: element: dict[str, Any] = { "type": "multi_external_select", "action_id": action_id, "placeholder": {"type": "plain_text", "text": placeholder}, "min_query_length": min_query_length, } if initial_options: element["initial_options"] = initial_options if max_selected_items is not None: element["max_selected_items"] = max_selected_items return element def build_multi_conversations_select( action_id: str, *, placeholder: str = "选择频道", initial_conversations: list[str] | None = None, max_selected_items: int | None = None, filter_include: list[str] | None = None, ) -> dict[str, Any]: element: dict[str, Any] = { "type": "multi_conversations_select", "action_id": action_id, "placeholder": {"type": "plain_text", "text": placeholder}, } if initial_conversations: element["initial_conversations"] = initial_conversations if max_selected_items is not None: element["max_selected_items"] = max_selected_items if filter_include: element["filter"] = {"include": filter_include} return element # ========== More Input Elements ========== def build_timepicker( action_id: str, *, placeholder: str = "选择时间", initial_time: str = "", ) -> dict[str, Any]: element: dict[str, Any] = { "type": "timepicker", "action_id": action_id, "placeholder": {"type": "plain_text", "text": placeholder}, } if initial_time: element["initial_time"] = initial_time return element def build_plain_text_input( action_id: str, *, placeholder: str = "请输入...", initial_value: str = "", multiline: bool = False, min_length: int | None = None, max_length: int | None = None, ) -> dict[str, Any]: element: dict[str, Any] = { "type": "plain_text_input", "action_id": action_id, "placeholder": {"type": "plain_text", "text": placeholder}, } if initial_value: element["initial_value"] = initial_value if multiline: element["multiline"] = True if min_length is not None: element["min_length"] = min_length if max_length is not None: element["max_length"] = max_length return element def build_url_text_input( action_id: str, *, placeholder: str = "请输入 URL...", initial_value: str = "", ) -> dict[str, Any]: element: dict[str, Any] = { "type": "url_text_input", "action_id": action_id, "placeholder": {"type": "plain_text", "text": placeholder}, } if initial_value: element["initial_value"] = initial_value return element def build_email_text_input( action_id: str, *, placeholder: str = "请输入邮箱...", initial_value: str = "", ) -> dict[str, Any]: element: dict[str, Any] = { "type": "email_text_input", "action_id": action_id, "placeholder": {"type": "plain_text", "text": placeholder}, } if initial_value: element["initial_value"] = initial_value return element def build_number_input( action_id: str, *, placeholder: str = "请输入数字...", initial_value: str = "", min_value: str | None = None, max_value: str | None = None, is_decimal_allowed: bool = False, ) -> dict[str, Any]: element: dict[str, Any] = { "type": "number_input", "action_id": action_id, "placeholder": {"type": "plain_text", "text": placeholder}, } if initial_value: element["initial_value"] = initial_value if min_value is not None: element["min_value"] = min_value if max_value is not None: element["max_value"] = max_value if is_decimal_allowed: element["is_decimal_allowed"] = True return element def build_file_input( action_id: str, *, filetypes: list[str] | None = None, max_files: int | None = None, ) -> dict[str, Any]: element: dict[str, Any] = { "type": "file_input", "action_id": action_id, } if filetypes: element["filetypes"] = filetypes if max_files is not None: element["max_files"] = max_files return element # ========== Rich Text Block ========== def build_rich_text(elements: list[dict[str, Any]]) -> dict[str, Any]: return { "type": "rich_text", "elements": elements, } def build_rich_text_section(elements: list[dict[str, Any]]) -> dict[str, Any]: return { "type": "rich_text_section", "elements": elements, } def build_rich_text_list( style: str, elements: list[dict[str, Any]], *, indent: int = 0, border: int = 0, ) -> dict[str, Any]: element: dict[str, Any] = { "type": "rich_text_list", "style": style, "elements": elements, "indent": indent, } if border: element["border"] = border return element