新增了Alexa渠道的所有核心功能模块,包括配置管理、请求校验、会话管理、安全验证、去重、响应格式化、渐进式响应、主动推送、提醒功能、配对绑定以及Webhook端点,并完成了插件注册和配置项定义。
144 lines
5.3 KiB
Python
144 lines
5.3 KiB
Python
import logging
|
|
import time
|
|
|
|
import httpx
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
LWA_TOKEN_URL = "https://api.amazon.com/auth/o2/token"
|
|
PROACTIVE_EVENTS_URL = "https://api.amazonalexa.com/v1/proactiveEvents"
|
|
PROACTIVE_SCOPE = "alexa::proactive_events"
|
|
LWA_TOKEN_TTL_MARGIN = 60
|
|
|
|
|
|
class ProactiveEventsService:
|
|
|
|
def __init__(self, account=None):
|
|
self._account = account
|
|
self._enabled = getattr(account, "proactive_events_enabled", False) if account else False
|
|
self._client_id = getattr(account, "oauth_client_id", "") if account else ""
|
|
self._client_secret = getattr(account, "oauth_client_secret", "") if account else ""
|
|
self._lwa_token: str | None = None
|
|
self._lwa_token_expires_at: float = 0
|
|
|
|
@property
|
|
def enabled(self) -> bool:
|
|
return self._enabled
|
|
|
|
async def send_message_alert(self, user_id: str, message: str) -> bool:
|
|
return await self._send_event(user_id=user_id, audience_type="Unicast")
|
|
|
|
async def send_broadcast_alert(self, message: str) -> bool:
|
|
return await self._send_event(user_id=None, audience_type="Broadcast")
|
|
|
|
async def _send_event(self, user_id: str | None, audience_type: str) -> bool:
|
|
if not self._enabled:
|
|
logger.debug("Proactive events disabled, skipping send")
|
|
return False
|
|
|
|
token = await self._get_lwa_token()
|
|
if not token:
|
|
logger.warning("Failed to acquire LWA token for proactive events")
|
|
return False
|
|
|
|
user_label = user_id[:12] if user_id else "broadcast"
|
|
|
|
event_body = {
|
|
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
"referenceId": f"fp-msg-{user_label}-{int(time.time())}",
|
|
"expiryTime": time.strftime(
|
|
"%Y-%m-%dT%H:%M:%SZ", time.gmtime(time.time() + 86400)
|
|
),
|
|
"event": {
|
|
"name": "AMAZON.MessageAlert.Activated",
|
|
"payload": {
|
|
"state": {
|
|
"status": "UNREAD",
|
|
"freshness": "NEW",
|
|
},
|
|
"messageGroup": {
|
|
"creator": {
|
|
"name": "ForcePilot",
|
|
},
|
|
"count": 1,
|
|
"urgency": "URGENT",
|
|
},
|
|
},
|
|
},
|
|
"localizedAttributes": [
|
|
{
|
|
"locale": "zh-CN",
|
|
"sellerName": "ForcePilot",
|
|
}
|
|
],
|
|
"relevantAudience": self._build_audience(user_id, audience_type),
|
|
}
|
|
|
|
async with httpx.AsyncClient(timeout=httpx.Timeout(10.0)) as client:
|
|
try:
|
|
resp = await client.post(
|
|
PROACTIVE_EVENTS_URL,
|
|
json=event_body,
|
|
headers={
|
|
"Authorization": f"Bearer {token}",
|
|
"Content-Type": "application/json",
|
|
},
|
|
)
|
|
if resp.status_code == 202:
|
|
logger.info(
|
|
"Proactive event (%s) sent to %s",
|
|
audience_type,
|
|
user_label,
|
|
)
|
|
return True
|
|
logger.warning(
|
|
"Proactive event API returned %d: %s",
|
|
resp.status_code,
|
|
resp.text[:200],
|
|
)
|
|
except Exception:
|
|
logger.exception("Proactive event send failed")
|
|
return False
|
|
|
|
@staticmethod
|
|
def _build_audience(user_id: str | None, audience_type: str) -> dict:
|
|
if audience_type == "Unicast" and user_id:
|
|
return {
|
|
"type": "Unicast",
|
|
"payload": {
|
|
"user": user_id,
|
|
},
|
|
}
|
|
return {"type": "Multicast"}
|
|
|
|
async def _get_lwa_token(self) -> str | None:
|
|
now = time.time()
|
|
if self._lwa_token and now < self._lwa_token_expires_at - LWA_TOKEN_TTL_MARGIN:
|
|
return self._lwa_token
|
|
|
|
if not self._client_id or not self._client_secret:
|
|
logger.warning("LWA credentials not configured for proactive events")
|
|
return None
|
|
|
|
async with httpx.AsyncClient(timeout=httpx.Timeout(10.0)) as client:
|
|
try:
|
|
resp = await client.post(
|
|
LWA_TOKEN_URL,
|
|
data={
|
|
"grant_type": "client_credentials",
|
|
"client_id": self._client_id,
|
|
"client_secret": self._client_secret,
|
|
"scope": PROACTIVE_SCOPE,
|
|
},
|
|
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
)
|
|
if resp.status_code == 200:
|
|
data = resp.json()
|
|
self._lwa_token = data.get("access_token")
|
|
expires_in = data.get("expires_in", 3600)
|
|
self._lwa_token_expires_at = now + expires_in
|
|
return self._lwa_token
|
|
logger.warning("LWA token request returned %d", resp.status_code)
|
|
except Exception:
|
|
logger.exception("LWA token acquisition failed")
|
|
return None |