import hashlib import hmac import logging from yuxi.channel.extensions.freshdesk.client import FreshdeskClient from yuxi.channel.extensions.freshdesk.config import FreshdeskConfigAdapter from yuxi.channel.extensions.freshdesk.dedupe import MessageDeduplicator from yuxi.channel.extensions.freshdesk.errors import FreshdeskError, FreshdeskErrorCode from yuxi.channel.extensions.freshdesk.reconciler import FreshdeskReconciler from yuxi.channel.extensions.freshdesk.types import FreshdeskIdentity, FreshdeskMode logger = logging.getLogger(__name__) class FreshdeskGateway: def __init__(self): self._accounts: dict[str, dict] = {} self._dedupe = MessageDeduplicator() self._reconcilers: dict[str, FreshdeskReconciler] = {} self._queue = None @property def accounts(self) -> dict[str, dict]: return self._accounts @property def dedupe(self) -> MessageDeduplicator: return self._dedupe @property def queue(self): return self._queue def resolve_account_by_hint(self, domain_hint: str | None = None, api_key_hint: str | None = None) -> dict | None: if not self._accounts: return None if len(self._accounts) == 1: return next(iter(self._accounts.values())) for entry in self._accounts.values(): account = entry["account"] if domain_hint and account.freshdesk_domain == domain_hint: return entry if domain_hint and account.freshchat_domain == domain_hint: return entry if api_key_hint and account.freshdesk_api_key.startswith(api_key_hint): return entry if api_key_hint and account.freshchat_api_key.startswith(api_key_hint): return entry return None async def start(self, ctx) -> object: adapter = FreshdeskConfigAdapter() config = getattr(ctx, "raw_config", None) or {} account = await self._resolve_account(ctx.account_id, adapter, config) if not adapter.is_configured(account): raise FreshdeskError(FreshdeskErrorCode.CONFIG_ERROR, "凭证配置不完整") client = FreshdeskClient( freshdesk_domain=account.freshdesk_domain, freshdesk_api_key=account.freshdesk_api_key, freshchat_domain=account.freshchat_domain, freshchat_api_key=account.freshchat_api_key, ) identity = await self._auth_test(client, account) self._queue = getattr(ctx, "queue", None) self._accounts[ctx.account_id] = { "account": account, "client": client, "identity": identity, } reconciler = FreshdeskReconciler(client, account, self._dedupe, self._queue) await reconciler.start() self._reconcilers[ctx.account_id] = reconciler logger.info("Freshdesk gateway started for account %s, mode=%s", account.account_id, account.mode) return { "running": True, "account": account, "identity": identity, } async def stop(self, ctx) -> None: reconciler = self._reconcilers.pop(ctx.account_id, None) if reconciler: await reconciler.stop() entry = self._accounts.pop(ctx.account_id, None) if entry is None: return await entry["client"].close() self._queue = None logger.info("Freshdesk gateway stopped for account %s", ctx.account_id) async def _resolve_account(self, account_id: str, adapter, config: dict): return adapter.resolve_account(account_id, config) async def _auth_test(self, client: FreshdeskClient, account) -> FreshdeskIdentity: fc_ok = False fd_ok = False fc_identity = {} fd_identity = {} if account.mode in (FreshdeskMode.FRESHCHAT, FreshdeskMode.BOTH) and account.freshchat_api_key: try: fc_identity = await client.fc_get_me() fc_ok = True except Exception as e: logger.warning("Freshchat Token 验证失败: %s", e) if account.mode in (FreshdeskMode.FRESHDESK, FreshdeskMode.BOTH) and account.freshdesk_api_key: try: fd_identity = await client.fd_get_me() fd_ok = True except Exception as e: logger.warning("Freshdesk API Key 验证失败: %s", e) if not fc_ok and not fd_ok: raise FreshdeskError(FreshdeskErrorCode.AUTH_ERROR, "所有凭证验证失败") return FreshdeskIdentity( freshdesk_agent_id=str(fd_identity.get("id", "")), freshdesk_agent_name=fd_identity.get("contact", {}).get("name", "") if fd_ok else "", freshchat_account_id=str(fc_identity.get("id", "")) if fc_ok else "", freshchat_account_name=fc_identity.get("name", "") if fc_ok else "", ) def verify_webhook_signature(self, body: bytes, x_freshchat_hmac: str, webhook_secret: str) -> bool: if not webhook_secret or not x_freshchat_hmac: return False computed = hmac.new( webhook_secret.encode("utf-8"), body, hashlib.sha256, ).hexdigest() return hmac.compare_digest(computed, x_freshchat_hmac)