新增企业微信、微博、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
208 lines
7.3 KiB
Python
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,
|
|
},
|
|
}
|