ForcePilot/backend/package/yuxi/channel/extensions/intercom/gateway.py

129 lines
4.2 KiB
Python
Raw Normal View History

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"]