133 lines
4.6 KiB
Python
133 lines
4.6 KiB
Python
"""
|
||
艾叶 IM Bridge — WhatsApp 通道
|
||
对接方式:
|
||
1. WhatsApp Business API (Cloud API) — Webhook 回调
|
||
2. 可扩展对接 OpenClaw/Moltbot Gateway 的 WebSocket
|
||
|
||
当前实现:WhatsApp Cloud API Webhook 模式
|
||
|
||
配置项:
|
||
phone_number_id: WhatsApp Business 电话号码 ID
|
||
access_token: Meta Graph API 长期令牌
|
||
verify_token: Webhook 验证令牌(自定义字符串)
|
||
api_version: Graph API 版本(默认 v21.0)
|
||
|
||
Webhook: POST /webhook/whatsapp
|
||
验证: GET /webhook/whatsapp
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
from typing import Any
|
||
|
||
import httpx
|
||
from fastapi import Request, Response
|
||
|
||
from core.channel_base import ChannelBase, InboundMessage, MessageType, OutboundMessage
|
||
|
||
logger = logging.getLogger("aiye.channel.whatsapp")
|
||
|
||
|
||
class WhatsAppChannel(ChannelBase):
|
||
"""WhatsApp Cloud API 通道"""
|
||
|
||
@property
|
||
def platform(self) -> str:
|
||
return "whatsapp"
|
||
|
||
async def start(self) -> None:
|
||
logger.info("WhatsApp 通道已就绪,Webhook: /webhook/whatsapp")
|
||
|
||
async def stop(self) -> None:
|
||
logger.info("WhatsApp 通道已停止")
|
||
|
||
async def send(self, msg: OutboundMessage) -> bool:
|
||
phone_id = self._config.get("phone_number_id", "")
|
||
token = self._config.get("access_token", "")
|
||
api_ver = self._config.get("api_version", "v21.0")
|
||
if not phone_id or not token:
|
||
logger.warning("WhatsApp 未配置 phone_number_id / access_token")
|
||
return False
|
||
|
||
url = f"https://graph.facebook.com/{api_ver}/{phone_id}/messages"
|
||
payload = {
|
||
"messaging_product": "whatsapp",
|
||
"to": msg.chat_id,
|
||
"type": "text",
|
||
"text": {"body": msg.content},
|
||
}
|
||
try:
|
||
async with httpx.AsyncClient(timeout=15) as client:
|
||
resp = await client.post(
|
||
url,
|
||
json=payload,
|
||
headers={"Authorization": f"Bearer {token}"},
|
||
)
|
||
if resp.status_code == 200:
|
||
return True
|
||
logger.warning("WhatsApp 发送失败: %d %s", resp.status_code, resp.text[:200])
|
||
except Exception as e:
|
||
logger.error("WhatsApp 发送异常: %s", e)
|
||
return False
|
||
|
||
def register_routes(self, app: Any) -> None:
|
||
channel = self
|
||
|
||
@app.get("/webhook/whatsapp")
|
||
async def whatsapp_verify(
|
||
request: Request,
|
||
):
|
||
"""Meta Webhook 验证"""
|
||
params = request.query_params
|
||
mode = params.get("hub.mode", "")
|
||
token = params.get("hub.verify_token", "")
|
||
challenge = params.get("hub.challenge", "")
|
||
if mode == "subscribe" and token == channel._config.get("verify_token", ""):
|
||
return Response(content=challenge, media_type="text/plain")
|
||
return Response(content="forbidden", status_code=403)
|
||
|
||
@app.post("/webhook/whatsapp")
|
||
async def whatsapp_webhook(request: Request):
|
||
try:
|
||
data = await request.json()
|
||
except Exception:
|
||
return {"status": "error"}
|
||
|
||
entries = data.get("entry", [])
|
||
for entry in entries:
|
||
changes = entry.get("changes", [])
|
||
for change in changes:
|
||
value = change.get("value", {})
|
||
messages = value.get("messages", [])
|
||
contacts = value.get("contacts", [])
|
||
contact_map = {
|
||
c.get("wa_id", ""): c.get("profile", {}).get("name", "")
|
||
for c in contacts
|
||
}
|
||
|
||
for wa_msg in messages:
|
||
msg_type = wa_msg.get("type", "")
|
||
from_id = wa_msg.get("from", "")
|
||
text = ""
|
||
if msg_type == "text":
|
||
text = wa_msg.get("text", {}).get("body", "")
|
||
|
||
if not text.strip():
|
||
continue
|
||
|
||
inbound = InboundMessage(
|
||
channel_id=channel.channel_id,
|
||
platform=channel.platform,
|
||
sender_id=from_id,
|
||
sender_name=contact_map.get(from_id, ""),
|
||
chat_id=from_id,
|
||
content=text,
|
||
raw=wa_msg,
|
||
)
|
||
|
||
reply = await channel.dispatch(inbound)
|
||
if reply:
|
||
await channel.send(reply)
|
||
|
||
return {"status": "ok"}
|