from __future__ import annotations import shlex from yuxi.channel.extensions.line.actions import create_message_action, create_uri_action, create_postback_action from yuxi.channel.extensions.line.template_messages import ( create_buttons_template, create_confirm_template, create_image_carousel_template, create_image_carousel_column, create_yes_no_confirm, create_button_menu, create_link_menu, create_product_carousel, to_template_message, ) from yuxi.channel.extensions.line.flex_templates import ( create_receipt_card, create_event_card, create_agenda_card, create_media_player_card, create_device_control_card, create_apple_tv_remote, ) LINE_CARD_COMMANDS: dict[str, dict] = { "info": { "syntax": '/card info "Title" "Body" ["Footer"]', "description": "信息卡片(标题+正文+可选页脚)", }, "image": { "syntax": '/card image "Title" "Caption" --url ', "description": "图片卡片", }, "action": { "syntax": '/card action "Title" "Body" --actions "Btn1|url1,Btn2|/cmd"', "description": "操作卡片(自动识别 URL/命令/postback)", }, "list": { "syntax": '/card list "Title" "Item1|Desc1,Item2|Desc2"', "description": "列表卡片", }, "receipt": { "syntax": '/card receipt "Title" "Item1:$10,Item2:$20" --total "$30"', "description": "收据/摘要卡片", }, "confirm": { "syntax": '/card confirm "Question?" --yes "Yes|data" --no "No|data"', "description": "确认对话框", }, "buttons": { "syntax": '/card buttons "Title" "Text" --actions "Btn1|url,Btn2|data"', "description": "按钮模板", }, } def parse_card_command(text: str) -> dict | None: if not text.startswith("/card "): return None args_str = text[6:].strip() try: parts = shlex.split(args_str) except ValueError: return None if not parts: return None card_type = parts[0].lower() if card_type not in LINE_CARD_COMMANDS: return None positional: list[str] = [] flags: dict[str, str] = {} current_flag: str | None = None for item in parts[1:]: if item.startswith("--"): current_flag = item[2:] flags[current_flag] = "" elif current_flag: flags[current_flag] = item current_flag = None else: positional.append(item) return { "type": card_type, "positional": positional, "flags": flags, } def build_card_message(parsed: dict) -> dict | None: card_type = parsed["type"] positional = parsed.get("positional", []) flags = parsed.get("flags", {}) match card_type: case "info": title = positional[0] if len(positional) > 0 else "Info" body = positional[1] if len(positional) > 1 else "" footer = positional[2] if len(positional) > 2 else None return create_info_card(title, body, footer) case "image": title = positional[0] if len(positional) > 0 else "Image" caption = positional[1] if len(positional) > 1 else "" url = flags.get("url", "") return create_image_card(title, caption, url) case "action": title = positional[0] if len(positional) > 0 else "Actions" body = positional[1] if len(positional) > 1 else "" actions_str = flags.get("actions", "") return create_action_card(title, body, actions_str) case "list": title = positional[0] if len(positional) > 0 else "List" items_str = positional[1] if len(positional) > 1 else "" return create_list_card(title, items_str) case "receipt": title = positional[0] if len(positional) > 0 else "Receipt" items_str = positional[1] if len(positional) > 1 else "" total = flags.get("total") footer = flags.get("footer") return create_receipt_card_command(title, items_str, total, footer) case "confirm": question = positional[0] if positional else "Are you sure?" yes_data = flags.get("yes", "Yes|yes") no_data = flags.get("no", "No|no") return create_confirm_card(question, yes_data, no_data) case "buttons": title = positional[0] if len(positional) > 0 else "Menu" text = positional[1] if len(positional) > 1 else "" actions_str = flags.get("actions", "") return create_buttons_card(title, text, actions_str) case _: return None def create_info_card(title: str, body: str, footer: str | None = None) -> dict: contents = { "type": "bubble", "body": { "type": "box", "layout": "vertical", "contents": [ {"type": "text", "text": title, "weight": "bold", "size": "xl", "wrap": True}, {"type": "text", "text": body, "size": "md", "wrap": True, "margin": "md"}, ], }, } if footer: contents["footer"] = { "type": "box", "layout": "vertical", "contents": [ {"type": "text", "text": footer, "size": "xs", "color": "#AAAAAA", "wrap": True}, ], } return {"type": "flex", "altText": title[:400], "contents": contents} def create_image_card(title: str, caption: str, url: str) -> dict: return { "type": "flex", "altText": title[:400], "contents": { "type": "bubble", "hero": { "type": "image", "url": url, "size": "full", "aspectRatio": "16:9", "aspectMode": "cover", }, "body": { "type": "box", "layout": "vertical", "contents": [ {"type": "text", "text": title, "weight": "bold", "size": "lg", "wrap": True}, {"type": "text", "text": caption, "size": "sm", "wrap": True, "color": "#888888", "margin": "md"}, ], }, }, } def create_action_card(title: str, body: str, actions_str: str) -> dict: actions = parse_actions(actions_str) return create_buttons_template( title=title, text=body, actions=actions, ) def create_list_card(title: str, items_str: str) -> dict: items: list[tuple[str, str]] = [] for item in items_str.split(","): parts = item.strip().split("|", 1) label = parts[0].strip() desc = parts[1].strip() if len(parts) > 1 else "" items.append((label, desc)) return create_receipt_card(title, items) def create_receipt_card_command(title: str, items_str: str, total: str | None, footer: str | None) -> dict: items: list[tuple[str, str]] = [] for item in items_str.split(","): parts = item.strip().split(":", 1) label = parts[0].strip() value = parts[1].strip() if len(parts) > 1 else "" items.append((label, value)) return create_receipt_card(title, items, total=total, footer=footer) def create_confirm_card(question: str, yes_data: str, no_data: str) -> dict: yes_label, yes_d = _split_label_data(yes_data, "Yes", "yes") no_label, no_d = _split_label_data(no_data, "No", "no") action_type = _detect_action_type(yes_d) confirm_action = _build_action(action_type, yes_label, yes_d) cancel_action = _build_action(_detect_action_type(no_d), no_label, no_d) template = create_confirm_template(question, confirm_action, cancel_action) return to_template_message(question, template) def create_buttons_card(title: str, text: str, actions_str: str) -> dict: actions = parse_actions(actions_str) template = create_buttons_template(title=title, text=text, actions=actions) return to_template_message(title, template) def parse_actions(actions_str: str) -> list[dict]: actions: list[dict] = [] if not actions_str: return actions for item in actions_str.split(","): parts = item.strip().split("|", 1) label = parts[0].strip() data = parts[1].strip() if len(parts) > 1 else label action_type = _detect_action_type(data) actions.append(_build_action(action_type, label, data)) return actions def _split_label_data(raw: str, default_label: str, default_data: str) -> tuple[str, str]: if "|" in raw: parts = raw.split("|", 1) return parts[0].strip(), parts[1].strip() return default_label, raw if raw else default_data def _detect_action_type(data: str) -> str: if data.startswith(("http://", "https://")): return "uri" if "=" in data and ":" not in data: return "postback" return "message" def _build_action(action_type: str, label: str, data: str) -> dict: match action_type: case "uri": return {"type": "uri", "label": label[:20], "uri": data} case "postback": return {"type": "postback", "label": label[:20], "data": data} case _: return {"type": "message", "label": label[:20], "text": data} def create_image_carousel_column( image_url: str, action_type: str = "uri", label: str = "", data: str = "", ) -> dict: return { "imageUrl": image_url, "action": _build_action(action_type, label, data), }