2023-01-20 15:42:57 +00:00
|
|
|
|
import math
|
2023-03-17 05:33:29 +00:00
|
|
|
|
from typing import Any, List, Tuple, Union, Optional, TYPE_CHECKING
|
2022-09-09 14:50:17 +00:00
|
|
|
|
|
|
|
|
|
from enkanetwork import (
|
2022-09-10 14:46:49 +00:00
|
|
|
|
DigitType,
|
2022-09-09 14:50:17 +00:00
|
|
|
|
EnkaNetworkAPI,
|
2022-09-10 14:46:49 +00:00
|
|
|
|
EnkaNetworkResponse,
|
|
|
|
|
EnkaServerError,
|
2022-09-09 14:50:17 +00:00
|
|
|
|
Equipments,
|
2022-09-10 14:46:49 +00:00
|
|
|
|
EquipmentsType,
|
|
|
|
|
HTTPException,
|
2022-09-09 14:50:17 +00:00
|
|
|
|
Stats,
|
2022-09-10 14:46:49 +00:00
|
|
|
|
StatsPercentage,
|
|
|
|
|
VaildateUIDError,
|
2023-02-08 14:56:25 +00:00
|
|
|
|
EnkaServerMaintanance,
|
|
|
|
|
EnkaServerUnknown,
|
|
|
|
|
EnkaServerRateLimit,
|
2023-02-09 05:57:16 +00:00
|
|
|
|
EnkaPlayerNotFound,
|
2022-09-10 14:46:49 +00:00
|
|
|
|
)
|
2022-09-09 14:50:17 +00:00
|
|
|
|
from pydantic import BaseModel
|
2023-03-17 05:33:29 +00:00
|
|
|
|
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
|
2022-09-09 14:50:17 +00:00
|
|
|
|
from telegram.constants import ChatAction
|
2023-03-17 05:33:29 +00:00
|
|
|
|
from telegram.ext import CommandHandler, MessageHandler, filters
|
2022-11-28 08:38:49 +00:00
|
|
|
|
from telegram.helpers import create_deep_linked_url
|
2022-09-09 14:50:17 +00:00
|
|
|
|
|
2022-10-07 05:55:50 +00:00
|
|
|
|
from core.config import config
|
2023-03-17 05:33:29 +00:00
|
|
|
|
from core.dependence.assets import DEFAULT_EnkaAssets, AssetsService
|
2023-03-14 01:27:22 +00:00
|
|
|
|
from core.dependence.redisdb import RedisDB
|
2023-03-16 15:00:04 +00:00
|
|
|
|
from core.handler.callbackqueryhandler import CallbackQueryHandler
|
2022-09-09 14:50:17 +00:00
|
|
|
|
from core.plugin import Plugin, handler
|
2023-03-14 01:27:22 +00:00
|
|
|
|
from core.services.players import PlayersService
|
|
|
|
|
from core.services.template.services import TemplateService
|
2022-09-09 16:27:56 +00:00
|
|
|
|
from metadata.shortname import roleToName
|
2023-01-19 12:54:29 +00:00
|
|
|
|
from modules.playercards.file import PlayerCardsFile
|
2022-12-11 07:00:54 +00:00
|
|
|
|
from modules.playercards.helpers import ArtifactStatsTheory
|
2022-12-27 13:56:12 +00:00
|
|
|
|
from utils.enkanetwork import RedisCache
|
2023-03-14 01:27:22 +00:00
|
|
|
|
from utils.helpers import download_resource
|
2022-09-09 14:50:17 +00:00
|
|
|
|
from utils.log import logger
|
2022-09-27 15:52:32 +00:00
|
|
|
|
from utils.patch.aiohttp import AioHttpTimeoutException
|
2023-08-26 10:19:00 +00:00
|
|
|
|
from utils.uid import mask_number
|
2022-09-09 14:50:17 +00:00
|
|
|
|
|
2023-03-17 05:33:29 +00:00
|
|
|
|
if TYPE_CHECKING:
|
|
|
|
|
from enkanetwork import CharacterInfo, EquipmentsStats
|
|
|
|
|
from telegram.ext import ContextTypes
|
|
|
|
|
from telegram import Update
|
|
|
|
|
|
2023-03-14 01:27:22 +00:00
|
|
|
|
try:
|
|
|
|
|
import ujson as jsonlib
|
|
|
|
|
except ImportError:
|
|
|
|
|
import json as jsonlib
|
2022-09-09 14:50:17 +00:00
|
|
|
|
|
2023-03-14 01:27:22 +00:00
|
|
|
|
|
|
|
|
|
class PlayerCards(Plugin):
|
2023-03-17 05:33:29 +00:00
|
|
|
|
def __init__(
|
|
|
|
|
self,
|
|
|
|
|
player_service: PlayersService,
|
|
|
|
|
template_service: TemplateService,
|
|
|
|
|
assets_service: AssetsService,
|
|
|
|
|
redis: RedisDB,
|
|
|
|
|
):
|
2023-03-14 01:27:22 +00:00
|
|
|
|
self.player_service = player_service
|
2023-02-08 14:56:25 +00:00
|
|
|
|
self.client = EnkaNetworkAPI(lang="chs", user_agent=config.enka_network_api_agent, cache=False)
|
2023-03-17 05:33:29 +00:00
|
|
|
|
self.cache = RedisCache(redis.client, key="plugin:player_cards:enka_network", ex=60)
|
2023-01-19 12:54:29 +00:00
|
|
|
|
self.player_cards_file = PlayerCardsFile()
|
2023-03-17 05:33:29 +00:00
|
|
|
|
self.assets_service = assets_service
|
2022-09-09 14:50:17 +00:00
|
|
|
|
self.template_service = template_service
|
2023-03-17 05:33:29 +00:00
|
|
|
|
self.kitsune: Optional[str] = None
|
2022-09-09 16:27:56 +00:00
|
|
|
|
|
2023-03-17 05:33:29 +00:00
|
|
|
|
async def _update_enka_data(self, uid) -> Union[EnkaNetworkResponse, str]:
|
2022-09-09 16:27:56 +00:00
|
|
|
|
try:
|
2023-01-19 12:54:29 +00:00
|
|
|
|
data = await self.cache.get(uid)
|
|
|
|
|
if data is not None:
|
|
|
|
|
return EnkaNetworkResponse.parse_obj(data)
|
2023-02-08 14:56:25 +00:00
|
|
|
|
user = await self.client.http.fetch_user_by_uid(uid)
|
2023-01-19 12:54:29 +00:00
|
|
|
|
data = user["content"].decode("utf-8", "surrogatepass") # type: ignore
|
2023-03-14 01:27:22 +00:00
|
|
|
|
data = jsonlib.loads(data)
|
2023-01-19 12:54:29 +00:00
|
|
|
|
data = await self.player_cards_file.merge_info(uid, data)
|
|
|
|
|
await self.cache.set(uid, data)
|
|
|
|
|
return EnkaNetworkResponse.parse_obj(data)
|
2022-09-27 15:52:32 +00:00
|
|
|
|
except AioHttpTimeoutException:
|
2023-01-19 12:54:29 +00:00
|
|
|
|
error = "Enka.Network 服务请求超时,请稍后重试"
|
2023-02-08 14:56:25 +00:00
|
|
|
|
except EnkaServerRateLimit:
|
|
|
|
|
error = "Enka.Network 已对此API进行速率限制,请稍后重试"
|
|
|
|
|
except EnkaServerMaintanance:
|
|
|
|
|
error = "Enka.Network 正在维护,请等待5-8小时或1天"
|
|
|
|
|
except EnkaServerError:
|
|
|
|
|
error = "Enka.Network 服务请求错误,请稍后重试"
|
|
|
|
|
except EnkaServerUnknown:
|
|
|
|
|
error = "Enka.Network 服务瞬间爆炸,请稍后重试"
|
2023-02-09 05:57:16 +00:00
|
|
|
|
except EnkaPlayerNotFound:
|
2023-02-08 14:56:25 +00:00
|
|
|
|
error = "UID 未找到,可能为服务器抽风,请稍后重试"
|
2023-02-09 05:57:16 +00:00
|
|
|
|
except VaildateUIDError:
|
|
|
|
|
error = "未找到玩家,请检查您的UID/用户名"
|
2022-09-09 16:27:56 +00:00
|
|
|
|
except HTTPException:
|
2023-01-19 12:54:29 +00:00
|
|
|
|
error = "Enka.Network HTTP 服务请求错误,请稍后重试"
|
|
|
|
|
return error
|
2022-09-09 14:50:17 +00:00
|
|
|
|
|
2023-03-17 05:33:29 +00:00
|
|
|
|
async def _load_history(self, uid) -> Optional[EnkaNetworkResponse]:
|
|
|
|
|
data = await self.player_cards_file.load_history_info(uid)
|
|
|
|
|
if data is None:
|
|
|
|
|
return None
|
|
|
|
|
return EnkaNetworkResponse.parse_obj(data)
|
|
|
|
|
|
2022-09-09 14:50:17 +00:00
|
|
|
|
@handler(CommandHandler, command="player_card", block=False)
|
|
|
|
|
@handler(MessageHandler, filters=filters.Regex("^角色卡片查询(.*)"), block=False)
|
2023-03-17 05:33:29 +00:00
|
|
|
|
async def player_cards(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> None:
|
2022-09-09 14:50:17 +00:00
|
|
|
|
user = update.effective_user
|
|
|
|
|
message = update.effective_message
|
2023-03-14 01:27:22 +00:00
|
|
|
|
args = self.get_args(context)
|
2022-09-09 14:50:17 +00:00
|
|
|
|
await message.reply_chat_action(ChatAction.TYPING)
|
2023-03-14 01:27:22 +00:00
|
|
|
|
player_info = await self.player_service.get_player(user.id)
|
|
|
|
|
if player_info is None:
|
2023-03-17 05:33:29 +00:00
|
|
|
|
buttons = [
|
|
|
|
|
[
|
|
|
|
|
InlineKeyboardButton(
|
|
|
|
|
"点我绑定账号",
|
|
|
|
|
url=create_deep_linked_url(context.bot.username, "set_uid"),
|
|
|
|
|
)
|
|
|
|
|
]
|
|
|
|
|
]
|
2022-09-09 14:50:17 +00:00
|
|
|
|
if filters.ChatType.GROUPS.filter(message):
|
2022-10-12 09:35:59 +00:00
|
|
|
|
reply_message = await message.reply_text(
|
2023-03-17 05:33:29 +00:00
|
|
|
|
"未查询到您所绑定的账号信息,请先私聊派蒙绑定账号",
|
|
|
|
|
reply_markup=InlineKeyboardMarkup(buttons),
|
2022-10-12 09:35:59 +00:00
|
|
|
|
)
|
2023-03-14 01:27:22 +00:00
|
|
|
|
self.add_delete_message_job(reply_message, delay=30)
|
2022-10-12 09:35:59 +00:00
|
|
|
|
|
2023-03-14 01:27:22 +00:00
|
|
|
|
self.add_delete_message_job(message, delay=30)
|
2022-10-12 09:35:59 +00:00
|
|
|
|
else:
|
2022-10-22 13:54:04 +00:00
|
|
|
|
await message.reply_text("未查询到您所绑定的账号信息,请先绑定账号", reply_markup=InlineKeyboardMarkup(buttons))
|
2022-09-09 14:50:17 +00:00
|
|
|
|
return
|
2023-03-17 05:33:29 +00:00
|
|
|
|
data = await self._load_history(player_info.player_id)
|
|
|
|
|
if data is None:
|
|
|
|
|
if isinstance(self.kitsune, str):
|
|
|
|
|
photo = self.kitsune
|
|
|
|
|
else:
|
|
|
|
|
photo = open("resources/img/kitsune.png", "rb")
|
|
|
|
|
buttons = [
|
|
|
|
|
[
|
|
|
|
|
InlineKeyboardButton(
|
|
|
|
|
"更新面板",
|
|
|
|
|
callback_data=f"update_player_card|{user.id}|{player_info.player_id}",
|
|
|
|
|
)
|
|
|
|
|
]
|
|
|
|
|
]
|
|
|
|
|
reply_message = await message.reply_photo(
|
|
|
|
|
photo=photo,
|
|
|
|
|
caption="角色列表未找到,请尝试点击下方按钮从 EnkaNetwork 更新角色列表",
|
|
|
|
|
reply_markup=InlineKeyboardMarkup(buttons),
|
|
|
|
|
)
|
|
|
|
|
if reply_message.photo:
|
|
|
|
|
self.kitsune = reply_message.photo[-1].file_id
|
2022-09-09 14:50:17 +00:00
|
|
|
|
return
|
2022-09-09 15:54:52 +00:00
|
|
|
|
if len(args) == 1:
|
2022-09-09 16:27:56 +00:00
|
|
|
|
character_name = roleToName(args[0])
|
2023-03-14 01:27:22 +00:00
|
|
|
|
logger.info(
|
|
|
|
|
"用户 %s[%s] 角色卡片查询命令请求 || character_name[%s] uid[%s]",
|
|
|
|
|
user.full_name,
|
|
|
|
|
user.id,
|
|
|
|
|
character_name,
|
|
|
|
|
player_info.player_id,
|
|
|
|
|
)
|
2022-09-09 15:54:52 +00:00
|
|
|
|
else:
|
2023-03-14 01:27:22 +00:00
|
|
|
|
logger.info("用户 %s[%s] 角色卡片查询命令请求", user.full_name, user.id)
|
2023-03-17 05:33:29 +00:00
|
|
|
|
ttl = await self.cache.ttl(player_info.player_id)
|
2023-07-09 16:42:36 +00:00
|
|
|
|
if data.characters is None or len(data.characters) == 0:
|
|
|
|
|
buttons = [
|
|
|
|
|
[
|
|
|
|
|
InlineKeyboardButton(
|
|
|
|
|
"更新面板",
|
2023-07-28 17:00:20 +00:00
|
|
|
|
callback_data=f"update_player_card|{user.id}|{player_info.player_id}",
|
2023-07-09 16:42:36 +00:00
|
|
|
|
)
|
|
|
|
|
]
|
|
|
|
|
]
|
|
|
|
|
else:
|
|
|
|
|
buttons = self.gen_button(data, user.id, player_info.player_id, update_button=ttl < 0)
|
2023-03-17 05:33:29 +00:00
|
|
|
|
if isinstance(self.kitsune, str):
|
|
|
|
|
photo = self.kitsune
|
2022-10-19 07:14:41 +00:00
|
|
|
|
else:
|
|
|
|
|
photo = open("resources/img/kitsune.png", "rb")
|
2022-10-10 03:37:58 +00:00
|
|
|
|
reply_message = await message.reply_photo(
|
2023-03-17 05:33:29 +00:00
|
|
|
|
photo=photo,
|
|
|
|
|
caption="请选择你要查询的角色",
|
|
|
|
|
reply_markup=InlineKeyboardMarkup(buttons),
|
2022-10-10 03:37:58 +00:00
|
|
|
|
)
|
2022-09-09 16:27:56 +00:00
|
|
|
|
if reply_message.photo:
|
2023-03-17 05:33:29 +00:00
|
|
|
|
self.kitsune = reply_message.photo[-1].file_id
|
2022-09-09 15:54:52 +00:00
|
|
|
|
return
|
|
|
|
|
for characters in data.characters:
|
|
|
|
|
if characters.name == character_name:
|
|
|
|
|
break
|
|
|
|
|
else:
|
2022-10-22 13:54:04 +00:00
|
|
|
|
await message.reply_text(f"角色展柜中未找到 {character_name} ,请检查角色是否存在于角色展柜中,或者等待角色数据更新后重试")
|
2022-09-09 14:50:17 +00:00
|
|
|
|
return
|
|
|
|
|
await message.reply_chat_action(ChatAction.UPLOAD_PHOTO)
|
2023-03-14 01:27:22 +00:00
|
|
|
|
render_result = await RenderTemplate(
|
|
|
|
|
player_info.player_id, characters, self.template_service
|
|
|
|
|
).render() # pylint: disable=W0631
|
2023-03-17 05:33:29 +00:00
|
|
|
|
await render_result.reply_photo(
|
|
|
|
|
message,
|
|
|
|
|
filename=f"player_card_{player_info.player_id}_{character_name}.png",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@handler(CallbackQueryHandler, pattern=r"^update_player_card\|", block=False)
|
|
|
|
|
async def update_player_card(self, update: "Update", _: "ContextTypes.DEFAULT_TYPE") -> None:
|
|
|
|
|
user = update.effective_user
|
|
|
|
|
message = update.effective_message
|
|
|
|
|
callback_query = update.callback_query
|
|
|
|
|
|
|
|
|
|
async def get_player_card_callback(callback_query_data: str) -> Tuple[int, int]:
|
|
|
|
|
_data = callback_query_data.split("|")
|
|
|
|
|
_user_id = int(_data[1])
|
|
|
|
|
_uid = int(_data[2])
|
|
|
|
|
logger.debug("callback_query_data函数返回 user_id[%s] uid[%s]", _user_id, _uid)
|
|
|
|
|
return _user_id, _uid
|
|
|
|
|
|
|
|
|
|
user_id, uid = await get_player_card_callback(callback_query.data)
|
|
|
|
|
if user.id != user_id:
|
|
|
|
|
await callback_query.answer(text="这不是你的按钮!\n" + config.notice.user_mismatch, show_alert=True)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
ttl = await self.cache.ttl(uid)
|
|
|
|
|
|
|
|
|
|
if ttl > 0:
|
|
|
|
|
await callback_query.answer(text=f"请等待 {ttl} 秒后再更新", show_alert=True)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
await message.reply_chat_action(ChatAction.TYPING)
|
|
|
|
|
await callback_query.answer(text="正在从 EnkaNetwork 获取角色列表 请不要重复点击按钮")
|
|
|
|
|
data = await self._update_enka_data(uid)
|
|
|
|
|
if isinstance(data, str):
|
|
|
|
|
await callback_query.answer(text=data, show_alert=True)
|
|
|
|
|
return
|
2023-07-09 16:42:36 +00:00
|
|
|
|
if data.characters is None or len(data.characters) == 0:
|
2023-03-18 14:03:56 +00:00
|
|
|
|
await callback_query.answer("请先将角色加入到角色展柜并允许查看角色详情后再使用此功能,如果已经添加了角色,请等待角色数据更新后重试", show_alert=True)
|
2023-07-08 06:02:09 +00:00
|
|
|
|
await message.delete()
|
2023-03-18 14:03:56 +00:00
|
|
|
|
return
|
2023-03-17 05:33:29 +00:00
|
|
|
|
buttons = self.gen_button(data, user.id, uid, update_button=False)
|
|
|
|
|
render_data = await self.parse_holder_data(data)
|
|
|
|
|
holder = await self.template_service.render(
|
2023-05-09 11:01:45 +00:00
|
|
|
|
"genshin/player_card/holder.jinja2",
|
2023-03-17 05:33:29 +00:00
|
|
|
|
render_data,
|
|
|
|
|
viewport={"width": 750, "height": 580},
|
|
|
|
|
ttl=60 * 10,
|
|
|
|
|
caption="更新角色列表成功,请选择你要查询的角色",
|
|
|
|
|
)
|
|
|
|
|
await holder.edit_media(message, reply_markup=InlineKeyboardMarkup(buttons))
|
2022-09-09 14:50:17 +00:00
|
|
|
|
|
2022-09-09 15:54:52 +00:00
|
|
|
|
@handler(CallbackQueryHandler, pattern=r"^get_player_card\|", block=False)
|
2023-03-17 05:33:29 +00:00
|
|
|
|
async def get_player_cards(self, update: "Update", _: "ContextTypes.DEFAULT_TYPE") -> None:
|
2022-09-09 15:54:52 +00:00
|
|
|
|
callback_query = update.callback_query
|
|
|
|
|
user = callback_query.from_user
|
|
|
|
|
message = callback_query.message
|
|
|
|
|
|
2023-03-17 05:33:29 +00:00
|
|
|
|
async def get_player_card_callback(
|
|
|
|
|
callback_query_data: str,
|
|
|
|
|
) -> Tuple[str, int, int]:
|
2022-09-09 15:54:52 +00:00
|
|
|
|
_data = callback_query_data.split("|")
|
|
|
|
|
_user_id = int(_data[1])
|
|
|
|
|
_uid = int(_data[2])
|
|
|
|
|
_result = _data[3]
|
2023-03-17 05:33:29 +00:00
|
|
|
|
logger.debug(
|
|
|
|
|
"callback_query_data函数返回 result[%s] user_id[%s] uid[%s]",
|
|
|
|
|
_result,
|
|
|
|
|
_user_id,
|
|
|
|
|
_uid,
|
|
|
|
|
)
|
2022-09-09 15:54:52 +00:00
|
|
|
|
return _result, _user_id, _uid
|
|
|
|
|
|
|
|
|
|
result, user_id, uid = await get_player_card_callback(callback_query.data)
|
|
|
|
|
if user.id != user_id:
|
2023-01-07 20:01:28 +00:00
|
|
|
|
await callback_query.answer(text="这不是你的按钮!\n" + config.notice.user_mismatch, show_alert=True)
|
2022-09-09 15:54:52 +00:00
|
|
|
|
return
|
2023-01-20 15:42:57 +00:00
|
|
|
|
if result == "empty_data":
|
|
|
|
|
await callback_query.answer(text="此按钮不可用", show_alert=True)
|
|
|
|
|
return
|
|
|
|
|
page = 0
|
|
|
|
|
if result.isdigit():
|
|
|
|
|
page = int(result)
|
2023-03-17 05:33:29 +00:00
|
|
|
|
logger.info(
|
|
|
|
|
"用户 %s[%s] 角色卡片查询命令请求 || page[%s] uid[%s]",
|
|
|
|
|
user.full_name,
|
|
|
|
|
user.id,
|
|
|
|
|
page,
|
|
|
|
|
uid,
|
|
|
|
|
)
|
2023-01-20 15:42:57 +00:00
|
|
|
|
else:
|
2023-03-17 05:33:29 +00:00
|
|
|
|
logger.info(
|
|
|
|
|
"用户 %s[%s] 角色卡片查询命令请求 || character_name[%s] uid[%s]",
|
|
|
|
|
user.full_name,
|
|
|
|
|
user.id,
|
|
|
|
|
result,
|
|
|
|
|
uid,
|
|
|
|
|
)
|
|
|
|
|
data = await self._load_history(uid)
|
2022-09-09 16:27:56 +00:00
|
|
|
|
if isinstance(data, str):
|
|
|
|
|
await message.reply_text(data)
|
2022-09-09 15:54:52 +00:00
|
|
|
|
return
|
2023-07-09 16:42:36 +00:00
|
|
|
|
if data.characters is None or len(data.characters) == 0:
|
2022-11-14 13:56:02 +00:00
|
|
|
|
await callback_query.answer("请先将角色加入到角色展柜并允许查看角色详情后再使用此功能,如果已经添加了角色,请等待角色数据更新后重试", show_alert=True)
|
2023-07-08 06:02:09 +00:00
|
|
|
|
await message.delete()
|
2022-09-09 15:54:52 +00:00
|
|
|
|
return
|
2023-01-20 15:42:57 +00:00
|
|
|
|
if page:
|
2023-03-17 05:33:29 +00:00
|
|
|
|
buttons = self.gen_button(data, user.id, uid, page, not await self.cache.ttl(uid) > 0)
|
2023-01-20 15:42:57 +00:00
|
|
|
|
await message.edit_reply_markup(reply_markup=InlineKeyboardMarkup(buttons))
|
|
|
|
|
await callback_query.answer(f"已切换到第 {page} 页", show_alert=False)
|
|
|
|
|
return
|
2022-09-09 15:54:52 +00:00
|
|
|
|
for characters in data.characters:
|
|
|
|
|
if characters.name == result:
|
|
|
|
|
break
|
|
|
|
|
else:
|
2022-11-14 13:56:02 +00:00
|
|
|
|
await message.delete()
|
|
|
|
|
await callback_query.answer(f"角色展柜中未找到 {result} ,请检查角色是否存在于角色展柜中,或者等待角色数据更新后重试", show_alert=True)
|
2022-09-09 15:54:52 +00:00
|
|
|
|
return
|
2022-09-10 12:56:22 +00:00
|
|
|
|
await callback_query.answer(text="正在渲染图片中 请稍等 请不要重复点击按钮", show_alert=False)
|
2022-09-09 15:54:52 +00:00
|
|
|
|
await message.reply_chat_action(ChatAction.UPLOAD_PHOTO)
|
2022-10-22 07:03:59 +00:00
|
|
|
|
render_result = await RenderTemplate(uid, characters, self.template_service).render() # pylint: disable=W0631
|
|
|
|
|
render_result.filename = f"player_card_{uid}_{result}.png"
|
|
|
|
|
await render_result.edit_media(message)
|
2022-09-09 15:54:52 +00:00
|
|
|
|
|
2023-01-20 15:42:57 +00:00
|
|
|
|
@staticmethod
|
|
|
|
|
def gen_button(
|
|
|
|
|
data: EnkaNetworkResponse,
|
|
|
|
|
user_id: Union[str, int],
|
|
|
|
|
uid: int,
|
|
|
|
|
page: int = 1,
|
2023-03-17 05:33:29 +00:00
|
|
|
|
update_button: bool = True,
|
2023-01-20 15:42:57 +00:00
|
|
|
|
) -> List[List[InlineKeyboardButton]]:
|
|
|
|
|
"""生成按钮"""
|
2023-03-18 14:03:56 +00:00
|
|
|
|
buttons = []
|
|
|
|
|
if data.characters:
|
|
|
|
|
buttons = [
|
|
|
|
|
InlineKeyboardButton(
|
|
|
|
|
value.name,
|
|
|
|
|
callback_data=f"get_player_card|{user_id}|{uid}|{value.name}",
|
|
|
|
|
)
|
|
|
|
|
for value in data.characters
|
|
|
|
|
if value.name
|
|
|
|
|
]
|
2023-01-20 15:42:57 +00:00
|
|
|
|
all_buttons = [buttons[i : i + 4] for i in range(0, len(buttons), 4)]
|
|
|
|
|
send_buttons = all_buttons[(page - 1) * 3 : page * 3]
|
|
|
|
|
last_page = page - 1 if page > 1 else 0
|
|
|
|
|
all_page = math.ceil(len(all_buttons) / 3)
|
|
|
|
|
next_page = page + 1 if page < all_page and all_page > 1 else 0
|
|
|
|
|
last_button = []
|
|
|
|
|
if last_page:
|
|
|
|
|
last_button.append(
|
|
|
|
|
InlineKeyboardButton(
|
|
|
|
|
"<< 上一页",
|
|
|
|
|
callback_data=f"get_player_card|{user_id}|{uid}|{last_page}",
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
if last_page or next_page:
|
|
|
|
|
last_button.append(
|
|
|
|
|
InlineKeyboardButton(
|
|
|
|
|
f"{page}/{all_page}",
|
|
|
|
|
callback_data=f"get_player_card|{user_id}|{uid}|empty_data",
|
|
|
|
|
)
|
|
|
|
|
)
|
2023-03-17 05:33:29 +00:00
|
|
|
|
if update_button:
|
|
|
|
|
last_button.append(
|
|
|
|
|
InlineKeyboardButton(
|
|
|
|
|
"更新面板",
|
|
|
|
|
callback_data=f"update_player_card|{user_id}|{uid}",
|
|
|
|
|
)
|
|
|
|
|
)
|
2023-01-20 15:42:57 +00:00
|
|
|
|
if next_page:
|
|
|
|
|
last_button.append(
|
|
|
|
|
InlineKeyboardButton(
|
|
|
|
|
"下一页 >>",
|
|
|
|
|
callback_data=f"get_player_card|{user_id}|{uid}|{next_page}",
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
if last_button:
|
|
|
|
|
send_buttons.append(last_button)
|
|
|
|
|
return send_buttons
|
|
|
|
|
|
2023-03-17 05:33:29 +00:00
|
|
|
|
async def parse_holder_data(self, data: EnkaNetworkResponse) -> dict:
|
|
|
|
|
"""
|
|
|
|
|
生成渲染所需数据
|
|
|
|
|
"""
|
|
|
|
|
characters_data = []
|
|
|
|
|
for idx, character in enumerate(data.characters):
|
|
|
|
|
characters_data.append(
|
|
|
|
|
{
|
|
|
|
|
"level": character.level,
|
|
|
|
|
"element": character.element.name,
|
|
|
|
|
"constellation": character.constellations_unlocked,
|
|
|
|
|
"rarity": character.rarity,
|
|
|
|
|
"icon": (await self.assets_service.avatar(character.id).icon()).as_uri(),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
if idx > 6:
|
|
|
|
|
break
|
|
|
|
|
return {
|
|
|
|
|
"uid": data.uid,
|
|
|
|
|
"level": data.player.level,
|
|
|
|
|
"signature": data.player.signature,
|
|
|
|
|
"characters": characters_data,
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-09 14:50:17 +00:00
|
|
|
|
|
|
|
|
|
class Artifact(BaseModel):
|
|
|
|
|
"""在 enka Equipments model 基础上扩展了圣遗物评分数据"""
|
|
|
|
|
|
|
|
|
|
equipment: Equipments
|
|
|
|
|
# 圣遗物评分
|
|
|
|
|
score: float = 0
|
|
|
|
|
# 圣遗物评级
|
|
|
|
|
score_label: str = "E"
|
|
|
|
|
# 圣遗物评级颜色
|
|
|
|
|
score_class: str = ""
|
|
|
|
|
# 圣遗物单行属性评分
|
|
|
|
|
substat_scores: List[float]
|
|
|
|
|
|
|
|
|
|
def __init__(self, **kwargs):
|
|
|
|
|
super().__init__(**kwargs)
|
|
|
|
|
for substat_scores in self.substat_scores:
|
|
|
|
|
self.score += substat_scores
|
|
|
|
|
self.score = round(self.score, 1)
|
|
|
|
|
|
2022-10-10 03:37:58 +00:00
|
|
|
|
for r in (
|
2022-10-22 07:03:59 +00:00
|
|
|
|
("D", 10),
|
|
|
|
|
("C", 16.5),
|
|
|
|
|
("B", 23.1),
|
|
|
|
|
("A", 29.7),
|
|
|
|
|
("S", 36.3),
|
|
|
|
|
("SS", 42.9),
|
|
|
|
|
("SSS", 49.5),
|
|
|
|
|
("ACE", 56.1),
|
|
|
|
|
("ACE²", 66),
|
2022-10-10 03:37:58 +00:00
|
|
|
|
):
|
2022-09-09 14:50:17 +00:00
|
|
|
|
if self.score >= r[1]:
|
|
|
|
|
self.score_label = r[0]
|
|
|
|
|
self.score_class = self.get_score_class(r[0])
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def get_score_class(label: str) -> str:
|
|
|
|
|
mapping = {
|
|
|
|
|
"D": "text-neutral-400",
|
|
|
|
|
"C": "text-neutral-200",
|
2022-10-09 04:52:39 +00:00
|
|
|
|
"B": "text-violet-400",
|
2022-09-09 14:50:17 +00:00
|
|
|
|
"A": "text-violet-400",
|
2022-10-09 04:52:39 +00:00
|
|
|
|
"S": "text-yellow-400",
|
2022-09-09 14:50:17 +00:00
|
|
|
|
"SS": "text-yellow-400",
|
|
|
|
|
"SSS": "text-yellow-400",
|
|
|
|
|
"ACE": "text-red-500",
|
|
|
|
|
"ACE²": "text-red-500",
|
|
|
|
|
}
|
|
|
|
|
return mapping.get(label, "text-neutral-400")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RenderTemplate:
|
2023-03-17 05:33:29 +00:00
|
|
|
|
def __init__(
|
|
|
|
|
self,
|
|
|
|
|
uid: Union[int, str],
|
|
|
|
|
character: "CharacterInfo",
|
|
|
|
|
template_service: TemplateService = None,
|
|
|
|
|
):
|
2022-09-09 14:50:17 +00:00
|
|
|
|
self.uid = uid
|
|
|
|
|
self.template_service = template_service
|
|
|
|
|
# 因为需要替换线上 enka 图片地址为本地地址,先克隆数据,避免修改原数据
|
|
|
|
|
self.character = character.copy(deep=True)
|
|
|
|
|
|
|
|
|
|
async def render(self):
|
|
|
|
|
# 缓存所有图片到本地
|
|
|
|
|
await self.cache_images()
|
|
|
|
|
|
|
|
|
|
artifacts = self.find_artifacts()
|
|
|
|
|
artifact_total_score: float = sum(artifact.score for artifact in artifacts)
|
|
|
|
|
|
|
|
|
|
artifact_total_score = round(artifact_total_score, 1)
|
|
|
|
|
|
|
|
|
|
artifact_total_score_label: str = "E"
|
2022-10-10 03:37:58 +00:00
|
|
|
|
for r in (
|
2022-10-22 07:03:59 +00:00
|
|
|
|
("D", 10),
|
|
|
|
|
("C", 16.5),
|
|
|
|
|
("B", 23.1),
|
|
|
|
|
("A", 29.7),
|
|
|
|
|
("S", 36.3),
|
|
|
|
|
("SS", 42.9),
|
|
|
|
|
("SSS", 49.5),
|
|
|
|
|
("ACE", 56.1),
|
|
|
|
|
("ACE²", 66),
|
2022-10-10 03:37:58 +00:00
|
|
|
|
):
|
2022-09-09 14:50:17 +00:00
|
|
|
|
if artifact_total_score / 5 >= r[1]:
|
|
|
|
|
artifact_total_score_label = r[0]
|
|
|
|
|
|
|
|
|
|
data = {
|
2023-08-26 10:19:00 +00:00
|
|
|
|
"uid": mask_number(self.uid),
|
2022-09-09 14:50:17 +00:00
|
|
|
|
"character": self.character,
|
|
|
|
|
"stats": await self.de_stats(),
|
|
|
|
|
"weapon": self.find_weapon(),
|
|
|
|
|
# 圣遗物评分
|
|
|
|
|
"artifact_total_score": artifact_total_score,
|
|
|
|
|
# 圣遗物评级
|
|
|
|
|
"artifact_total_score_label": artifact_total_score_label,
|
|
|
|
|
# 圣遗物评级颜色
|
|
|
|
|
"artifact_total_score_class": Artifact.get_score_class(artifact_total_score_label),
|
|
|
|
|
"artifacts": artifacts,
|
|
|
|
|
# 需要在模板中使用的 enum 类型
|
|
|
|
|
"DigitType": DigitType,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return await self.template_service.render(
|
2023-05-09 11:01:45 +00:00
|
|
|
|
"genshin/player_card/player_card.jinja2",
|
2022-09-09 14:50:17 +00:00
|
|
|
|
data,
|
|
|
|
|
{"width": 950, "height": 1080},
|
|
|
|
|
full_page=True,
|
2022-10-10 03:37:58 +00:00
|
|
|
|
query_selector=".text-neutral-200",
|
2022-10-22 07:03:59 +00:00
|
|
|
|
ttl=7 * 24 * 60 * 60,
|
2022-09-09 14:50:17 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
async def de_stats(self) -> List[Tuple[str, Any]]:
|
|
|
|
|
stats = self.character.stats
|
|
|
|
|
items: List[Tuple[str, Any]] = []
|
|
|
|
|
logger.debug(self.character.stats)
|
|
|
|
|
|
|
|
|
|
# items.append(("基础生命值", stats.BASE_HP.to_rounded()))
|
|
|
|
|
items.append(("生命值", stats.FIGHT_PROP_MAX_HP.to_rounded()))
|
|
|
|
|
# items.append(("基础攻击力", stats.FIGHT_PROP_BASE_ATTACK.to_rounded()))
|
|
|
|
|
items.append(("攻击力", stats.FIGHT_PROP_CUR_ATTACK.to_rounded()))
|
|
|
|
|
# items.append(("基础防御力", stats.FIGHT_PROP_BASE_DEFENSE.to_rounded()))
|
|
|
|
|
items.append(("防御力", stats.FIGHT_PROP_CUR_DEFENSE.to_rounded()))
|
|
|
|
|
items.append(("暴击率", stats.FIGHT_PROP_CRITICAL.to_percentage_symbol()))
|
|
|
|
|
items.append(
|
|
|
|
|
(
|
|
|
|
|
"暴击伤害",
|
|
|
|
|
stats.FIGHT_PROP_CRITICAL_HURT.to_percentage_symbol(),
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
items.append(
|
|
|
|
|
(
|
|
|
|
|
"元素充能效率",
|
|
|
|
|
stats.FIGHT_PROP_CHARGE_EFFICIENCY.to_percentage_symbol(),
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
items.append(("元素精通", stats.FIGHT_PROP_ELEMENT_MASTERY.to_rounded()))
|
|
|
|
|
|
|
|
|
|
# 查找元素伤害加成和治疗加成
|
|
|
|
|
max_stat = StatsPercentage() # 用于记录最高元素伤害加成 避免武器特效影响
|
|
|
|
|
for stat in stats:
|
|
|
|
|
if 40 <= stat[1].id <= 46: # 元素伤害加成
|
|
|
|
|
if max_stat.value <= stat[1].value:
|
|
|
|
|
max_stat = stat[1]
|
|
|
|
|
elif stat[1].id == 29: # 物理伤害加成
|
|
|
|
|
pass
|
|
|
|
|
elif stat[1].id != 26: # 治疗加成
|
|
|
|
|
continue
|
2022-10-10 03:37:58 +00:00
|
|
|
|
value = stat[1].to_rounded() if isinstance(stat[1], Stats) else stat[1].to_percentage_symbol()
|
2022-09-09 14:50:17 +00:00
|
|
|
|
if value in ("0%", 0):
|
|
|
|
|
continue
|
2022-10-10 03:37:58 +00:00
|
|
|
|
name = DEFAULT_EnkaAssets.get_hash_map(stat[0])
|
2022-09-09 14:50:17 +00:00
|
|
|
|
if name is None:
|
|
|
|
|
continue
|
|
|
|
|
items.append((name, value))
|
|
|
|
|
|
|
|
|
|
if max_stat.id != 0:
|
|
|
|
|
for item in items:
|
2022-09-10 12:22:51 +00:00
|
|
|
|
if "元素伤害加成" in item[0] and max_stat.to_percentage_symbol() != item[1]:
|
|
|
|
|
items.remove(item)
|
2022-09-09 14:50:17 +00:00
|
|
|
|
|
|
|
|
|
return items
|
|
|
|
|
|
|
|
|
|
async def cache_images(self) -> None:
|
|
|
|
|
"""缓存所有图片到本地"""
|
|
|
|
|
# TODO: 并发下载所有资源
|
|
|
|
|
c = self.character
|
|
|
|
|
# 角色
|
2023-03-14 01:27:22 +00:00
|
|
|
|
c.image.banner.url = await download_resource(c.image.banner.url)
|
2022-09-09 14:50:17 +00:00
|
|
|
|
|
|
|
|
|
# 技能
|
|
|
|
|
for item in c.skills:
|
2023-03-14 01:27:22 +00:00
|
|
|
|
item.icon.url = await download_resource(item.icon.url)
|
2022-09-09 14:50:17 +00:00
|
|
|
|
|
|
|
|
|
# 命座
|
|
|
|
|
for item in c.constellations:
|
2023-03-14 01:27:22 +00:00
|
|
|
|
item.icon.url = await download_resource(item.icon.url)
|
2022-09-09 14:50:17 +00:00
|
|
|
|
|
|
|
|
|
# 装备,包括圣遗物和武器
|
|
|
|
|
for item in c.equipments:
|
2023-03-14 01:27:22 +00:00
|
|
|
|
item.detail.icon.url = await download_resource(item.detail.icon.url)
|
2022-09-09 14:50:17 +00:00
|
|
|
|
|
2023-03-14 01:27:22 +00:00
|
|
|
|
def find_weapon(self) -> Optional[Equipments]:
|
2022-09-09 14:50:17 +00:00
|
|
|
|
"""在 equipments 数组中找到武器,equipments 数组包含圣遗物和武器"""
|
|
|
|
|
for item in self.character.equipments:
|
|
|
|
|
if item.type == EquipmentsType.WEAPON:
|
|
|
|
|
return item
|
2023-03-14 01:27:22 +00:00
|
|
|
|
return None
|
2022-09-09 14:50:17 +00:00
|
|
|
|
|
|
|
|
|
def find_artifacts(self) -> List[Artifact]:
|
|
|
|
|
"""在 equipments 数组中找到圣遗物,并转换成带有分数的 model。equipments 数组包含圣遗物和武器"""
|
|
|
|
|
|
|
|
|
|
stats = ArtifactStatsTheory(self.character.name)
|
|
|
|
|
|
2023-03-17 05:33:29 +00:00
|
|
|
|
def substat_score(s: "EquipmentsStats") -> float:
|
2022-09-09 14:50:17 +00:00
|
|
|
|
return stats.theory(s)
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
Artifact(
|
|
|
|
|
equipment=e,
|
|
|
|
|
# 圣遗物单行属性评分
|
|
|
|
|
substat_scores=[substat_score(s) for s in e.detail.substats],
|
|
|
|
|
)
|
|
|
|
|
for e in self.character.equipments
|
|
|
|
|
if e.type == EquipmentsType.ARTIFACT
|
|
|
|
|
]
|