Enka_Panel_Bot/gspanel/data_source.py
2023-01-14 21:59:43 +08:00

343 lines
14 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 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,
)