实现完整的Freshdesk和Freshchat集成支持,包含会话守卫、错误定义、消息去重、配置管理、webhook处理、出站消息发送、状态监控、安全校验、配对功能和流式回复支持
142 lines
5.2 KiB
Python
142 lines
5.2 KiB
Python
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)
|