feat(sign_in): 新增独立每日签到插件,与抽卡签到共用同一份签到数据
- 群聊命令「签到」(别名: 每日签到/打卡),白名单群聊或管理员可用 - 复用 xapi /bot/gacha/sign-in + /bot/points/add,与抽卡/WPF 共用 bot_gacha_daily_sign_in 表 - 积分随机 1-100,流水沿用 gacha_sign/抽卡签到;先到先得,重复签到提示已签到 - 新增 10 个单元测试,ruff 通过;更新 PLUGINS.md / README.md
This commit is contained in:
88
danding_bot/plugins/danding_sign_in/api.py
Normal file
88
danding_bot/plugins/danding_sign_in/api.py
Normal file
@@ -0,0 +1,88 @@
|
||||
"""独立签到插件 - 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},
|
||||
)
|
||||
Reference in New Issue
Block a user