diff --git a/StarRailUID/sruid_utils/api/mys/models.py b/StarRailUID/sruid_utils/api/mys/models.py index bc9632e..925a893 100644 --- a/StarRailUID/sruid_utils/api/mys/models.py +++ b/StarRailUID/sruid_utils/api/mys/models.py @@ -1,4 +1,4 @@ -from typing import Any, List, TypedDict +from typing import Any, Dict, List, Optional, TypedDict class RoleBasicInfo(TypedDict): @@ -119,3 +119,86 @@ class SignList(TypedDict): biz: str resign: bool short_extra_award: SignExtraAward + + +##################### +# 基础信息 角色信息 # +#################### + + +class Stats(TypedDict): + active_days: int + avatar_num: int + achievement_num: int + chest_num: int + abyss_process: str + + +class AvatarListItem(TypedDict): + id: int + level: int + name: str + element: str + icon: str + rarity: int + rank: int + is_chosen: bool + + +class RoleIndex(TypedDict): + stats: Stats + avatar_list: List[AvatarListItem] + + +################ +# 角色详细信息 # +################ + + +class Equip(TypedDict): + id: int + level: int + rank: int + name: str + desc: str + icon: str + + +class RelicsItem(TypedDict): + id: int + level: int + pos: int + name: str + desc: str + icon: str + rarity: int + + +class RanksItem(TypedDict): + id: int + pos: int + name: str + icon: str + desc: str + is_unlocked: bool + + +class AvatarListItemDetail(TypedDict): + id: int + level: int + name: str + element: str + icon: str + rarity: int + rank: int + image: str + equip: Optional[Equip] + relics: List[RelicsItem] + ornaments: List + ranks: List[RanksItem] + + +class AvatarInfo(TypedDict): + avatar_list: List[AvatarListItemDetail] + equip_wiki: Dict[str, str] + relic_wiki: Dict diff --git a/StarRailUID/starrailuid_roleinfo/__init__.py b/StarRailUID/starrailuid_roleinfo/__init__.py index 20e8ff7..9715fe0 100644 --- a/StarRailUID/starrailuid_roleinfo/__init__.py +++ b/StarRailUID/starrailuid_roleinfo/__init__.py @@ -1,10 +1,17 @@ from gsuid_core.sv import SV from gsuid_core.bot import Bot from gsuid_core.models import Event +import re +from .draw_roleinfo_card import get_role_img sv_get_info = SV('sr查询信息') @sv_get_info.on_command(('sr', 'sruid')) async def send_role_info(bot: Bot, ev: Event): - await bot.send("WIP 前面的区域,以后再来探索吧") + name = ''.join(re.findall('[\u4e00-\u9fa5]', ev.text)) + if name: + return + + await bot.logger.info('开始执行[sr查询信息]') + await bot.send(await get_role_img(bot.bot_id, ev.user_id)) diff --git a/StarRailUID/starrailuid_roleinfo/draw_roleinfo_card.py b/StarRailUID/starrailuid_roleinfo/draw_roleinfo_card.py new file mode 100644 index 0000000..e2e0949 --- /dev/null +++ b/StarRailUID/starrailuid_roleinfo/draw_roleinfo_card.py @@ -0,0 +1,208 @@ +from pathlib import Path +from typing import Dict, List, Optional + +from PIL import Image, ImageDraw + +from gsuid_core.logger import logger + +from ..utils.api import get_sqla +from ..utils.mys_api import mys_api +from .utils import get_icon, wrap_list +from ..utils.image.convert import convert_img +from ..utils.fonts.starrail_fonts import ( + sr_font_24, + sr_font_30, + sr_font_36, +) + +TEXT_PATH = Path(__file__).parent / 'texture2D' + +bg1 = Image.open(TEXT_PATH / 'bg1.png') +bg2 = Image.open(TEXT_PATH / 'bg2.png') +bg3 = Image.open(TEXT_PATH / 'bg3.png') +user_avatar = ( + Image.open(TEXT_PATH / "200101.png").resize((220, 220)).convert("RGBA") +) +char_bg_4 = Image.open(TEXT_PATH / 'rarity4_bg.png').convert("RGBA") +char_bg_5 = Image.open(TEXT_PATH / 'rarity5_bg.png').convert("RGBA") +circle = Image.open(TEXT_PATH / 'char_weapon_bg.png').convert("RGBA") + +bg_color = (248, 248, 248) +white_color = (255, 255, 255) +color_color = (40, 18, 7) +first_color = (22, 8, 31) + +elements = { + "ice": Image.open(TEXT_PATH / "IconNatureColorIce.png").convert("RGBA"), + "fire": Image.open(TEXT_PATH / "IconNatureColorFire.png").convert("RGBA"), + "imaginary": Image.open( + TEXT_PATH / "IconNatureColorImaginary.png" + ).convert("RGBA"), + "quantum": Image.open(TEXT_PATH / "IconNatureColorQuantum.png").convert( + "RGBA" + ), + "lightning": Image.open(TEXT_PATH / "IconNatureColorThunder.png").convert( + "RGBA" + ), + "wind": Image.open(TEXT_PATH / "IconNatureColorWind.png").convert("RGBA"), + "physical": Image.open(TEXT_PATH / "IconNaturePhysical.png").convert( + "RGBA" + ), +} + + +async def get_role_img(bot_id: str, user_id: str): + sqla = get_sqla(bot_id) + uid_list: List = await sqla.get_bind_sruid_list(user_id) + logger.info(f'[每日信息]UID: {uid_list}') + # 进行校验UID是否绑定CK + useable_uid_list = [] + for uid in uid_list: + status = await sqla.get_user_cookie(uid) + if status is not None: + useable_uid_list.append(uid) + uid = useable_uid_list[0] + res = await convert_img(await draw_role_card(uid)) + logger.info(f'[每日信息]可用UID: {useable_uid_list}') + if not useable_uid_list: + return '请先绑定一个可用CK & UID再来查询哦~' + return res + + +def _lv(level: int) -> str: + return f"Lv.0{level}" if level < 10 else f"Lv.{level}" + + +async def draw_role_card(sr_uid: str) -> Image.Image: + role_basic_info = await mys_api.get_role_basic_info(sr_uid) + role_index = await mys_api.get_role_index(sr_uid) + stats = role_index['stats'] + avatars = role_index['avatar_list'] + + # 名称 + nickname = role_basic_info['nickname'] + + # 基本状态 + active_days = stats['active_days'] + avater_num = stats['avatar_num'] + achievement_num = stats['achievement_num'] + chest_num = stats['chest_num'] + level = role_basic_info['level'] + + # 忘却之庭 + abyss_process = stats['abyss_process'] + + # 角色武器 + details = (await mys_api.get_avatar_info(sr_uid, avatars[0]['id']))[ + 'avatar_list' + ] + equips: Dict[int, Optional[str]] = {} + for detail in details: + equip = detail['equip'] + equips[detail['id']] = equip['icon'] if equip is not None else None # type: ignore + + img_bg1 = bg1.copy() + bg1_draw = ImageDraw.Draw(img_bg1) + + # 写Nickname + bg1_draw.text( + (400, 85), nickname, font=sr_font_36, fill=white_color, anchor='mm' + ) + # 写UID + bg1_draw.text( + (400, 165), + f"UID {sr_uid}", + font=sr_font_30, + fill=white_color, + anchor='mm', + ) + # 贴头像 + img_bg1.paste(user_avatar, (286, 213), mask=user_avatar) + + # 写基本信息 + bg1_draw.text( + (143, 590), + str(active_days), + font=sr_font_36, + fill=white_color, + anchor='mm', + ) # 活跃天数 + bg1_draw.text( + (270, 590), + str(avater_num), + font=sr_font_36, + fill=white_color, + anchor='mm', + ) # 解锁角色 + bg1_draw.text( + (398, 590), + str(achievement_num), + font=sr_font_36, + fill=white_color, + anchor='mm', + ) # 达成成就 + bg1_draw.text( + (525, 590), + str(chest_num), + font=sr_font_36, + fill=white_color, + anchor='mm', + ) # 战利品开启 + bg1_draw.text( + (666, 590), str(level), font=sr_font_36, fill=white_color, anchor='mm' + ) # 开拓等级 + + # 画忘却之庭 + bg1_draw.text( + (471, 722), + abyss_process, + font=sr_font_30, + fill=first_color, + anchor='mm', + ) + + # 角色部分 每五个一组 + lines = [] + for five_avatars in wrap_list(avatars, 5): + line = bg2.copy() + x = 70 + for avatar in five_avatars: + char_bg = ( + char_bg_4 if avatar['rarity'] == 4 else char_bg_5 + ).copy() + char_draw = ImageDraw.Draw(char_bg) + char_icon = await get_icon(avatar['icon']) + element_icon = elements[avatar['element']] + + char_bg.paste(char_icon, (4, 8), mask=char_icon) + char_bg.paste(element_icon, (81, 10), mask=element_icon) + + if equip := equips[avatar['id']]: + char_bg.paste(circle, (0, 0), mask=circle) + equip_icon = (await get_icon(equip)).resize((48, 48)) + char_bg.paste(equip_icon, (9, 80), mask=equip_icon) + + char_draw.text( + (60, 146), + _lv(avatar['level']), + font=sr_font_24, + fill=color_color, + anchor='mm', + ) + + line.paste(char_bg, (x, 0)) + x += 135 + lines.append(line) + + # 绘制总图 + img = Image.new("RGBA", (800, 880 + len(lines) * 200), bg_color) + img.paste(img_bg1, (0, 0)) + + y = 810 + for line in lines: + img.paste(line, (0, y), mask=line) + y += 200 + + img.paste(bg3, (0, len(lines) * 200 + 810)) + + return img diff --git a/StarRailUID/starrailuid_roleinfo/texture2D/200101.png b/StarRailUID/starrailuid_roleinfo/texture2D/200101.png new file mode 100644 index 0000000..2af01d3 Binary files /dev/null and b/StarRailUID/starrailuid_roleinfo/texture2D/200101.png differ diff --git a/StarRailUID/starrailuid_roleinfo/texture2D/IconNatureColorFire.png b/StarRailUID/starrailuid_roleinfo/texture2D/IconNatureColorFire.png new file mode 100644 index 0000000..c0e5c09 Binary files /dev/null and b/StarRailUID/starrailuid_roleinfo/texture2D/IconNatureColorFire.png differ diff --git a/StarRailUID/starrailuid_roleinfo/texture2D/IconNatureColorIce.png b/StarRailUID/starrailuid_roleinfo/texture2D/IconNatureColorIce.png new file mode 100644 index 0000000..9026e17 Binary files /dev/null and b/StarRailUID/starrailuid_roleinfo/texture2D/IconNatureColorIce.png differ diff --git a/StarRailUID/starrailuid_roleinfo/texture2D/IconNatureColorImaginary.png b/StarRailUID/starrailuid_roleinfo/texture2D/IconNatureColorImaginary.png new file mode 100644 index 0000000..da9f292 Binary files /dev/null and b/StarRailUID/starrailuid_roleinfo/texture2D/IconNatureColorImaginary.png differ diff --git a/StarRailUID/starrailuid_roleinfo/texture2D/IconNatureColorQuantum.png b/StarRailUID/starrailuid_roleinfo/texture2D/IconNatureColorQuantum.png new file mode 100644 index 0000000..3ba24c0 Binary files /dev/null and b/StarRailUID/starrailuid_roleinfo/texture2D/IconNatureColorQuantum.png differ diff --git a/StarRailUID/starrailuid_roleinfo/texture2D/IconNatureColorThunder.png b/StarRailUID/starrailuid_roleinfo/texture2D/IconNatureColorThunder.png new file mode 100644 index 0000000..9982c61 Binary files /dev/null and b/StarRailUID/starrailuid_roleinfo/texture2D/IconNatureColorThunder.png differ diff --git a/StarRailUID/starrailuid_roleinfo/texture2D/IconNatureColorWind.png b/StarRailUID/starrailuid_roleinfo/texture2D/IconNatureColorWind.png new file mode 100644 index 0000000..f92982a Binary files /dev/null and b/StarRailUID/starrailuid_roleinfo/texture2D/IconNatureColorWind.png differ diff --git a/StarRailUID/starrailuid_roleinfo/texture2D/IconNaturePhysical.png b/StarRailUID/starrailuid_roleinfo/texture2D/IconNaturePhysical.png new file mode 100644 index 0000000..e78da4f Binary files /dev/null and b/StarRailUID/starrailuid_roleinfo/texture2D/IconNaturePhysical.png differ diff --git a/StarRailUID/starrailuid_roleinfo/texture2D/bg1.png b/StarRailUID/starrailuid_roleinfo/texture2D/bg1.png new file mode 100644 index 0000000..dedd71b Binary files /dev/null and b/StarRailUID/starrailuid_roleinfo/texture2D/bg1.png differ diff --git a/StarRailUID/starrailuid_roleinfo/texture2D/bg2.png b/StarRailUID/starrailuid_roleinfo/texture2D/bg2.png new file mode 100644 index 0000000..eb8721b Binary files /dev/null and b/StarRailUID/starrailuid_roleinfo/texture2D/bg2.png differ diff --git a/StarRailUID/starrailuid_roleinfo/texture2D/bg3.png b/StarRailUID/starrailuid_roleinfo/texture2D/bg3.png new file mode 100644 index 0000000..53d23e5 Binary files /dev/null and b/StarRailUID/starrailuid_roleinfo/texture2D/bg3.png differ diff --git a/StarRailUID/starrailuid_roleinfo/texture2D/char_weapon_bg.png b/StarRailUID/starrailuid_roleinfo/texture2D/char_weapon_bg.png new file mode 100644 index 0000000..92d64d5 Binary files /dev/null and b/StarRailUID/starrailuid_roleinfo/texture2D/char_weapon_bg.png differ diff --git a/StarRailUID/starrailuid_roleinfo/texture2D/rarity4_bg.png b/StarRailUID/starrailuid_roleinfo/texture2D/rarity4_bg.png new file mode 100644 index 0000000..031f478 Binary files /dev/null and b/StarRailUID/starrailuid_roleinfo/texture2D/rarity4_bg.png differ diff --git a/StarRailUID/starrailuid_roleinfo/texture2D/rarity5_bg.png b/StarRailUID/starrailuid_roleinfo/texture2D/rarity5_bg.png new file mode 100644 index 0000000..9e07c32 Binary files /dev/null and b/StarRailUID/starrailuid_roleinfo/texture2D/rarity5_bg.png differ diff --git a/StarRailUID/starrailuid_roleinfo/utils.py b/StarRailUID/starrailuid_roleinfo/utils.py new file mode 100644 index 0000000..ef34197 --- /dev/null +++ b/StarRailUID/starrailuid_roleinfo/utils.py @@ -0,0 +1,18 @@ +from io import BytesIO +from typing import List, TypeVar, Generator + +from PIL import Image +from aiohttp import ClientSession + +T = TypeVar("T") + + +def wrap_list(lst: List[T], n: int) -> Generator[List[T], None, None]: + for i in range(0, len(lst), n): + yield lst[i : i + n] + + +async def get_icon(url: str) -> Image.Image: + async with ClientSession() as client: + async with client.get(url) as resp: + return Image.open(BytesIO(await resp.read())).convert("RGBA") diff --git a/StarRailUID/utils/mys_api.py b/StarRailUID/utils/mys_api.py index 2d3e561..5a53bbd 100644 --- a/StarRailUID/utils/mys_api.py +++ b/StarRailUID/utils/mys_api.py @@ -19,6 +19,8 @@ from ..sruid_utils.api.mys.models import ( MonthlyAward, DailyNoteData, RoleBasicInfo, + RoleIndex, + AvatarInfo ) RECOGNIZE_SERVER = { @@ -113,6 +115,27 @@ class _MysApi(BaseMysApi): data = cast(DailyNoteData, data['data']) return data + async def get_role_index(self, uid: str) -> Union[RoleIndex, int]: + data = await self.simple_mys_req('STAR_RAIL_INDEX_URL', uid) + if isinstance(data, Dict): + data = cast(RoleIndex, data['data']) + return data + + async def get_avatar_info( + self, uid: str, avatar_id: int, need_wiki: bool = False + ) -> Union[AvatarInfo, int]: + data = await self.simple_mys_req( + 'STAR_RAIL_AVATAR_INFO_URL', + uid, + params={ + "id": avatar_id, + "need_wiki": "true" if need_wiki else "false" + }, + ) + if isinstance(data, Dict): + data = cast(AvatarInfo, data['data']) + return data + async def get_sign_list(self, uid) -> Union[SignList, int]: # is_os = self.check_os(uid) is_os = False @@ -283,7 +306,7 @@ class _MysApi(BaseMysApi): server_id = 'cn_qd01' if is_os else 'prod_gf_cn' else: server_id = RECOGNIZE_SERVER.get(uid[0]) - is_os = False if int(uid[0]) < 6 else True + is_os = int(uid[0]) >= 6 ex_params = '&'.join([f'{k}={v}' for k, v in params.items()]) if is_os: _URL = _API[f'{URL}_OS'] @@ -292,8 +315,9 @@ class _MysApi(BaseMysApi): else: _URL = _API[URL] HEADER = copy.deepcopy(self._HEADER) + param_str = f'role_id={uid}&server={server_id}' HEADER['DS'] = get_ds_token( - ex_params if ex_params else f'role_id={uid}&server={server_id}' + f"{ex_params}&{param_str}" if ex_params else param_str ) HEADER.update(header) if cookie is not None: @@ -303,11 +327,12 @@ class _MysApi(BaseMysApi): if ck is None: return -51 HEADER['Cookie'] = ck + param_dict = {'server': server_id, 'role_id': uid} data = await self._mys_request( url=_URL, method='GET', header=HEADER, - params=params if params else {'server': server_id, 'role_id': uid}, + params={**params, **param_dict} if params else param_dict, use_proxy=True if is_os else False, ) return data