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