ForcePilot/backend/package/yuxi/channel/extensions/whatsapp/monitor.py
Kris 7215418610 feat(channel): 添加企业微信、微博、WhatsApp 和 Workplace 渠道扩展
新增企业微信、微博、WhatsApp、Workplace 四个渠道扩展。

企业微信渠道扩展主要模块:config, gateway, webhook, webhook_bot, outbound, streaming, pairing, security, crypto, dedupe, persistent_dedupe, card, directory, events, externalcontact, media, mentions, menu, message, oauth, status

微博渠道扩展主要模块:config, gateway, webhook, outbound, streaming, pairing, security, dedupe, passive_reply, broadcast, message, menu, media, subscription, template, status

WhatsApp 渠道扩展主要模块:config, gateway, webhook, outbound, streaming, pairing, security, dedupe, actions, monitor, status

Workplace 渠道扩展主要模块:config, gateway, webhook, outbound, streaming, pairing, security, dedupe, actions, challenge, groups, media, mentions, menu, monitor, persona, quick_reply, signature, subscriptions, template, threading, users, status
2026-05-21 12:01:56 +08:00

208 lines
7.3 KiB
Python

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,
},
}