from __future__ import annotations import asyncio import time from typing import Any import httpx from yuxi.channels.exceptions import ChannelAuthenticationError from yuxi.utils.logging_config import logger class MPClient: def __init__(self, http_client: httpx.AsyncClient, config: dict[str, Any]): self._http_client = http_client self._config = config self._access_token: str | None = None self._token_expires_at: float = 0.0 @property def app_id(self) -> str: return self._config["app_id"] async def get_access_token(self) -> str: now = time.time() if self._access_token and now < self._token_expires_at - 300: return self._access_token for attempt in range(3): try: return await self._refresh_token() except httpx.TimeoutException: if attempt < 2: await asyncio.sleep(2**attempt) else: raise ChannelAuthenticationError("Failed to refresh MP access_token after 3 attempts") async def _refresh_token(self) -> str: url = "https://api.weixin.qq.com/cgi-bin/token" params = { "grant_type": "client_credential", "appid": self._config["app_id"], "secret": self._config["app_secret"], } resp = await self._http_client.get(url, params=params) data = resp.json() if "access_token" in data: self._access_token = data["access_token"] self._token_expires_at = time.time() + data.get("expires_in", 7200) logger.debug(f"[MP] Token refreshed, expires in {data.get('expires_in')}s") return self._access_token raise ChannelAuthenticationError(f"Failed to get MP access_token: {data.get('errmsg', 'Unknown')}") def invalidate_token(self) -> None: self._access_token = None self._token_expires_at = 0.0 async def upload_temp_media(self, file_data: bytes, filename: str, media_type: str) -> str: token = await self.get_access_token() url = f"https://api.weixin.qq.com/cgi-bin/media/upload?access_token={token}&type={media_type}" files = {"media": (filename, file_data)} resp = await self._http_client.post(url, files=files) data = resp.json() media_id = data.get("media_id") if not media_id: raise ChannelAuthenticationError(f"Failed to upload MP media: {data.get('errmsg', 'Unknown')}") return media_id