343 lines
14 KiB
Python
343 lines
14 KiB
Python
import asyncio
|
||
import json
|
||
from copy import deepcopy
|
||
from time import time
|
||
from typing import Dict, List, Literal, Union
|
||
|
||
from httpx import AsyncClient, HTTPError
|
||
|
||
# from nonebot import require
|
||
# from nonebot.log import logger
|
||
from ci import logger
|
||
|
||
# require("nonebot_plugin_htmlrender")
|
||
# from nonebot_plugin_htmlrender import template_to_pic
|
||
from defs.browser import template_to_pic
|
||
|
||
from gspanel.__utils__ import LOCAL_DIR, SCALE_FACTOR, TPL_VERSION, download
|
||
from gspanel.data_convert import (
|
||
simplDamageRes,
|
||
simplFightProp,
|
||
simplTeamDamageRes,
|
||
transFromEnka,
|
||
transToTeyvat,
|
||
)
|
||
|
||
|
||
async def queryPanelApi(uid: str, source: Literal["enka", "mgg"] = "enka") -> Dict:
|
||
"""
|
||
原神游戏内角色展柜数据请求
|
||
|
||
* ``param uid: str`` 查询用户 UID
|
||
* ``param source: Literal["enka", "mgg"] = "enka"`` 查询接口
|
||
- ``return: Dict`` 查询结果,出错时返回 ``{"error": "错误信息"}``
|
||
"""
|
||
enkaMirrors = [
|
||
"https://enka.network",
|
||
"https://enka.minigg.cn",
|
||
"https://enka.microgg.cn",
|
||
]
|
||
async with AsyncClient() as client:
|
||
resJson = {}
|
||
for idx, root in enumerate(enkaMirrors):
|
||
try:
|
||
res = await client.get(
|
||
url=f"{root}/u/{uid}/__data.json",
|
||
headers={
|
||
"Accept": "application/json",
|
||
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-US;q=0.7",
|
||
"Cache-Control": "no-cache",
|
||
"Cookie": "locale=zh-CN",
|
||
"Referer": "https://enka.network/",
|
||
"User-Agent": (
|
||
"Mozilla/5.0 (Linux; Android 12; Nexus 5) "
|
||
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
||
"Chrome/102.0.0.0 Mobile Safari/537.36"
|
||
),
|
||
},
|
||
timeout=20.0,
|
||
)
|
||
resJson = res.json()
|
||
break
|
||
except (HTTPError, json.decoder.JSONDecodeError) as e:
|
||
if idx == len(enkaMirrors) - 1:
|
||
logger.opt(exception=e).error("面板数据接口无法访问或返回错误")
|
||
return {"error": f"[{e.__class__.__name__}] 暂时无法访问面板数据接口.."}
|
||
else:
|
||
logger.info(f"从 {root} 获取面板失败,正在自动切换镜像重试...")
|
||
if not resJson.get("playerInfo"):
|
||
return {"error": f"玩家 {uid} 返回信息不全,接口可能正在维护.."}
|
||
if not resJson.get("avatarInfoList"):
|
||
return {"error": f"玩家 {uid} 的角色展柜详细数据已隐藏!"}
|
||
if not resJson["playerInfo"].get("showAvatarInfoList"):
|
||
return {"error": f"玩家 {uid} 的角色展柜内还没有角色哦!"}
|
||
return resJson
|
||
|
||
|
||
async def queryDamageApi(body: Dict, mode: Literal["single", "team"] = "single") -> Dict:
|
||
"""
|
||
角色伤害计算数据请求(提瓦特小助手)
|
||
|
||
* ``param body: Dict`` 查询角色数据
|
||
* ``param mode: Literal["single", "team"] = "single"`` 查询接口类型,默认请求角色伤害接口,传入 ``"team"`` 请求队伍伤害接口
|
||
- ``return: Dict`` 查询结果,出错时返回 ``{}``
|
||
"""
|
||
apiMap = {
|
||
"single": "https://api.lelaer.com/ys/getDamageResult.php",
|
||
"team": "https://api.lelaer.com/ys/getTeamResult.php",
|
||
}
|
||
async with AsyncClient() as client:
|
||
try:
|
||
res = await client.post(
|
||
apiMap[mode],
|
||
json=body,
|
||
headers={
|
||
"referer": "https://servicewechat.com/wx2ac9dce11213c3a8/192/page-frame.html",
|
||
"user-agent": (
|
||
"Mozilla/5.0 (Linux; Android 12; SM-G977N Build/SP1A.210812.016; wv) "
|
||
"AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/86.0.4240.99 "
|
||
"XWEB/4375 MMWEBSDK/20221011 Mobile Safari/537.36 MMWEBID/4357 "
|
||
"MicroMessenger/8.0.30.2244(0x28001E44) WeChat/arm64 Weixin GPVersion/1 "
|
||
"NetType/WIFI Language/zh_CN ABI/arm64 MiniProgramEnv/android"
|
||
),
|
||
},
|
||
)
|
||
return res.json()
|
||
except (HTTPError, json.decoder.JSONDecodeError) as e:
|
||
logger.opt(exception=e).error("提瓦特小助手接口无法访问或返回错误")
|
||
return {}
|
||
except Exception as e:
|
||
logger.opt(exception=e).error("提瓦特小助手接口错误")
|
||
return {}
|
||
|
||
|
||
async def getAvatarData(uid: str, char: str = "全部") -> Dict:
|
||
"""
|
||
角色数据获取(内部格式)
|
||
|
||
* ``param uid: str`` 查询用户 UID
|
||
* ``param char: str = "全部"`` 查询角色名
|
||
- ``return: Dict`` 查询结果。出错时返回 ``{"error": "错误信息"}``
|
||
"""
|
||
# 总是先读取一遍缓存
|
||
cache = LOCAL_DIR / "cache" / f"{uid}.json"
|
||
if cache.exists():
|
||
cacheData = json.loads(cache.read_text(encoding="utf-8"))
|
||
nextQueryTime = cacheData["next"]
|
||
else:
|
||
cacheData, nextQueryTime = {}, 0
|
||
|
||
if int(time()) <= nextQueryTime:
|
||
logger.info("UID{} 的角色展柜数据刷新冷却还有 {} 秒!".format(uid, nextQueryTime - int(time())))
|
||
else:
|
||
logger.info(f"UID{uid} 的角色展柜数据正在刷新!")
|
||
newData = await queryPanelApi(uid)
|
||
if not cacheData and newData.get("error"):
|
||
return newData
|
||
elif not newData.get("error"):
|
||
avatarsCache = {str(x["id"]): x for x in cacheData.get("avatars", [])}
|
||
now, wait4Dmg, avatars, avatarIdsNew = int(time()), {}, [], []
|
||
for newAvatar in newData["avatarInfoList"]:
|
||
if newAvatar["avatarId"] in [10000005, 10000007]:
|
||
logger.info("旅行者面板查询暂未支持!")
|
||
continue
|
||
tmp, gotDmg = await transFromEnka(newAvatar, now), False
|
||
|
||
if str(tmp["id"]) in avatarsCache:
|
||
# 保留旧的伤害计算数据
|
||
avatarsCache[str(tmp["id"])].pop("time")
|
||
cacheDmg = avatarsCache[str(tmp["id"])].pop("damage")
|
||
nowStat = {
|
||
k: v for k, v in tmp.items() if k not in ["damage", "time"]
|
||
}
|
||
if cacheDmg and avatarsCache[str(tmp["id"])] == nowStat:
|
||
logger.info(f"UID{uid} 的 {tmp['name']} 伤害计算结果无需刷新!")
|
||
tmp["damage"], gotDmg = cacheDmg, True
|
||
else:
|
||
logger.info(
|
||
"UID{} 的 {} 数据发生变化:\n{}\n{}".format(
|
||
uid, tmp["name"], avatarsCache[str(tmp["id"])], nowStat
|
||
)
|
||
)
|
||
avatarIdsNew.append(tmp["id"])
|
||
avatars.append(tmp)
|
||
if not gotDmg:
|
||
wait4Dmg[str(len(avatars) - 1)] = tmp
|
||
|
||
if wait4Dmg:
|
||
logger.info(
|
||
"正在为 UID{} 的 {} 重新请求伤害计算接口".format(
|
||
uid, "/".join(f"[{aI}]{a['name']}" for aI, a in wait4Dmg.items())
|
||
)
|
||
)
|
||
# 深拷贝避免转换对上下文中的 avatars 产生影响
|
||
wtf = deepcopy([a for _, a in wait4Dmg.items()])
|
||
teyvatBody = await transToTeyvat(wtf, uid)
|
||
teyvatRaw = await queryDamageApi(teyvatBody)
|
||
if teyvatRaw.get("code", "x") != 200 or len(wait4Dmg) != len(
|
||
teyvatRaw.get("result", [])
|
||
):
|
||
logger.error(
|
||
(
|
||
f"UID{uid} 的 {len(wait4Dmg)} 位角色伤害计算请求失败!"
|
||
f"\n>>>> [提瓦特返回] {teyvatRaw}"
|
||
)
|
||
)
|
||
else:
|
||
for dmgIdx, dmgData in enumerate(teyvatRaw.get("result", [])):
|
||
aIdx = int(list(wait4Dmg.keys())[dmgIdx])
|
||
avatars[aIdx]["damage"] = await simplDamageRes(dmgData)
|
||
|
||
cacheData["avatars"] = [
|
||
*avatars,
|
||
*[
|
||
aData
|
||
for _, aData in avatarsCache.items()
|
||
if aData["id"] not in avatarIdsNew
|
||
],
|
||
]
|
||
cacheData["next"] = now + newData["ttl"]
|
||
cache.write_text(
|
||
json.dumps(cacheData, ensure_ascii=False, indent=2), encoding="utf-8"
|
||
)
|
||
|
||
# 获取所需角色数据
|
||
if char == "全部":
|
||
return cacheData
|
||
searchRes = [x for x in cacheData["avatars"] if x["name"] == char]
|
||
return (
|
||
{
|
||
"error": "玩家 {} 游戏内展柜中的 {} 位角色中没有 {}!".format(
|
||
uid, len(cacheData["avatars"]), char
|
||
)
|
||
}
|
||
if not searchRes
|
||
else searchRes[0]
|
||
)
|
||
|
||
|
||
async def getPanel(uid: str, char: str = "全部") -> Union[bytes, str]:
|
||
"""
|
||
原神游戏内角色展柜消息生成入口
|
||
|
||
* ``param uid: str`` 查询用户 UID
|
||
* ``param char: str = "全部"`` 查询角色
|
||
- ``return: Union[bytes, str]`` 查询结果。一般返回图片字节,出错时返回错误信息字符串
|
||
"""
|
||
# 获取面板数据
|
||
data = await getAvatarData(uid, char)
|
||
if data.get("error"):
|
||
return data["error"]
|
||
if char == "全部":
|
||
return "成功获取了 UID{} 的{}等 {} 位角色数据!".format(
|
||
uid, "、".join(a["name"] for a in data["avatars"]), len(data["avatars"])
|
||
)
|
||
|
||
# 图标下载任务
|
||
dlTasks = [
|
||
download(data["icon"], local=char),
|
||
download(data["gachaAvatarImg"], local=char),
|
||
*[download(sData["icon"], local=char) for _, sData in data["skills"].items()],
|
||
*[download(conData["icon"], local=char) for conData in data["consts"]],
|
||
download(data["weapon"]["icon"], local="weapon"),
|
||
*[download(relicData["icon"], local="artifacts") for relicData in data["relics"]],
|
||
]
|
||
await asyncio.gather(*dlTasks)
|
||
dlTasks.clear()
|
||
|
||
# 渲染截图
|
||
data["fightProp"] = await simplFightProp(
|
||
data["fightProp"], data["baseProp"], char, data["element"]
|
||
)
|
||
htmlBase = str(LOCAL_DIR.resolve())
|
||
return await template_to_pic(
|
||
template_path=htmlBase,
|
||
template_name=f"panel-{TPL_VERSION}.html",
|
||
templates={"css": TPL_VERSION, "uid": uid, "data": data},
|
||
pages={
|
||
"device_scale_factor": SCALE_FACTOR,
|
||
"viewport": {"width": 600, "height": 300},
|
||
"base_url": f"file://{htmlBase}",
|
||
},
|
||
wait=2,
|
||
)
|
||
|
||
|
||
async def getTeam(uid: str, chars: List[str] = []) -> Union[bytes, str]:
|
||
"""
|
||
队伍伤害消息生成入口
|
||
|
||
* ``param uid: str`` 查询用户 UID
|
||
* ``param chars: List[str] = []`` 查询角色,为空默认数据中前四个
|
||
- ``return: Union[bytes, str]`` 查询结果。一般返回图片字节,出错时返回错误信息字符串
|
||
"""
|
||
# 获取面板数据
|
||
data = await getAvatarData(uid, "全部")
|
||
if data.get("error"):
|
||
return data["error"]
|
||
|
||
if chars:
|
||
extract = [a for a in data["avatars"] if a["name"] in chars]
|
||
if len(extract) != len(chars):
|
||
gotThis = [a["name"] for a in extract]
|
||
return "玩家 {} 的最新数据中未发现{}!".format(
|
||
uid, "、".join(c for c in chars if c not in gotThis)
|
||
)
|
||
elif len(data["avatars"]) >= 4:
|
||
extract = data["avatars"][:4]
|
||
logger.info(
|
||
"UID{} 未指定队伍,自动选择面板中前 4 位进行计算:{} ...".format(
|
||
uid, "、".join(a["name"] for a in extract)
|
||
)
|
||
)
|
||
else:
|
||
return f"玩家 {uid} 的面板数据甚至不足以组成一支队伍呢!"
|
||
|
||
# 图片下载任务
|
||
for tmp in extract:
|
||
dlTasks = [
|
||
download(tmp["icon"], local=tmp["name"]),
|
||
*[
|
||
download(sData["icon"], local=tmp["name"])
|
||
for _, sData in tmp["skills"].items()
|
||
],
|
||
download(tmp["weapon"]["icon"], local="weapon"),
|
||
*[
|
||
download(
|
||
f"UI_RelicIcon_{relicData['icon'].split('_')[-2]}_4",
|
||
local="artifacts",
|
||
)
|
||
for relicData in tmp["relics"]
|
||
],
|
||
]
|
||
await asyncio.gather(*dlTasks)
|
||
dlTasks.clear()
|
||
|
||
teyvatBody = await transToTeyvat(deepcopy(extract), uid)
|
||
teyvatRaw = await queryDamageApi(teyvatBody, "team")
|
||
if teyvatRaw.get("code", "x") != 200 or not teyvatRaw.get("result"):
|
||
logger.error(
|
||
(f"UID{uid} 的 {len(extract)} 位角色队伍伤害计算请求失败!" f"\n>>>> [提瓦特返回] {teyvatRaw}")
|
||
)
|
||
return f"玩家 {uid} 队伍伤害计算失败,接口可能发生变动!" if teyvatRaw else "啊哦,队伍伤害计算小程序状态异常!"
|
||
try:
|
||
data = await simplTeamDamageRes(
|
||
teyvatRaw["result"], {a["name"]: a for a in extract}
|
||
)
|
||
except Exception as e:
|
||
logger.opt(exception=e).error("队伍伤害数据解析出错")
|
||
return f"[{e.__class__.__name__}] 队伍伤害数据解析出错咯"
|
||
|
||
htmlBase = str(LOCAL_DIR.resolve())
|
||
return await template_to_pic(
|
||
template_path=htmlBase,
|
||
template_name=f"team-{TPL_VERSION}.html",
|
||
templates={"css": TPL_VERSION, "data": data},
|
||
pages={
|
||
"device_scale_factor": SCALE_FACTOR,
|
||
"viewport": {"width": 600, "height": 300},
|
||
"base_url": f"file://{htmlBase}",
|
||
},
|
||
wait=2,
|
||
)
|