import hashlib import hmac import logging from yuxi.channel.context import ChannelContext from yuxi.channel.extensions.intercom.client import IntercomClient from yuxi.channel.extensions.intercom.config import IntercomConfigAdapter from yuxi.channel.extensions.intercom.dedupe import DedupeCache from yuxi.channel.extensions.intercom.errors import IntercomError, IntercomErrorCode from yuxi.channel.extensions.intercom.types import IntercomAdminIdentity logger = logging.getLogger(__name__) class IntercomGateway: delivery_mode = "direct" def __init__(self): self._accounts: dict[str, dict] = {} self._dedupe = DedupeCache() async def start(self, ctx: ChannelContext) -> object: adapter = IntercomConfigAdapter() config = getattr(ctx, "raw_config", None) or {} account = await adapter.resolve_account(ctx.account_id, config) if not account.get("access_token"): raise IntercomError(IntercomErrorCode.CONFIG_ERROR, "access_token is required") client = IntercomClient(account["access_token"], datacenter=account.get("datacenter", "us")) identity = await self._auth_test(client, account) ctx._intercom_identity = identity self._accounts[ctx.account_id] = { "account": account, "client": client, "identity": identity, } logger.info( "Intercom gateway started for account %s (admin=%s, app=%s)", ctx.account_id, identity.admin_id, identity.app_id, ) return { "account": account, "identity": identity, "client": client, } async def stop(self, ctx: ChannelContext) -> None: entry = self._accounts.pop(ctx.account_id, None) if entry is None: return await entry["client"].close() for attr in ("_intercom_identity",): try: delattr(ctx, attr) except AttributeError: pass logger.info("Intercom gateway stopped for account %s", ctx.account_id) async def _auth_test(self, client: IntercomClient, account: dict) -> IntercomAdminIdentity: try: me = await client.get_me() except Exception as e: raise IntercomError(IntercomErrorCode.AUTH_ERROR, f"Token 验证失败: {e}") from e admin_id = account.get("admin_id") or str(me.get("id", "")) return IntercomAdminIdentity( admin_id=admin_id, app_id=str(me.get("app", {}).get("id_code", "")), name=me.get("name", ""), email=me.get("email", ""), ) def verify_webhook_signature(self, body: bytes, x_hub_signature: str, webhook_secret: str) -> bool: if not webhook_secret: logger.warning("Webhook secret not configured, skipping signature verification") return True if not x_hub_signature: return False if not x_hub_signature.startswith("sha256="): return False received = x_hub_signature[7:] computed = hmac.new( webhook_secret.encode("utf-8"), body, hashlib.sha256, ).hexdigest() return hmac.compare_digest(computed, received) def get_account(self, account_id: str) -> dict | None: return self._accounts.get(account_id) def get_client(self, account_id: str) -> IntercomClient | None: entry = self._accounts.get(account_id) return entry["client"] if entry else None def get_identity(self, account_id: str) -> IntercomAdminIdentity | None: entry = self._accounts.get(account_id) return entry["identity"] if entry else None def is_duplicate(self, event_id: str) -> bool: return self._dedupe.is_duplicate(event_id) def check_duplicate(self, event_id: str) -> bool: return self._dedupe.check_duplicate(event_id) def mark_seen(self, event_id: str) -> None: self._dedupe.mark_seen(event_id) def reset_dedupe(self) -> None: self._dedupe.reset() @staticmethod async def resolve_gateway_auth_bypass_paths(config: dict) -> list[str]: return ["/webhook/intercom"]