from __future__ import annotations import json import logging from datetime import datetime, UTC from yuxi.channel.extensions.whatsapp.types import WhatsAppAccount, WebhookMessage logger = logging.getLogger(__name__) def parse_webhook_to_unified(payload: dict, account: WhatsAppAccount) -> list[dict]: results: list[dict] = [] try: entries = payload.get("entry", []) for entry in entries: for change in entry.get("changes", []): value = change.get("value", {}) metadata = value.get("metadata", {}) contacts = value.get("contacts", []) raw_messages = value.get("messages", []) for raw_msg in raw_messages: wm = _extract_message(raw_msg, metadata, contacts) unified = _build_unified_message(wm, account, metadata) if unified: results.append(unified) except Exception: logger.exception(f"Failed to parse WhatsApp webhook payload: {json.dumps(payload, indent=2)[:500]}") return results def parse_statuses(payload: dict, account: WhatsAppAccount) -> list[dict]: results: list[dict] = [] try: entries = payload.get("entry", []) for entry in entries: for change in entry.get("changes", []): value = change.get("value", {}) raw_statuses = value.get("statuses", []) for s in raw_statuses: results.append({ "msg_id": s.get("id", ""), "recipient_id": s.get("recipient_id", ""), "status": s.get("status", ""), "timestamp": s.get("timestamp", ""), "conversation": s.get("conversation", {}), "pricing": s.get("pricing", {}), "errors": s.get("errors", []), "account_id": account.account_id, }) except Exception: logger.exception("Failed to parse WhatsApp statuses") return results def _extract_message(raw: dict, metadata: dict, contacts: list[dict]) -> WebhookMessage: msg_type = raw.get("type", "text") contact_name = "" sender_id = raw.get("from", "") for c in contacts: if c.get("wa_id") == sender_id: contact_name = c.get("profile", {}).get("name", "") break body = "" media_id = None mime_type = None filename = None reply_to_id = None match msg_type: case "text": body = raw.get("text", {}).get("body", "") case "image": media = raw.get("image", {}) body = media.get("caption", "") or "[图片]" media_id = media.get("id") mime_type = media.get("mime_type", "") case "video": media = raw.get("video", {}) body = media.get("caption", "") or "[视频]" media_id = media.get("id") mime_type = media.get("mime_type", "") case "audio" | "voice": media = raw.get("audio", {}) or raw.get("voice", {}) body = "[语音消息]" media_id = media.get("id") mime_type = media.get("mime_type", "") case "document": media = raw.get("document", {}) filename = media.get("filename", "unknown") body = f"[文件] {filename}" media_id = media.get("id") mime_type = media.get("mime_type", "") case "sticker": media = raw.get("sticker", {}) body = "[贴纸]" media_id = media.get("id") mime_type = media.get("mime_type", "") case "location": loc = raw.get("location", {}) body = f"[位置] {loc.get('latitude')},{loc.get('longitude')}" case "contacts": contacts_list = raw.get("contacts", []) names = [c.get("name", {}).get("formatted_name", "") for c in contacts_list] body = f"[联系人] {', '.join(names)}" if names else "[联系人]" case "reaction": reaction = raw.get("reaction", {}) body = f"[Reaction] {reaction.get('emoji', '')}" case "button": button = raw.get("button", {}) body = f"[按钮] {button.get('text', '')}" case "interactive": interactive = raw.get("interactive", {}) if interactive.get("type") == "button_reply": body = interactive.get("button_reply", {}).get("title", "[交互式回复]") elif interactive.get("type") == "list_reply": body = interactive.get("list_reply", {}).get("title", "[列表回复]") else: body = "[交互式消息]" case "order": body = "[购物车订单]" case "system": sys_data = raw.get("system", {}) sys_type = sys_data.get("type", "") if sys_type == "user_identity_changed": new_wa_id = sys_data.get("new_wa_id", "") body = f"[系统通知] 用户换号: {sender_id} → {new_wa_id}" elif sys_type == "user_changed_number": body = f"[系统通知] 用户号码变更" else: body = f"[系统通知] {sys_type}" context = raw.get("context", {}) reply_to_id = context.get("id") return WebhookMessage( msg_id=raw.get("id", ""), from_=raw.get("from", ""), timestamp=int(raw.get("timestamp", 0)), msg_type=msg_type, body=body, media_id=media_id, mime_type=mime_type, filename=filename, mentioned_ids=raw.get("text", {}).get("mentioned_ids", []) if msg_type == "text" else [], reply_to_id=reply_to_id, contact_name=contact_name, group_id=raw.get("group_id"), referral=raw.get("referral"), ) def _build_unified_message(wm: WebhookMessage, account: WhatsAppAccount, metadata: dict) -> dict: message_type_map = { "text": "TEXT", "image": "IMAGE", "video": "FILE", "audio": "VOICE", "voice": "VOICE", "document": "FILE", "sticker": "IMAGE", "location": "TEXT", "contacts": "TEXT", "reaction": "EVENT", "button": "EVENT", "interactive": "EVENT", "order": "EVENT", "system": "EVENT", } is_group = ( "g.us" in wm.from_ if wm.from_ else False ) timestamp = None if wm.timestamp: timestamp = datetime.fromtimestamp(wm.timestamp, tz=UTC) return { "msg_id": wm.msg_id, "channel_type": "whatsapp", "account_id": account.account_id, "content": wm.body, "message_type": message_type_map.get(wm.msg_type, "TEXT"), "media_urls": ([wm.media_id] if wm.media_id else []), "sender": { "id": wm.from_, "display_name": wm.contact_name, "kind": "GROUP" if is_group else "DIRECT", }, "group": {"id": wm.group_id or wm.from_} if is_group else None, "timestamp": timestamp, "mentioned_ids": wm.mentioned_ids, "reply_to_id": wm.reply_to_id, "raw_payload": { "msg_type": wm.msg_type, "media_id": wm.media_id, "phone_number_id": metadata.get("phone_number_id", ""), "referral": wm.referral, }, }