新增了Zalo用户频道的完整适配器实现,包括: - 基础的适配器初始化与导出结构 - 群组同步与成员获取功能 - 请求限流与退避重试机制 - 健康检查与状态探针 - 消息反应/表情处理工具 - 贴纸缓存与消息去重功能 - 消息ID格式化与追踪 - TTS语音合成支持 - 消息发送权限校验 - 长文本分块发送 - 操作审批流程 - 常量配置与国际化支持 - 图像视觉分析功能 - 贴纸消息处理 - 登录与配置向导 - 群组上下文缓存 - 网关连接管理 - 配置Schema校验 - 状态问题与安全审计 - 内联按钮与交互组件 - 交互式回调分发 - 联系人与群组目录管理 - 富媒体卡片消息支持
105 lines
3.4 KiB
Python
105 lines
3.4 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
from yuxi.utils.logging_config import logger
|
|
|
|
from .bridge import BridgeClient
|
|
from .login import LoginFlow
|
|
|
|
|
|
@dataclass
|
|
class GatewayContext:
|
|
bridge_url: str
|
|
config: dict[str, Any]
|
|
bridge: BridgeClient | None = None
|
|
login_flow: LoginFlow | None = None
|
|
|
|
|
|
class ZaloGateway:
|
|
def __init__(self):
|
|
self._accounts: dict[str, GatewayContext] = {}
|
|
self._lock = asyncio.Lock()
|
|
|
|
async def start_account(self, profile: str, config: dict[str, Any]) -> GatewayContext:
|
|
async with self._lock:
|
|
if profile in self._accounts:
|
|
logger.info(f"[ZaloUser] Gateway account '{profile}' already started")
|
|
return self._accounts[profile]
|
|
|
|
bridge_url = config.get("bridge_url", "http://localhost:5556")
|
|
timeout = config.get("network", {}).get("connect_timeout", 10)
|
|
|
|
bridge = BridgeClient(bridge_url, timeout)
|
|
await bridge.start()
|
|
|
|
login_flow = LoginFlow(bridge, config)
|
|
|
|
ctx = GatewayContext(
|
|
bridge_url=bridge_url,
|
|
config=config,
|
|
bridge=bridge,
|
|
login_flow=login_flow,
|
|
)
|
|
self._accounts[profile] = ctx
|
|
logger.info(f"[ZaloUser] Gateway account '{profile}' started")
|
|
return ctx
|
|
|
|
async def login_with_qr_start(self, profile: str = "default") -> dict[str, Any]:
|
|
if profile not in self._accounts:
|
|
raise ValueError(f"Account '{profile}' not started. Call start_account first.")
|
|
|
|
ctx = self._accounts[profile]
|
|
if ctx.login_flow is None:
|
|
ctx.login_flow = LoginFlow(ctx.bridge, ctx.config)
|
|
result = await ctx.login_flow.start_qr_login()
|
|
return result
|
|
|
|
async def login_with_qr_wait(
|
|
self,
|
|
profile: str = "default",
|
|
poll_interval: float = 2.0,
|
|
timeout: float = 120.0,
|
|
) -> dict[str, Any]:
|
|
if profile not in self._accounts:
|
|
raise ValueError(f"Account '{profile}' not started.")
|
|
ctx = self._accounts[profile]
|
|
if ctx.login_flow is None:
|
|
ctx.login_flow = LoginFlow(ctx.bridge, ctx.config)
|
|
try:
|
|
logged_in = await ctx.login_flow.wait_for_login(poll_interval, timeout)
|
|
return {"status": "success", "logged_in": True}
|
|
except Exception as e:
|
|
return {"status": "error", "logged_in": False, "error": str(e)}
|
|
|
|
async def logout_account(self, profile: str = "default") -> dict[str, Any]:
|
|
if profile not in self._accounts:
|
|
return {"status": "not_found"}
|
|
ctx = self._accounts[profile]
|
|
if ctx.login_flow:
|
|
await ctx.login_flow.logout()
|
|
if ctx.bridge:
|
|
await ctx.bridge.stop()
|
|
del self._accounts[profile]
|
|
logger.info(f"[ZaloUser] Gateway account '{profile}' logged out")
|
|
return {"status": "logged_out"}
|
|
|
|
async def shutdown(self) -> None:
|
|
for profile in list(self._accounts.keys()):
|
|
await self.logout_account(profile)
|
|
|
|
def list_accounts(self) -> list[str]:
|
|
return list(self._accounts.keys())
|
|
|
|
|
|
_zalo_gateway: ZaloGateway | None = None
|
|
|
|
|
|
def get_zalo_gateway() -> ZaloGateway:
|
|
global _zalo_gateway
|
|
if _zalo_gateway is None:
|
|
_zalo_gateway = ZaloGateway()
|
|
return _zalo_gateway
|