Files
DanDingNoneBot/danding_bot/plugins/danding_points/api.py

182 lines
6.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)