import asyncio import logging import xml.etree.ElementTree as ET from fastapi import APIRouter, Query, Request from fastapi.responses import PlainTextResponse from yuxi.channel.extensions.wechat_kf.crypto import WeChatCrypto from yuxi.channel.extensions.wechat_kf.dedupe import MessageDeduplicator from yuxi.channel.extensions.wechat_kf.config import WeChatKFConfig from yuxi.channel.extensions.wechat_kf.gateway import _get_gateway logger = logging.getLogger(__name__) router = APIRouter(prefix="/webhook/wechat-kf", tags=["wechat-kf"]) _config = WeChatKFConfig() _deduplicator = MessageDeduplicator() _pairing = None def _get_pairing(): global _pairing if _pairing is None: from yuxi.channel.extensions.wechat_kf.pairing import WeChatKFPairing _pairing = WeChatKFPairing() return _pairing def _build_crypto() -> WeChatCrypto | None: account = _config.resolve_account() if not account.is_configured(): return None return WeChatCrypto( token=account.token, encoding_aes_key=account.encoding_aes_key, app_id=account.corp_id, ) @router.get("/callback") async def kf_url_verify( msg_signature: str = Query(...), timestamp: str = Query(...), nonce: str = Query(...), echostr: str = Query(...), ): crypto = _build_crypto() if crypto is None: return PlainTextResponse("not configured", status_code=503) if not crypto.verify_signature(msg_signature, timestamp, nonce, echostr): logger.warning("WeChat KF URL verification failed: invalid signature") return PlainTextResponse("signature failed", status_code=403) try: decoded, _ = crypto.decrypt(echostr) return PlainTextResponse(decoded) except Exception: logger.exception("WeChat KF echostr decryption failed") return PlainTextResponse("decrypt failed", status_code=400) @router.post("/callback") async def kf_message_callback( request: Request, msg_signature: str = Query(...), timestamp: str = Query(...), nonce: str = Query(...), ): crypto = _build_crypto() if crypto is None: return PlainTextResponse("not configured", status_code=503) body = await request.body() body_str = body.decode("utf-8") try: root = ET.fromstring(body_str) encrypt_el = root.find("Encrypt") encrypt = encrypt_el.text or "" if encrypt_el is not None else "" except Exception: logger.exception("WeChat KF XML parse failed") return PlainTextResponse("invalid xml", status_code=400) if not crypto.verify_signature(msg_signature, timestamp, nonce, encrypt): logger.warning("WeChat KF message callback: invalid msg_signature") return PlainTextResponse("signature failed", status_code=403) try: decrypted_xml, _ = crypto.decrypt(encrypt) except Exception: logger.exception("WeChat KF message decryption failed") return PlainTextResponse("decrypt failed", status_code=403) try: root = ET.fromstring(decrypted_xml) except Exception: logger.exception("WeChat KF decrypted XML parse failed") return PlainTextResponse("invalid decrypted xml", status_code=400) msg_type = root.findtext("MsgType", "") event = root.findtext("Event", "") open_kfid = root.findtext("OpenKfId", "") token = root.findtext("Token", "") if msg_type == "event" and event == "kf_msg_or_event": asyncio.create_task(_handle_kf_event(open_kfid, token)) return PlainTextResponse("success") async def _handle_kf_event(open_kfid: str, token: str): gw = _get_gateway() if not gw: logger.warning("No WeChat KF gateway available for event handling") return from yuxi.channel.extensions.wechat_kf.sync import sync_messages try: result = await sync_messages( http=gw._http, access_token=await gw.get_access_token(), open_kfid=open_kfid, token=token, ) if result.get("errcode") == 0: for msg in result.get("msg_list", []): await gw._handle_message(msg) except Exception: logger.exception("WeChat KF event handling failed for open_kfid=%s", open_kfid)