完成Help Scout渠道的完整功能实现,包含API客户端、webhook处理、轮询同步、自动回复、工单标签分配、客户资料管理等核心能力,附带完整的配置Schema与插件元信息
75 lines
2.2 KiB
Python
75 lines
2.2 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
import time
|
|
from dataclasses import dataclass
|
|
|
|
import httpx
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
TOKEN_URL = "https://api.helpscout.net/v2/oauth2/token"
|
|
REFRESH_BUFFER_SECONDS = 300
|
|
|
|
|
|
@dataclass
|
|
class HelpScoutToken:
|
|
access_token: str
|
|
token_type: str = "bearer"
|
|
expires_at: float = 0.0
|
|
|
|
|
|
class HelpScoutAuth:
|
|
def __init__(self, app_id: str, app_secret: str):
|
|
self._app_id = app_id
|
|
self._app_secret = app_secret
|
|
self._token: HelpScoutToken | None = None
|
|
|
|
@property
|
|
def access_token(self) -> str | None:
|
|
if self._token:
|
|
return self._token.access_token
|
|
return None
|
|
|
|
@property
|
|
def auth_header(self) -> dict:
|
|
return {"Authorization": f"Bearer {self.access_token or ''}"}
|
|
|
|
@property
|
|
def is_token_valid(self) -> bool:
|
|
return self._token is not None and time.time() < self._token.expires_at - REFRESH_BUFFER_SECONDS
|
|
|
|
async def ensure_token(self) -> str:
|
|
if not self.is_token_valid:
|
|
await self._fetch_token()
|
|
return self._token.access_token
|
|
|
|
async def force_refresh(self) -> str:
|
|
logger.info("Help Scout: forcing token refresh due to 401")
|
|
await self._fetch_token()
|
|
return self._token.access_token
|
|
|
|
async def _fetch_token(self) -> None:
|
|
async with httpx.AsyncClient(timeout=15.0) as client:
|
|
resp = await client.post(
|
|
TOKEN_URL,
|
|
data={
|
|
"grant_type": "client_credentials",
|
|
"client_id": self._app_id,
|
|
"client_secret": self._app_secret,
|
|
},
|
|
)
|
|
resp.raise_for_status()
|
|
data = resp.json()
|
|
expires_in = data.get("expires_in", 172800)
|
|
self._token = HelpScoutToken(
|
|
access_token=data["access_token"],
|
|
token_type=data.get("token_type", "bearer"),
|
|
expires_at=time.time() + expires_in,
|
|
)
|
|
logger.info(
|
|
"Help Scout token obtained: expires_in=%ds, expires_at=%s",
|
|
expires_in,
|
|
time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(self._token.expires_at)),
|
|
)
|