2024-01-23 13:56:59 +00:00
|
|
|
import asyncio
|
2024-05-27 13:24:14 +00:00
|
|
|
import math
|
2024-05-21 13:38:51 +00:00
|
|
|
from typing import List, Optional, TYPE_CHECKING, Dict, Union, Tuple, Any
|
2023-04-29 16:15:21 +00:00
|
|
|
|
2024-05-21 13:38:51 +00:00
|
|
|
from arkowrapper import ArkoWrapper
|
2023-04-29 16:15:21 +00:00
|
|
|
from pydantic import BaseModel
|
2023-08-21 14:43:01 +00:00
|
|
|
from telegram.constants import ChatAction
|
2023-04-29 16:15:21 +00:00
|
|
|
from telegram.ext import filters
|
|
|
|
|
|
|
|
from core.dependence.assets import AssetsService, AssetsCouldNotFound
|
|
|
|
from core.plugin import Plugin, handler
|
|
|
|
from core.services.cookies import CookiesService
|
|
|
|
from core.services.template.models import FileType
|
|
|
|
from core.services.template.services import TemplateService
|
2023-05-06 14:46:38 +00:00
|
|
|
from core.services.wiki.services import WikiService
|
2024-06-18 17:33:28 +00:00
|
|
|
from gram_core.plugin.methods.inline_use_data import IInlineUseData
|
2024-05-21 13:38:51 +00:00
|
|
|
from gram_core.services.template.models import RenderGroupResult
|
2024-01-23 13:56:59 +00:00
|
|
|
from plugins.tools.genshin import GenshinHelper, CharacterDetails
|
2024-03-29 13:40:23 +00:00
|
|
|
from plugins.tools.head_icon import HeadIconService
|
2024-03-29 15:14:24 +00:00
|
|
|
from plugins.tools.phone_theme import PhoneThemeService
|
2023-04-29 16:15:21 +00:00
|
|
|
from utils.log import logger
|
2023-08-26 13:21:26 +00:00
|
|
|
from utils.uid import mask_number
|
2023-04-29 16:15:21 +00:00
|
|
|
|
|
|
|
if TYPE_CHECKING:
|
2023-07-19 05:52:30 +00:00
|
|
|
from simnet import StarRailClient
|
2024-01-23 13:56:59 +00:00
|
|
|
from simnet.models.starrail.calculator import StarrailCalculatorCharacterDetails
|
|
|
|
from simnet.models.starrail.chronicle.characters import StarRailDetailCharacter
|
2023-04-29 16:15:21 +00:00
|
|
|
from telegram.ext import ContextTypes
|
|
|
|
from telegram import Update
|
2024-05-21 13:38:51 +00:00
|
|
|
from gram_core.services.template.models import RenderResult
|
2023-04-29 16:15:21 +00:00
|
|
|
|
2024-03-29 13:40:23 +00:00
|
|
|
MAX_AVATAR_COUNT = 40
|
2024-01-23 13:56:59 +00:00
|
|
|
|
2023-04-29 16:15:21 +00:00
|
|
|
|
|
|
|
class EquipmentData(BaseModel):
|
|
|
|
id: int
|
|
|
|
name: str
|
|
|
|
level: int
|
|
|
|
eidolon: int
|
2023-05-06 14:46:38 +00:00
|
|
|
rarity: int
|
2023-04-29 16:15:21 +00:00
|
|
|
icon: str
|
|
|
|
|
|
|
|
|
2024-01-23 13:56:59 +00:00
|
|
|
class SkillData(BaseModel):
|
|
|
|
id: int
|
|
|
|
level: int
|
|
|
|
max_level: int
|
|
|
|
|
|
|
|
|
2023-04-29 16:15:21 +00:00
|
|
|
class AvatarData(BaseModel):
|
|
|
|
id: int
|
|
|
|
name: str
|
|
|
|
level: int
|
|
|
|
eidolon: int
|
|
|
|
rarity: int
|
|
|
|
icon: str = ""
|
2024-01-23 13:56:59 +00:00
|
|
|
skills: List[SkillData]
|
2023-04-29 16:15:21 +00:00
|
|
|
equipment: Optional[EquipmentData] = None
|
|
|
|
|
|
|
|
|
|
|
|
class AvatarListPlugin(Plugin):
|
|
|
|
"""练度统计"""
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
cookies_service: CookiesService = None,
|
|
|
|
assets_service: AssetsService = None,
|
|
|
|
template_service: TemplateService = None,
|
2023-05-06 14:46:38 +00:00
|
|
|
wiki_service: WikiService = None,
|
2023-04-29 16:15:21 +00:00
|
|
|
helper: GenshinHelper = None,
|
2024-01-23 13:56:59 +00:00
|
|
|
character_details: CharacterDetails = None,
|
2024-03-29 13:40:23 +00:00
|
|
|
head_icon: HeadIconService = None,
|
2024-03-29 15:14:24 +00:00
|
|
|
phone_theme: PhoneThemeService = None,
|
2023-04-29 16:15:21 +00:00
|
|
|
) -> None:
|
|
|
|
self.cookies_service = cookies_service
|
|
|
|
self.assets_service = assets_service
|
|
|
|
self.template_service = template_service
|
2023-05-06 14:46:38 +00:00
|
|
|
self.wiki_service = wiki_service
|
2023-04-29 16:15:21 +00:00
|
|
|
self.helper = helper
|
2024-01-23 13:56:59 +00:00
|
|
|
self.character_details = character_details
|
2024-03-29 13:40:23 +00:00
|
|
|
self.head_icon = head_icon
|
2024-03-29 15:14:24 +00:00
|
|
|
self.phone_theme = phone_theme
|
2024-01-23 13:56:59 +00:00
|
|
|
|
|
|
|
async def get_avatar_data(
|
|
|
|
self, character_id: int, client: "StarRailClient"
|
|
|
|
) -> Optional["StarrailCalculatorCharacterDetails"]:
|
|
|
|
return await self.character_details.get_character_details(client, character_id)
|
2023-04-29 16:15:21 +00:00
|
|
|
|
|
|
|
@staticmethod
|
2024-01-23 13:56:59 +00:00
|
|
|
async def get_avatars_data(client: "StarRailClient") -> List["StarRailDetailCharacter"]:
|
|
|
|
task_info_results = (await client.get_starrail_characters()).avatar_list
|
|
|
|
|
2023-04-29 16:15:21 +00:00
|
|
|
return sorted(
|
2024-01-23 13:56:59 +00:00
|
|
|
list(filter(lambda x: x, task_info_results)),
|
2023-04-29 16:15:21 +00:00
|
|
|
key=lambda x: (
|
|
|
|
x.level,
|
|
|
|
x.rarity,
|
|
|
|
sum([i.is_unlocked for i in x.ranks]),
|
|
|
|
),
|
|
|
|
reverse=True,
|
|
|
|
)
|
|
|
|
|
2023-05-06 14:46:38 +00:00
|
|
|
def get_light_cone_star(self, name: str) -> int:
|
|
|
|
light_cone = self.wiki_service.light_cone.get_by_name(name)
|
2023-08-29 06:50:37 +00:00
|
|
|
return light_cone.rank if light_cone else 3
|
2023-05-06 14:46:38 +00:00
|
|
|
|
2024-01-23 13:56:59 +00:00
|
|
|
async def get_avatars_details(
|
|
|
|
self, characters: List["StarRailDetailCharacter"], client: "StarRailClient"
|
|
|
|
) -> Dict[int, "StarrailCalculatorCharacterDetails"]:
|
|
|
|
async def _task(cid):
|
|
|
|
return await self.get_avatar_data(cid, client)
|
|
|
|
|
|
|
|
task_detail_results = await asyncio.gather(*[_task(character.id) for character in characters])
|
|
|
|
return {character.id: detail for character, detail in zip(characters, task_detail_results)}
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def get_skill_data(character: Optional["StarrailCalculatorCharacterDetails"]) -> List[SkillData]:
|
|
|
|
if not character:
|
|
|
|
return [SkillData(id=i, level=1, max_level=10) for i in range(1, 5)]
|
|
|
|
return [SkillData(id=skill.id, level=skill.cur_level, max_level=skill.max_level) for skill in character.skills]
|
|
|
|
|
|
|
|
async def get_final_data(
|
|
|
|
self, characters: List["StarRailDetailCharacter"], client: "StarRailClient"
|
|
|
|
) -> List[AvatarData]:
|
|
|
|
details = await self.get_avatars_details(characters, client)
|
2023-04-29 16:15:21 +00:00
|
|
|
data = []
|
|
|
|
for character in characters:
|
|
|
|
try:
|
2023-05-04 14:28:13 +00:00
|
|
|
equip = (
|
|
|
|
EquipmentData(
|
|
|
|
id=character.equip.id,
|
|
|
|
name=character.equip.name,
|
|
|
|
level=character.equip.level,
|
|
|
|
eidolon=character.equip.rank,
|
2023-05-06 14:46:38 +00:00
|
|
|
rarity=self.get_light_cone_star(character.equip.name),
|
2023-05-04 14:28:13 +00:00
|
|
|
icon=self.assets_service.light_cone.icon(character.equip.id, character.equip.name).as_uri(),
|
|
|
|
)
|
|
|
|
if character.equip
|
|
|
|
else None
|
|
|
|
)
|
2024-01-23 13:56:59 +00:00
|
|
|
detail = details.get(character.id)
|
2023-04-29 16:15:21 +00:00
|
|
|
avatar = AvatarData(
|
|
|
|
id=character.id,
|
|
|
|
name=character.name,
|
|
|
|
level=character.level,
|
|
|
|
eidolon=sum([i.is_unlocked for i in character.ranks]),
|
|
|
|
rarity=character.rarity,
|
|
|
|
icon=self.assets_service.avatar.icon(character.id, character.name).as_uri(),
|
2024-01-23 13:56:59 +00:00
|
|
|
skills=self.get_skill_data(detail),
|
2023-04-29 16:15:21 +00:00
|
|
|
equipment=equip,
|
|
|
|
)
|
|
|
|
data.append(avatar)
|
|
|
|
except AssetsCouldNotFound as e:
|
|
|
|
logger.warning("未找到角色 %s[%s] 的资源: %s", character.name, character.id, e)
|
|
|
|
return data
|
|
|
|
|
2024-05-21 13:38:51 +00:00
|
|
|
async def avatar_list_render(
|
|
|
|
self,
|
|
|
|
base_render_data: Dict,
|
|
|
|
avatar_datas: List[AvatarData],
|
|
|
|
only_one_page: bool,
|
|
|
|
) -> Union[Tuple[Any], List["RenderResult"], None]:
|
|
|
|
def render_task(start_id: int, c: List[AvatarData]):
|
|
|
|
_render_data = {
|
|
|
|
"avatar_datas": c, # 角色数据
|
|
|
|
"start_id": start_id, # 开始序号
|
|
|
|
}
|
|
|
|
_render_data.update(base_render_data)
|
|
|
|
return self.template_service.render(
|
|
|
|
"starrail/avatar_list/main.html",
|
|
|
|
_render_data,
|
|
|
|
viewport={"width": 1040, "height": 500},
|
|
|
|
full_page=True,
|
|
|
|
query_selector=".container",
|
|
|
|
file_type=FileType.PHOTO,
|
|
|
|
ttl=30 * 24 * 60 * 60,
|
|
|
|
)
|
|
|
|
|
|
|
|
if only_one_page:
|
|
|
|
return [await render_task(0, avatar_datas)]
|
2024-05-27 13:24:14 +00:00
|
|
|
image_count = len(avatar_datas)
|
|
|
|
while image_count > MAX_AVATAR_COUNT:
|
|
|
|
image_count /= 2
|
|
|
|
image_count = math.ceil(image_count)
|
|
|
|
avatar_datas_group = [avatar_datas[i : i + image_count] for i in range(0, len(avatar_datas), image_count)]
|
|
|
|
tasks = [render_task(i * image_count, c) for i, c in enumerate(avatar_datas_group)]
|
2024-05-21 13:38:51 +00:00
|
|
|
return await asyncio.gather(*tasks)
|
|
|
|
|
2024-06-18 17:33:28 +00:00
|
|
|
async def render(self, client: "StarRailClient", all_avatars: bool = False) -> List["RenderResult"]:
|
|
|
|
characters: List["StarRailDetailCharacter"] = await self.get_avatars_data(client)
|
|
|
|
record_card = await client.get_record_card()
|
|
|
|
nickname = record_card.nickname
|
|
|
|
has_more = (not all_avatars) and len(characters) > MAX_AVATAR_COUNT
|
|
|
|
if has_more:
|
|
|
|
characters = characters[:MAX_AVATAR_COUNT]
|
|
|
|
avatar_datas = await self.get_final_data(characters, client)
|
|
|
|
|
|
|
|
base_render_data = {
|
|
|
|
"uid": mask_number(client.player_id), # 玩家uid
|
|
|
|
"nickname": nickname, # 玩家昵称
|
|
|
|
"has_more": has_more, # 是否显示了全部角色
|
|
|
|
"avatar": (await self.head_icon.get_head_icon(client.player_id)).as_uri(),
|
|
|
|
"background": (await self.phone_theme.get_phone_theme(client.player_id)).as_uri(),
|
|
|
|
}
|
|
|
|
|
|
|
|
return await self.avatar_list_render(base_render_data, avatar_datas, has_more)
|
|
|
|
|
2024-03-25 12:43:11 +00:00
|
|
|
@handler.command("avatars", cookie=True, block=False)
|
|
|
|
@handler.message(filters.Regex(r"^(全部)?练度统计$"), cookie=True, block=False)
|
2023-08-21 14:43:01 +00:00
|
|
|
async def avatar_list(self, update: "Update", _: "ContextTypes.DEFAULT_TYPE"):
|
2024-03-10 12:50:32 +00:00
|
|
|
user_id = await self.get_real_user_id(update)
|
2023-04-29 16:15:21 +00:00
|
|
|
message = update.effective_message
|
2024-06-12 12:43:28 +00:00
|
|
|
uid, offset = self.get_real_uid_or_offset(update)
|
2023-05-04 14:28:13 +00:00
|
|
|
all_avatars = "全部" in message.text or "all" in message.text # 是否发送全部角色
|
2024-03-10 12:50:32 +00:00
|
|
|
self.log_user(update, logger.info, "[bold]练度统计[/bold]: all=%s", all_avatars, extra={"markup": True})
|
2023-04-29 16:15:21 +00:00
|
|
|
await message.reply_chat_action(ChatAction.TYPING)
|
2023-08-21 14:43:01 +00:00
|
|
|
|
2024-06-12 12:43:28 +00:00
|
|
|
async with self.helper.genshin(user_id, player_id=uid, offset=offset) as client:
|
2024-05-21 13:38:51 +00:00
|
|
|
notice = await message.reply_text("彦卿需要收集整理数据,还请耐心等待哦~")
|
2024-06-06 15:49:48 +00:00
|
|
|
self.add_delete_message_job(notice, delay=60)
|
2024-06-18 17:33:28 +00:00
|
|
|
images = await self.render(client, all_avatars)
|
2024-05-21 13:38:51 +00:00
|
|
|
|
|
|
|
for group in ArkoWrapper(images).group(10): # 每 10 张图片分一个组
|
2024-06-11 04:44:34 +00:00
|
|
|
await RenderGroupResult(results=group).reply_media_group(message, write_timeout=60)
|
2024-05-21 13:38:51 +00:00
|
|
|
|
2024-03-10 12:50:32 +00:00
|
|
|
self.log_user(
|
|
|
|
update,
|
|
|
|
logger.info,
|
2024-05-21 13:38:51 +00:00
|
|
|
"[bold]练度统计[/bold]发送图片成功",
|
2023-04-29 16:15:21 +00:00
|
|
|
extra={"markup": True},
|
|
|
|
)
|
2024-06-18 17:33:28 +00:00
|
|
|
|
|
|
|
async def avatar_list_use_by_inline(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> None:
|
|
|
|
callback_query = update.callback_query
|
|
|
|
user = update.effective_user
|
|
|
|
user_id = user.id
|
|
|
|
uid = IInlineUseData.get_uid_from_context(context)
|
|
|
|
self.log_user(update, logger.info, "查询练度统计")
|
|
|
|
|
|
|
|
async with self.helper.genshin(user_id, player_id=uid) as client:
|
|
|
|
client: "StarRailClient"
|
|
|
|
images = await self.render(client)
|
|
|
|
render = images[0]
|
|
|
|
await render.edit_inline_media(callback_query)
|
|
|
|
|
|
|
|
async def get_inline_use_data(self) -> List[Optional[IInlineUseData]]:
|
|
|
|
return [
|
|
|
|
IInlineUseData(
|
|
|
|
text="练度统计",
|
|
|
|
hash="avatar_list",
|
|
|
|
callback=self.avatar_list_use_by_inline,
|
|
|
|
cookie=True,
|
|
|
|
player=True,
|
|
|
|
)
|
|
|
|
]
|