新增企业微信、微博、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
89 lines
3.0 KiB
Python
89 lines
3.0 KiB
Python
import hashlib
|
|
import hmac
|
|
import json
|
|
import logging
|
|
|
|
from fastapi import APIRouter, HTTPException, Query, Request
|
|
from fastapi.responses import JSONResponse, PlainTextResponse
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/api/channel/workplace", tags=["workplace-webhook"])
|
|
|
|
|
|
@router.get("/webhook")
|
|
async def verify_webhook(
|
|
hub_mode: str = Query(None, alias="hub.mode"),
|
|
hub_challenge: str = Query(None, alias="hub.challenge"),
|
|
hub_verify_token: str = Query(None, alias="hub.verify_token"),
|
|
):
|
|
if hub_mode != "subscribe":
|
|
raise HTTPException(status_code=400, detail="Invalid hub.mode")
|
|
|
|
from yuxi.channel.extensions.workplace.config import WorkplaceConfigAdapter
|
|
|
|
adapter = WorkplaceConfigAdapter()
|
|
account = await adapter.resolve_account("default")
|
|
|
|
expected_token = account.get("verify_token", "")
|
|
if not expected_token or hub_verify_token != expected_token:
|
|
logger.warning("Webhook verify_token mismatch")
|
|
raise HTTPException(status_code=403, detail="Verify token mismatch")
|
|
|
|
logger.info("Webhook verified successfully")
|
|
return PlainTextResponse(hub_challenge or "", status_code=200)
|
|
|
|
|
|
@router.post("/webhook")
|
|
async def receive_webhook(request: Request):
|
|
body = await request.body()
|
|
signature = request.headers.get("X-Hub-Signature-256", "")
|
|
|
|
from yuxi.channel.extensions.workplace.config import WorkplaceConfigAdapter
|
|
|
|
adapter = WorkplaceConfigAdapter()
|
|
account = await adapter.resolve_account("default")
|
|
app_secret = account.get("app_secret", "")
|
|
|
|
if app_secret and not _verify_signature(body, signature, app_secret):
|
|
logger.warning("Webhook signature verification failed")
|
|
raise HTTPException(status_code=403, detail="Invalid signature")
|
|
|
|
try:
|
|
payload = json.loads(body)
|
|
except json.JSONDecodeError:
|
|
logger.error("Webhook payload is not valid JSON")
|
|
raise HTTPException(status_code=400, detail="Invalid JSON")
|
|
|
|
from yuxi.channel.extensions.workplace.gateway import _get_webhook_queue
|
|
|
|
queue = _get_webhook_queue()
|
|
if queue is not None:
|
|
entries = payload.get("entry", [])
|
|
enqueued = 0
|
|
for entry in entries:
|
|
for messaging_event in entry.get("messaging", []):
|
|
await queue.put(messaging_event)
|
|
enqueued += 1
|
|
for change in entry.get("changes", []):
|
|
await queue.put(change)
|
|
enqueued += 1
|
|
if enqueued == 0:
|
|
await queue.put(payload)
|
|
else:
|
|
logger.debug("Webhook enqueued %d events", enqueued)
|
|
|
|
return JSONResponse({"status": "ok"})
|
|
|
|
|
|
def _verify_signature(body: bytes, signature: str, app_secret: str) -> bool:
|
|
if not signature or not app_secret:
|
|
return False
|
|
expected = hmac.new(
|
|
app_secret.encode("utf-8"),
|
|
body,
|
|
hashlib.sha256,
|
|
).hexdigest()
|
|
expected_header = f"sha256={expected}"
|
|
return hmac.compare_digest(expected_header.encode("utf-8"), signature.encode("utf-8"))
|