"""独立签到插件 - xapi 签到记录客户端。 只封装 `POST /bot/gacha/sign-in`,与 onmyoji_gacha 抽卡插件调用同一端点、 写入同一张 `bot_gacha_daily_sign_in` 表,保证两个入口共用一份签到数据。 积分发放由共享的 danding_points.points_api(/bot/points/add)完成。 """ from __future__ import annotations import asyncio import logging from typing import Any, Dict, Optional import aiohttp from .config import Config logger = logging.getLogger(__name__) class SignInAPI: """签到记录 API,封装 xapi /bot/gacha/sign-in。""" def __init__(self, config: Config): self.config = config def _url(self, path: str) -> str: """拼接 /bot/gacha 端点地址。""" return f"{self.config.GACHA_API_HOST}/{path.lstrip('/')}" def _auth(self) -> Dict[str, str]: """生成 xapi Bot 鉴权参数。""" return { "user": self.config.BOT_USER_ID, "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/gacha,并只向上层暴露 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("sign-in api request failed path=%s error=%s", path, exc) return None except asyncio.TimeoutError as exc: logger.error("sign-in 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 维持旧调用方失败语义。""" if resp.status != 200: logger.error("sign-in api bad status path=%s status=%s", path, resp.status) return None body = await resp.json() if body.get("code") != 200: logger.error("sign-in 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 record_sign_in(self, user_id: str, points_awarded: int) -> Optional[Dict[str, Any]]: """记录每日签到;返回 xapi data(含 signed_already),失败返回 None。""" return await self._request( "POST", "sign-in", payload={"user_id": user_id, "points_awarded": points_awarded}, )