129 lines
4.2 KiB
Python
129 lines
4.2 KiB
Python
|
|
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"]
|