from __future__ import annotations import re DIRECTIVE_PATTERN = re.compile(r"\[\[(\w+):\s*(.*?)\]\]") def parse_line_directives(text: str) -> tuple[str, dict]: directives: dict = {} cleaned = text for match in DIRECTIVE_PATTERN.finditer(text): directive_name = match.group(1) args_str = match.group(2) directive_data = _parse_directive_args(directive_name, args_str) if directive_data: directives.setdefault(directive_name, []).append(directive_data) cleaned = cleaned.replace(match.group(0), "") cleaned = re.sub(r"\n{3,}", "\n\n", cleaned).strip() return cleaned, directives def _parse_directive_args(name: str, args_str: str) -> dict | None: match name: case "quick_replies": items = [item.strip() for item in args_str.split(",") if item.strip()] return {"type": "quick_replies", "items": items[:13]} if items else None case "location": parts = [p.strip() for p in args_str.split("|")] if len(parts) >= 4: try: return { "type": "location", "title": parts[0], "address": parts[1], "latitude": float(parts[2]), "longitude": float(parts[3]), } except ValueError: return None return None case "confirm": parts = [p.strip() for p in args_str.split("|")] if len(parts) >= 2: return { "type": "confirm", "question": parts[0], "yes_label": _extract_label(parts[1]) if len(parts) > 1 else "Yes", "yes_data": _extract_data(parts[1]) if len(parts) > 1 else "yes", "no_label": _extract_label(parts[2]) if len(parts) > 2 else "No", "no_data": _extract_data(parts[2]) if len(parts) > 2 else "no", } return None case "buttons": parts = [p.strip() for p in args_str.split("|")] if len(parts) >= 3: title = parts[0] description = parts[1] button_items = [b.strip() for b in parts[2].split(",")] buttons = [] for btn in button_items: btn_parts = btn.split(":", 1) btn_label = btn_parts[0].strip() btn_data = btn_parts[1].strip() if len(btn_parts) > 1 else btn_label action_type = _detect_action_type(btn_data) buttons.append({"label": btn_label, "data": btn_data, "action_type": action_type}) return { "type": "buttons", "title": title, "description": description, "buttons": buttons[:4], } return None case "media_player": parts = [p.strip() for p in args_str.split("|")] if len(parts) >= 4: return { "type": "media_player", "song": parts[0], "artist": parts[1], "source": parts[2], "image_url": parts[3], "state": parts[4] if len(parts) > 4 else "playing", } return None case "event": parts = [p.strip() for p in args_str.split("|")] if len(parts) >= 3: return { "type": "event", "title": parts[0], "date": parts[1], "time": parts[2], "location": parts[3] if len(parts) > 3 else "", "description": parts[4] if len(parts) > 4 else "", } return None case "agenda": parts = [p.strip() for p in args_str.split("|")] if len(parts) >= 2: events = [] for evt in parts[1].split(","): evt_parts = evt.strip().split(":", 1) event_name = evt_parts[0].strip() event_time = evt_parts[1].strip() if len(evt_parts) > 1 else "" events.append({"name": event_name, "time": event_time}) return { "type": "agenda", "title": parts[0], "events": events, } return None case "device": parts = [p.strip() for p in args_str.split("|")] if len(parts) >= 3: name = parts[0] device_type = parts[1] state = parts[2] controls = [] if len(parts) > 3: for ctrl in parts[3].split(","): ctrl_parts = ctrl.strip().split(":", 1) ctrl_label = ctrl_parts[0].strip() ctrl_data = ctrl_parts[1].strip() if len(ctrl_parts) > 1 else ctrl_label action_type = _detect_action_type(ctrl_data) controls.append({"label": ctrl_label, "data": ctrl_data, "action_type": action_type}) return { "type": "device", "name": name, "device_type": device_type, "state": state, "controls": controls[:6], } return None case "appletv_remote": parts = [p.strip() for p in args_str.split("|")] return { "type": "appletv_remote", "name": parts[0] if parts else "Apple TV", "state": parts[1] if len(parts) > 1 else "idle", } case _: return None def _extract_label(item: str) -> str: if ":" in item: return item.split(":", 1)[0] return item def _extract_data(item: str) -> str: if ":" in item: return item.split(":", 1)[1] return item 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_line_messages_from_directives(directives: dict) -> list[dict]: messages: list[dict] = [] for directive_type, items in directives.items(): for item in items: message = _build_directive_message(directive_type, item) if message: if isinstance(message, list): messages.extend(message) else: messages.append(message) return messages def _build_directive_message(directive_type: str, data: dict) -> dict | list[dict] | None: match directive_type: case "quick_replies": quick_reply_items = _build_quick_reply_action_items(data.get("items", [])) return { "type": "text", "text": data.get("text", ""), "quickReply": {"items": quick_reply_items}, } case "location": return { "type": "location", "title": data.get("title", ""), "address": data.get("address", ""), "latitude": data.get("latitude", 0), "longitude": data.get("longitude", 0), } case "confirm": template = _build_confirm_template(data) return {"type": "template", "altText": data.get("question", "Confirm")[:400], "template": template} case "buttons": template = _build_buttons_template(data) return {"type": "template", "altText": data.get("title", "Menu")[:400], "template": template} case "media_player": from yuxi.channel.extensions.line.flex_templates import create_media_player_card return create_media_player_card( song=data.get("song", ""), artist=data.get("artist", ""), source=data.get("source", ""), image_url=data.get("image_url", ""), state=data.get("state", "playing"), ) case "event": from yuxi.channel.extensions.line.flex_templates import create_event_card return create_event_card( title=data.get("title", ""), date=data.get("date", ""), time=data.get("time", ""), location=data.get("location", ""), description=data.get("description", ""), ) case "agenda": from yuxi.channel.extensions.line.flex_templates import create_agenda_card return create_agenda_card( title=data.get("title", ""), events=data.get("events", []), ) case "device": from yuxi.channel.extensions.line.flex_templates import create_device_control_card return create_device_control_card( name=data.get("name", ""), device_type=data.get("device_type", ""), state=data.get("state", ""), controls=data.get("controls", []), ) case "appletv_remote": from yuxi.channel.extensions.line.flex_templates import create_apple_tv_remote return create_apple_tv_remote( name=data.get("name", "Apple TV"), state=data.get("state", "idle"), ) case _: return None def _build_quick_reply_action_items(items: list[str]) -> list[dict]: result: list[dict] = [] for label in items[:13]: result.append({ "type": "action", "action": {"type": "message", "label": label[:20], "text": label}, }) return result def _build_confirm_template(data: dict) -> dict: yes_label = data.get("yes_label", "Yes") yes_data = data.get("yes_data", "yes") no_label = data.get("no_label", "No") no_data = data.get("no_data", "no") return { "type": "confirm", "text": data.get("question", ""), "actions": [ {"type": "message", "label": yes_label[:20], "text": yes_data}, {"type": "message", "label": no_label[:20], "text": no_data}, ], } def _build_buttons_template(data: dict) -> dict: buttons = data.get("buttons", []) actions: list[dict] = [] for btn in buttons[:4]: action_type = btn.get("action_type", "message") if action_type == "uri": actions.append({"type": "uri", "label": btn["label"][:20], "uri": btn["data"]}) elif action_type == "postback": actions.append({"type": "postback", "label": btn["label"][:20], "data": btn["data"]}) else: actions.append({"type": "message", "label": btn["label"][:20], "text": btn["data"]}) return { "type": "buttons", "title": data.get("title", "")[:40], "text": data.get("description", "")[:160], "actions": actions, }