import asyncio import aiohttp import logging from typing import Tuple, List, Dict, Any, Optional from .config import Config logger = logging.getLogger(__name__) class PointsAPI: """Points system API for managing user points.""" def __init__(self, config: Config): self.config = config def _url(self, path: str) -> str: """拼接 /bot/points 端点地址。""" return f"{self.config.POINTS_API_HOST}/{path.lstrip('/')}" def _auth(self) -> Dict[str, str]: """生成 xapi Bot 鉴权参数。""" return { "user": self.config.BOT_USER, "token": self.config.BOT_TOKEN, } async def _request( self, method: str, path: str, *, payload: Optional[Dict[str, Any]] = None, params: Optional[Dict[str, Any]] = None, ) -> Optional[Dict[str, Any]]: """调用 xapi /bot/points,并只向上层暴露 data。""" request_url = self._url(path) timeout = aiohttp.ClientTimeout(total=10) try: async with aiohttp.ClientSession() as session: if method == "GET": request_params = {**self._auth(), **(params or {})} async with session.get(request_url, params=request_params, timeout=timeout) as resp: return await self._parse_response(resp, path) request_payload = {**self._auth(), **(payload or {})} async with session.post(request_url, json=request_payload, timeout=timeout) as resp: return await self._parse_response(resp, path) except aiohttp.ClientError as exc: logger.error("points api request failed path=%s error=%s", path, exc) return None except asyncio.TimeoutError as exc: logger.error("points api request timeout path=%s error=%s", path, exc) return None async def _parse_response(self, resp: aiohttp.ClientResponse, path: str) -> Optional[Dict[str, Any]]: """解析 xapi 统一响应,失败时返回 None 维持旧 API 失败语义。""" if resp.status != 200: logger.error("points api bad status path=%s status=%s", path, resp.status) return None body = await resp.json() if body.get("code") != 200: logger.error("points api fail path=%s code=%s message=%s", path, body.get("code"), body.get("message")) return None data = body.get("data") return data if isinstance(data, dict) else None async def get_balance(self, user_id: str) -> int: """Get user's current points balance.""" data = await self._request("GET", "balance", params={"user_id": user_id}) if data is None: return 0 return int(data.get("balance", 0) or 0) async def add_points( self, user_id: str, amount: int, source: str, reason: str = None ) -> Tuple[bool, int]: """Add points to user account. Returns: (success, new_balance) """ # 保留原 PointsAPI 的入参失败语义;限额校验由 xapi 承担。 if not isinstance(amount, int) or amount <= 0: return False, 0 if not user_id or not source: return False, 0 data = await self._request( "POST", "add", payload={"user_id": user_id, "amount": amount, "source": source, "reason": reason}, ) return self._change_result(data) async def spend_points( self, user_id: str, amount: int, source: str, reason: str = None ) -> Tuple[bool, int]: """Spend points from user account. Returns: (success, new_balance) """ # 保留原 PointsAPI 的入参失败语义;余额与限额校验由 xapi 承担。 if not isinstance(amount, int) or amount <= 0: return False, 0 if not user_id or not source: return False, 0 data = await self._request( "POST", "spend", payload={"user_id": user_id, "amount": amount, "source": source, "reason": reason}, ) return self._change_result(data) async def set_points( self, user_id: str, amount: int, source: str, reason: str = None ) -> Tuple[bool, int]: """Set user's points to exact amount. Returns: (success, new_balance) """ # set 仍保持原 PointsAPI 行为:只校验非负,不做余额上限判断。 if not isinstance(amount, int) or amount < 0: return False, 0 if not user_id or not source: return False, 0 data = await self._request( "POST", "set", payload={"user_id": user_id, "amount": amount, "source": source, "reason": reason}, ) return self._change_result(data) async def get_transactions( self, user_id: str, limit: int = 20, offset: int = 0 ) -> List[Dict[str, Any]]: """Get transaction history for a user. Returns: List of transaction dicts """ limit = max(1, min(100, limit)) offset = max(0, offset) data = await self._request( "GET", "transactions", params={"user_id": user_id, "limit": limit, "offset": offset}, ) if data is None: return [] items = data.get("items", []) return items if isinstance(items, list) else [] async def get_ranking( self, limit: int = 10, order_by: str = "points" ) -> List[Dict[str, Any]]: """Get points ranking. Returns: List of ranking dicts with rank field """ limit = max(1, min(100, limit)) if order_by not in ("points", "total_earned"): order_by = "points" data = await self._request( "GET", "ranking", params={"limit": limit, "order_by": order_by}, ) if data is None: return [] items = data.get("items", []) return items if isinstance(items, list) else [] def _change_result(self, data: Optional[Dict[str, Any]]) -> Tuple[bool, int]: """解析 add/spend/set 响应并维持旧失败返回值。""" if data is None: return False, 0 return bool(data.get("success")), int(data.get("balance", 0) or 0)