新增企业微信、微博、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
123 lines
4.6 KiB
Python
123 lines
4.6 KiB
Python
import asyncio
|
|
import time
|
|
import logging
|
|
|
|
from yuxi.channel.extensions.whatsapp.monitor import parse_webhook_to_unified, parse_statuses
|
|
from yuxi.channel.extensions.whatsapp.types import WhatsAppAccount
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class WhatsAppGateway:
|
|
|
|
def __init__(self, config_adapter, deduplicator):
|
|
self._config_adapter = config_adapter
|
|
self._deduplicator = deduplicator
|
|
self._running = False
|
|
self._last_message_at: float | None = None
|
|
self._tasks: list[asyncio.Task] = []
|
|
self._queue: asyncio.Queue | None = None
|
|
|
|
@property
|
|
def last_message_at(self) -> float | None:
|
|
return self._last_message_at
|
|
|
|
def get_queue(self) -> asyncio.Queue | None:
|
|
return self._queue
|
|
|
|
async def start(self, ctx) -> object:
|
|
account = self._resolve_account(ctx)
|
|
if not account.is_configured():
|
|
logger.warning("whatsapp account %s not configured, skipping start", account.account_id)
|
|
return {"running": False, "reason": "not-configured"}
|
|
|
|
self._queue = asyncio.Queue(maxsize=1000)
|
|
self._running = True
|
|
|
|
task = asyncio.create_task(self._webhook_processing_loop(account))
|
|
self._tasks.append(task)
|
|
|
|
logger.info("whatsapp gateway started for account %s", account.account_id)
|
|
return {"running": True, "account_id": account.account_id}
|
|
|
|
async def stop(self, ctx) -> None:
|
|
self._running = False
|
|
|
|
for task in self._tasks:
|
|
task.cancel()
|
|
self._tasks.clear()
|
|
|
|
self._queue = None
|
|
|
|
logger.info("whatsapp gateway stopped")
|
|
|
|
async def enqueue_webhook(self, payload: dict) -> None:
|
|
if self._queue is not None:
|
|
await self._queue.put(payload)
|
|
|
|
async def _webhook_processing_loop(self, account: WhatsAppAccount):
|
|
while self._running and self._queue is not None:
|
|
try:
|
|
payload = await asyncio.wait_for(self._queue.get(), timeout=1.0)
|
|
await self._process_payload(payload, account)
|
|
except TimeoutError:
|
|
continue
|
|
except asyncio.CancelledError:
|
|
break
|
|
except Exception:
|
|
logger.exception("Error processing webhook payload")
|
|
|
|
async def _process_payload(self, payload: dict, account: WhatsAppAccount):
|
|
messages = parse_webhook_to_unified(payload, account)
|
|
status_events = parse_statuses(payload, account)
|
|
|
|
for status in status_events:
|
|
logger.info(
|
|
"WhatsApp status: msg_id=%s status=%s recipient=%s",
|
|
status["msg_id"], status["status"], status["recipient_id"],
|
|
)
|
|
if status["errors"]:
|
|
logger.warning("WhatsApp message error: %s", status["errors"])
|
|
|
|
entries = payload.get("entry", [])
|
|
for entry in entries:
|
|
for change in entry.get("changes", []):
|
|
value = change.get("value", {})
|
|
for err in value.get("errors", []):
|
|
logger.error(
|
|
"WhatsApp webhook error: code=%s title=%s",
|
|
err.get("code"), err.get("title"),
|
|
)
|
|
|
|
for unified_msg in messages:
|
|
if self._is_echo(unified_msg, account):
|
|
continue
|
|
|
|
if self._deduplicator.is_duplicate(unified_msg.get("msg_id", "")):
|
|
logger.debug("Duplicate message ignored: %s", unified_msg.get("msg_id"))
|
|
continue
|
|
|
|
target_queue = self._resolve_target_queue(unified_msg)
|
|
if target_queue:
|
|
self._last_message_at = time.monotonic()
|
|
await target_queue.put(unified_msg)
|
|
logger.debug("WhatsApp message enqueued: sender=%s", unified_msg.get("sender", {}).get("id"))
|
|
|
|
@staticmethod
|
|
def _is_echo(msg: dict, account: WhatsAppAccount) -> bool:
|
|
sender = msg.get("sender", {})
|
|
sender_id = sender.get("id", "")
|
|
return sender_id == account.phone_number_id
|
|
|
|
def _resolve_target_queue(self, _msg: dict) -> asyncio.Queue | None:
|
|
return self._queue
|
|
|
|
def _resolve_account(self, ctx) -> WhatsAppAccount:
|
|
config = getattr(ctx, "config", {}) if ctx else {}
|
|
accounts = config.get("accounts", {})
|
|
account_id = getattr(ctx, "account_id", "default")
|
|
raw = accounts.get(account_id, {})
|
|
|
|
account_dict = self._config_adapter._build_account(account_id, raw)
|
|
field_names = {f.name for f in WhatsAppAccount.__dataclass_fields__.values()}
|
|
return WhatsAppAccount(**{k: v for k, v in account_dict.items() if k in field_names}) |