Files
HeXi/hexi/plugins/nonebot_plugin_bf_bot/services/fetchers/gametools.py
T

132 lines
4.9 KiB
Python
Raw Normal View History

"""EA 系取数(api.gametools.network)。
- `fetch_all_stats`:bf3/bf4/bf1/bfv/bf6 的 `/all` 战绩接口(by-id 与 by-name
并发请求,谁先拿到有效数据用谁)。
- `get_player_info_by_name` / `get_player_info_by_ea_id`:`bfglobal/player` 查人,
绑定流程用。
- `get_bf6_rank`:`bf6/profile` 取等级(playerCard.rank)。
gametools 是同步 requests 的部分一律 `asyncio.to_thread`,避免卡事件循环。
"""
import asyncio
import json
import httpx
import requests
from nonebot import logger
_API = "https://api.gametools.network"
_HEADERS = {"accept": "application/json"}
async def fetch_all_stats(game, player, player_id, user_id, platform):
"""查 `{game}/all/`:by-id 与 by-name 并发,先成功(且无 errors)的胜出。"""
base = f"{_API}/{game}/all/?format_values=true"
url_by_id = (
f"{base}&playerid={player_id}&oid={user_id}"
f"&platform={platform}&skip_battlelog=false&lang=en-us"
)
url_by_name = (
f"{base}&name={player}&platform={platform}&skip_battlelog=false&lang=en-us"
)
async with httpx.AsyncClient(timeout=10.0) as client:
# 创建两个并发任务
task_id = asyncio.create_task(client.get(url_by_id))
task_name = asyncio.create_task(client.get(url_by_name))
# 等待第一个完成的任务
done, pending = await asyncio.wait(
[task_id, task_name], return_when=asyncio.FIRST_COMPLETED
)
# 取消未完成的任务
for task in pending:
task.cancel()
# 检查已完成的任务是否成功且无 errors
for task in done:
try:
response = await task
if response.status_code == 200:
data = response.json()
if "errors" not in data:
logger.info(
f"成功获取数据({'ID' if task is task_id else 'Name'}方式)"
)
return data
except (httpx.RequestError, ValueError) as e:
logger.warning(f"请求或解析失败: {e}")
# 如果所有尝试都失败,返回错误
return {"errors": ["未找到玩家"]}
async def _get_player_json(url: str) -> dict:
"""requests 放线程池 + 超时;网络/解析失败统一降级成 {"errors": [...]}。"""
try:
response = await asyncio.to_thread(
requests.get, url, headers=_HEADERS, timeout=10
)
return json.loads(response.text)
except Exception as err: # noqa: BLE001 - 调用方按 errors 分支处理
logger.warning(f"查询玩家信息失败: {err}")
return {"errors": [str(err)]}
def player_display_name(info: dict, fallback: str = "error") -> str:
"""从 bfglobal/player 响应里取展示名。
该接口实测有两种形态:一种给 personaName,另一种(同一玩家也会出现)只给
name + nucleusId;漏掉 name 时卡片会把玩家名显示成 "error"(老代码的问题)。
"""
return (
info.get("personaName")
or info.get("name")
or info.get("displayName")
or info.get("username")
or fallback
)
async def get_player_info_by_name(user_name, platform) -> dict:
url = (
f"{_API}/bfglobal/player/?name={user_name}"
f"&platform={platform}&skip_battlelog=false"
)
logger.info(f"查询用户请求url:{url}")
return await _get_player_json(url)
async def get_player_info_by_ea_id(player_id, user_id, platform) -> dict:
url = (
f"{_API}/bfglobal/player/?playerid={player_id}&oid={user_id}"
f"&platform={platform}&skip_battlelog=false"
)
return await _get_player_json(url)
async def get_bf6_rank(player, player_id, user_id, platform) -> int:
"""取BF6等级:gametools profile 接口返回 playerProfiles[0].playerCard.rank"""
url = (
f"{_API}/bf6/profile/?name={player}"
f"&playerid={player_id}&nucleus_id={user_id}&platform={platform}"
f"&skip_battlelog=true&lang=en-us"
)
try:
response = await asyncio.to_thread(
requests.get, url, headers=_HEADERS, timeout=10
)
profiles = response.json().get("playerProfiles") or []
except Exception as err: # noqa: BLE001 - 等级取不到就当 0,不打断整卡
logger.warning(f"查询BF6等级失败: {err}")
return 0
logger.info(f"BF6 profile 返回:{str(profiles)[:300]}")
if not isinstance(profiles, list) or not profiles:
logger.info(f"未搜索到用户: {player}")
return 0
profile = profiles[0] if isinstance(profiles[0], dict) else {}
# 该接口的真实字段:playerCard.rank 是等级数字,rankName 是称号名
rank = (profile.get("playerCard") or {}).get("rank") or 0
logger.info(f"BF6等级: {rank}(称号:{profile.get('rankName')})")
return rank