新增小红书、XMPP、元宝、Zalo 四个渠道扩展。 小红书渠道扩展主要模块:config, gateway, webhook, outbound, streaming, pairing, security, dedupe, media, status, window XMPP 渠道扩展主要模块:plugin, config, gateway, outbound, streaming, pairing, security, dedupe, accounts, commands, muc, rate_limiter, stanza_utils, status, monitor 元宝渠道扩展主要模块:plugin, client, config_schema, gateway, outbound(chunk/queue/transport), inbound(dispatcher), streaming, pairing, security, accounts, actions, commands, codec(biz/conn), session, shared, utils Zalo 渠道扩展主要模块:api, config, gateway, webhook, outbound, pairing, security, session, polling, monitor, status
78 lines
2.7 KiB
Python
78 lines
2.7 KiB
Python
import hmac
|
|
import secrets
|
|
import time
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ZaloPairing:
|
|
id_label = "zaloUserId"
|
|
|
|
def __init__(self):
|
|
self._codes: dict[str, tuple[str, float]] = {}
|
|
self._code_ttl_seconds = 300
|
|
|
|
def _cleanup_expired(self) -> None:
|
|
now = time.monotonic()
|
|
expired = [pid for pid, (_, ts) in self._codes.items() if now - ts > self._code_ttl_seconds]
|
|
for pid in expired:
|
|
self._codes.pop(pid, None)
|
|
|
|
async def generate_code(self, peer_id: str) -> str:
|
|
code = f"{secrets.randbelow(1_000_000):06d}"
|
|
self._codes[peer_id] = (code, time.monotonic())
|
|
return code
|
|
|
|
async def verify_code(self, peer_id: str, code: str) -> bool:
|
|
self._cleanup_expired()
|
|
stored = self._codes.pop(peer_id, None)
|
|
if stored is None:
|
|
return False
|
|
stored_code, created_at = stored
|
|
if time.monotonic() - created_at > self._code_ttl_seconds:
|
|
return False
|
|
return hmac.compare_digest(stored_code, code)
|
|
|
|
@staticmethod
|
|
def normalize_allow_entry(entry: str) -> str:
|
|
stripped = entry.strip()
|
|
|
|
for prefix in ("zalo:", "zl:"):
|
|
if stripped.startswith(prefix):
|
|
stripped = stripped[len(prefix) :]
|
|
break
|
|
|
|
return stripped
|
|
|
|
async def notify_approval(self, config: dict, peer_id: str, account_id: str | None = None) -> None:
|
|
from yuxi.channel.extensions.zalo.config import ZaloConfigAdapter
|
|
from yuxi.channel.extensions.zalo.api import ZaloBotApi
|
|
|
|
adapter = ZaloConfigAdapter()
|
|
aid = account_id or adapter.default_account_id(config)
|
|
account = await adapter.resolve_account(aid)
|
|
|
|
bot_token = account.get("bot_token", "")
|
|
if not bot_token:
|
|
logger.warning("Zalo notify_approval: no bot_token for account %s", aid)
|
|
return
|
|
|
|
api = ZaloBotApi(bot_token, proxy=account.get("proxy"))
|
|
try:
|
|
await api.send_message(peer_id, self.pairing_approved_message())
|
|
logger.info("Zalo pairing approved notification sent to peer %s", peer_id)
|
|
except Exception:
|
|
logger.exception("Zalo notify_approval: failed to send notification to peer %s", peer_id)
|
|
finally:
|
|
await api.close()
|
|
|
|
def pairing_prompt_message(self, peer_id: str) -> str:
|
|
return (
|
|
f"To start using this bot, please ask an admin to approve your pairing request.\n\n"
|
|
f"Your Zalo user id: {peer_id}"
|
|
)
|
|
|
|
def pairing_approved_message(self) -> str:
|
|
return "Your pairing request has been approved. You can now chat with the bot."
|