import re import logging from datetime import datetime, UTC from yuxi.channel.extensions.slack.constants import SLACK_MAX_BUTTONS, SLACK_MAX_SELECT_OPTIONS logger = logging.getLogger(__name__) class SlackInteractive: SLACK_BUTTONS_PATTERN = re.compile(r"\[\[slack_buttons:\s*(.+?)\]\]", re.DOTALL) SLACK_SELECT_PATTERN = re.compile(r"\[\[slack_select:\s*(.+?)\]\]", re.DOTALL) SLACK_DATEPICKER_PATTERN = re.compile(r"\[\[slack_datepicker:\s*(.+?)\]\]", re.DOTALL) SLACK_USERS_PATTERN = re.compile(r"\[\[slack_users:\s*(.+?)\]\]", re.DOTALL) SLACK_CHANNELS_PATTERN = re.compile(r"\[\[slack_channels:\s*(.+?)\]\]", re.DOTALL) SLACK_RADIO_PATTERN = re.compile(r"\[\[slack_radio:\s*(.+?)\]\]", re.DOTALL) SLACK_CHECKBOX_PATTERN = re.compile(r"\[\[slack_checkbox:\s*(.+?)\]\]", re.DOTALL) def enable_interactive_replies(self, config: dict, account_id: str | None = None) -> bool: return config.get("channels", {}).get("slack", {}).get("interactiveReplies", False) def parse_buttons(self, text: str) -> list[dict] | None: match = self.SLACK_BUTTONS_PATTERN.search(text) if not match: return None raw = match.group(1).strip() buttons = [] for item in raw.split(","): item = item.strip() if not item: continue parts = item.split(":") if len(parts) >= 2: label = parts[0].strip() value = parts[1].strip() style = parts[2].strip() if len(parts) > 2 else "default" buttons.append({"text": label, "value": value, "style": style}) if len(buttons) >= SLACK_MAX_BUTTONS: break return buttons if buttons else None def parse_select(self, text: str) -> dict | None: match = self.SLACK_SELECT_PATTERN.search(text) if not match: return None raw = match.group(1).strip() parts = raw.split("|", 1) placeholder = "选择一个选项" options_str = parts[0].strip() if len(parts) == 2: placeholder = parts[0].strip() options_str = parts[1].strip() options = [] for item in options_str.split(","): item = item.strip() if not item: continue kv = item.split(":", 1) if len(kv) == 2: options.append({"text": kv[0].strip(), "value": kv[1].strip()}) if len(options) >= SLACK_MAX_SELECT_OPTIONS: break if not options: return None return {"placeholder": placeholder, "options": options} def detect_auto_select(self, text: str) -> dict | None: lines = text.strip().split("\n") last_line = lines[-1].strip() if lines else "" if last_line.lower().startswith("options:"): options_text = last_line[len("options:") :].strip() options = [] for item in options_text.split(","): item = item.strip() if not item: continue kv = item.split(":", 1) if len(kv) == 2: options.append({"text": kv[0].strip(), "value": kv[1].strip()}) if not options: return None if len(options) <= SLACK_MAX_BUTTONS: buttons = [{"text": o["text"], "value": o["value"], "style": "default"} for o in options] return {"type": "buttons", "buttons": buttons} return {"type": "select", "placeholder": "选择一个选项", "options": options} return None def build_block_kit_buttons(self, buttons: list[dict]) -> list[dict]: block_elements = [] for btn in buttons: style = btn.get("style", "default") element = { "type": "button", "text": {"type": "plain_text", "text": btn["text"]}, "value": btn["value"], "action_id": f"slack_btn_{btn['value'][:20]}", } if style in ("primary", "danger"): element["style"] = style block_elements.append(element) return [{"type": "actions", "elements": block_elements}] def build_block_kit_select(self, select_data: dict) -> list[dict]: options = [] for opt in select_data["options"]: options.append( { "text": {"type": "plain_text", "text": opt["text"]}, "value": opt["value"], } ) return [ { "type": "section", "accessory": { "type": "static_select", "placeholder": {"type": "plain_text", "text": select_data["placeholder"]}, "options": options, "action_id": "slack_select_default", }, } ] def build_datepicker(self, action_id: str, placeholder: str = "选择日期") -> dict: return { "type": "section", "accessory": { "type": "datepicker", "initial_date": datetime.now().strftime("%Y-%m-%d"), "placeholder": {"type": "plain_text", "text": placeholder}, "action_id": action_id, }, } def build_overflow_menu(self, options: list[dict], action_id: str) -> dict: return { "type": "actions", "elements": [ { "type": "overflow", "options": [ {"text": {"type": "plain_text", "text": o["text"]}, "value": o["value"]} for o in options[:5] ], "action_id": action_id, } ], } def build_users_select(self, action_id: str, placeholder: str = "选择用户") -> dict: return { "type": "section", "accessory": { "type": "users_select", "placeholder": {"type": "plain_text", "text": placeholder}, "action_id": action_id, }, } def build_conversations_select(self, action_id: str, placeholder: str = "选择对话") -> dict: return { "type": "section", "accessory": { "type": "conversations_select", "placeholder": {"type": "plain_text", "text": placeholder}, "action_id": action_id, }, } def build_channels_select(self, action_id: str, placeholder: str = "选择频道") -> dict: return { "type": "section", "accessory": { "type": "channels_select", "placeholder": {"type": "plain_text", "text": placeholder}, "action_id": action_id, }, } def build_radio_buttons(self, options: list[dict], action_id: str) -> dict: return { "type": "section", "accessory": { "type": "radio_buttons", "options": [{"text": {"type": "plain_text", "text": o["text"]}, "value": o["value"]} for o in options], "action_id": action_id, }, } def build_checkboxes(self, options: list[dict], action_id: str) -> dict: return { "type": "section", "accessory": { "type": "checkboxes", "options": [{"text": {"type": "plain_text", "text": o["text"]}, "value": o["value"]} for o in options], "action_id": action_id, }, } def build_timepicker(self, action_id: str, placeholder: str = "选择时间", initial_time: str | None = None) -> dict: element = { "type": "section", "accessory": { "type": "timepicker", "placeholder": {"type": "plain_text", "text": placeholder}, "action_id": action_id, }, } if initial_time: element["accessory"]["initial_time"] = initial_time return element def build_datetimepicker(self, action_id: str, placeholder: str = "选择日期时间") -> dict: from datetime import datetime now = datetime.now(UTC) return { "type": "section", "accessory": { "type": "datetimepicker", "initial_date_time": int(now.timestamp()), "action_id": action_id, }, } def build_plain_text_input( self, action_id: str, placeholder: str = "输入文本", multiline: bool = False, max_length: int | None = None ) -> dict: element = { "type": "input", "element": { "type": "plain_text_input", "action_id": action_id, "placeholder": {"type": "plain_text", "text": placeholder}, "multiline": multiline, }, "label": {"type": "plain_text", "text": placeholder}, } if max_length: element["element"]["max_length"] = max_length return element def build_number_input( self, action_id: str, placeholder: str = "输入数字", is_decimal_allowed: bool = False ) -> dict: return { "type": "input", "element": { "type": "number_input", "is_decimal_allowed": is_decimal_allowed, "action_id": action_id, "placeholder": {"type": "plain_text", "text": placeholder}, }, "label": {"type": "plain_text", "text": placeholder}, } def build_multi_static_select( self, options: list[dict], action_id: str, placeholder: str = "选择多个选项", max_selected_items: int | None = None, ) -> dict: element = { "type": "section", "accessory": { "type": "multi_static_select", "placeholder": {"type": "plain_text", "text": placeholder}, "options": [{"text": {"type": "plain_text", "text": o["text"]}, "value": o["value"]} for o in options], "action_id": action_id, }, } if max_selected_items: element["accessory"]["max_selected_items"] = max_selected_items return element def build_multi_users_select( self, action_id: str, placeholder: str = "选择多个用户", max_selected_items: int | None = None ) -> dict: element = { "type": "section", "accessory": { "type": "multi_users_select", "placeholder": {"type": "plain_text", "text": placeholder}, "action_id": action_id, }, } if max_selected_items: element["accessory"]["max_selected_items"] = max_selected_items return element def build_multi_channels_select( self, action_id: str, placeholder: str = "选择多个频道", max_selected_items: int | None = None ) -> dict: element = { "type": "section", "accessory": { "type": "multi_channels_select", "placeholder": {"type": "plain_text", "text": placeholder}, "action_id": action_id, }, } if max_selected_items: element["accessory"]["max_selected_items"] = max_selected_items return element def build_external_select(self, action_id: str, placeholder: str = "搜索选项", min_query_length: int = 3) -> dict: return { "type": "section", "accessory": { "type": "external_select", "placeholder": {"type": "plain_text", "text": placeholder}, "action_id": action_id, "min_query_length": min_query_length, }, } def parse_datepicker(self, text: str) -> dict | None: match = self.SLACK_DATEPICKER_PATTERN.search(text) if not match: return None placeholder = match.group(1).strip() return {"placeholder": placeholder or "选择日期", "action_id": "slack_datepicker_default"} def parse_users_select(self, text: str) -> dict | None: match = self.SLACK_USERS_PATTERN.search(text) if not match: return None placeholder = match.group(1).strip() return {"placeholder": placeholder or "选择用户", "action_id": "slack_users_default"} def parse_channels_select(self, text: str) -> dict | None: match = self.SLACK_CHANNELS_PATTERN.search(text) if not match: return None placeholder = match.group(1).strip() return {"placeholder": placeholder or "选择频道", "action_id": "slack_channels_default"} def parse_radio_buttons(self, text: str) -> list[dict] | None: match = self.SLACK_RADIO_PATTERN.search(text) if not match: return None raw = match.group(1).strip() options = [] for item in raw.split(","): item = item.strip() if not item: continue parts = item.split(":", 1) if len(parts) == 2: options.append({"text": parts[0].strip(), "value": parts[1].strip()}) return options if options else None def parse_checkboxes(self, text: str) -> list[dict] | None: match = self.SLACK_CHECKBOX_PATTERN.search(text) if not match: return None raw = match.group(1).strip() options = [] for item in raw.split(","): item = item.strip() if not item: continue parts = item.split(":", 1) if len(parts) == 2: options.append({"text": parts[0].strip(), "value": parts[1].strip()}) return options if options else None