新增 Zalo OA 官方账号完整集成能力,包含: 1. 基础通信能力:消息编解码、目标归一化、文本分块 2. 安全与校验:Webhook 签名验证、DM 策略管理、配对流程 3. 辅助工具:重复事件去重、请求限流、异常告警 4. 管理功能:账号多实例管理、配置验证、健康诊断 5. 扩展能力:媒体托管、视觉识别、TTS 语音合成 6. 运维支持:审计日志、状态监控、目录同步
109 lines
3.1 KiB
Python
109 lines
3.1 KiB
Python
from __future__ import annotations
|
|
|
|
import secrets
|
|
import time
|
|
from typing import Any
|
|
|
|
from yuxi.utils.logging_config import logger
|
|
|
|
PAIRING_CODE_BYTES = 4
|
|
PAIRING_CODE_TTL_SEC = 600
|
|
|
|
|
|
class PairingStore:
|
|
def __init__(self):
|
|
self._codes: dict[str, dict[str, Any]] = {}
|
|
|
|
def generate_code(self, user_id: str) -> str:
|
|
code = secrets.token_hex(PAIRING_CODE_BYTES).upper()[:8]
|
|
self._codes[code] = {
|
|
"user_id": user_id,
|
|
"created_at": time.time(),
|
|
"used": False,
|
|
}
|
|
return code
|
|
|
|
def verify_code(self, code: str, user_id: str) -> bool:
|
|
entry = self._codes.get(code)
|
|
if not entry:
|
|
return False
|
|
if time.time() - entry["created_at"] > PAIRING_CODE_TTL_SEC:
|
|
del self._codes[code]
|
|
return False
|
|
if entry["used"] or entry["user_id"] != user_id:
|
|
return False
|
|
entry["used"] = True
|
|
return True
|
|
|
|
def cleanup_expired(self):
|
|
now = time.time()
|
|
expired = [k for k, v in self._codes.items() if now - v["created_at"] > PAIRING_CODE_TTL_SEC]
|
|
for k in expired:
|
|
del self._codes[k]
|
|
|
|
|
|
def build_pairing_message(code: str, oa_name: str = "") -> str:
|
|
lines = [
|
|
"Welcome! This Zalo OA requires pairing to chat.",
|
|
f"Your pairing code: {code}",
|
|
"Reply with this code to complete pairing.",
|
|
f"This code expires in {PAIRING_CODE_TTL_SEC // 60} minutes.",
|
|
]
|
|
if oa_name:
|
|
lines.insert(0, f"Hello from {oa_name}!")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def build_pairing_notification(user_id: str, oa_name: str) -> str:
|
|
return (
|
|
f"[Pairing] New pairing request from user {user_id}"
|
|
+ (f" to {oa_name}" if oa_name else "")
|
|
+ ". Please approve."
|
|
)
|
|
|
|
|
|
def build_pairing_success_message(oa_name: str = "") -> str:
|
|
return "Pairing successful! You can now chat with " + (oa_name or "this OA") + "."
|
|
|
|
|
|
async def send_pairing_message(
|
|
client: Any,
|
|
user_id: str,
|
|
code: str,
|
|
oa_name: str = "",
|
|
) -> bool:
|
|
message = build_pairing_message(code, oa_name)
|
|
try:
|
|
result = await client.send_message(
|
|
{
|
|
"recipient": {"user_id": user_id},
|
|
"message": {"text": message},
|
|
}
|
|
)
|
|
msg_id = result.get("message_id", "") if isinstance(result, dict) else ""
|
|
logger.info(f"[ZaloOA] Pairing code sent to {user_id}, msg_id={msg_id}")
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"[ZaloOA] Failed to send pairing code to {user_id}: {e}")
|
|
return False
|
|
|
|
|
|
async def send_pairing_success(
|
|
client: Any,
|
|
user_id: str,
|
|
oa_name: str = "",
|
|
) -> bool:
|
|
message = build_pairing_success_message(oa_name)
|
|
try:
|
|
await client.send_message(
|
|
{
|
|
"recipient": {"user_id": user_id},
|
|
"message": {"text": message},
|
|
}
|
|
)
|
|
logger.info(f"[ZaloOA] Pairing success sent to {user_id}")
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"[ZaloOA] Failed to send pairing success to {user_id}: {e}")
|
|
return False
|