✨ 新增gs个人日历
23
GenshinUID/genshinuid_cale/__init__.py
Normal file
@ -0,0 +1,23 @@
|
||||
from gsuid_core.sv import SV
|
||||
from gsuid_core.bot import Bot
|
||||
from gsuid_core.models import Event
|
||||
from gsuid_core.logger import logger
|
||||
from gsuid_core.utils.error_reply import UID_HINT
|
||||
|
||||
from ..utils.convert import get_uid
|
||||
from .draw_cale_pic import draw_cale_img
|
||||
|
||||
sv_cale = SV('个人日历')
|
||||
|
||||
|
||||
@sv_cale.on_command(
|
||||
('个人日历', '日历', '查询个人日历', '查询日历'), block=True
|
||||
)
|
||||
async def send_cale_pic(bot: Bot, ev: Event):
|
||||
uid = await get_uid(bot, ev)
|
||||
if uid is None:
|
||||
return await bot.send(UID_HINT)
|
||||
logger.info(f'[个人日历] uid: {uid}')
|
||||
|
||||
im = await draw_cale_img(ev, uid)
|
||||
await bot.send_option(im)
|
201
GenshinUID/genshinuid_cale/draw_cale_pic.py
Normal file
@ -0,0 +1,201 @@
|
||||
from pathlib import Path
|
||||
from typing import Union
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
from gsuid_core.models import Event
|
||||
from gsuid_core.utils.error_reply import get_error
|
||||
from gsuid_core.utils.image.convert import convert_img
|
||||
|
||||
from ..utils.api.mys.models import Act, FixedAct
|
||||
from ..utils.mys_api import mys_api, get_base_data
|
||||
from ..utils.resource.download_url import download
|
||||
from ..genshinuid_xkdata.draw_teyvat_img import gen_char
|
||||
from ..utils.fonts.genshin_fonts import gs_font_26, gs_font_30, gs_font_38
|
||||
from ..utils.resource.RESOURCE_PATH import CHAR_PATH, TEMP_PATH, WEAPON_PATH
|
||||
from ..utils.image.image_tools import (
|
||||
get_v4_bg,
|
||||
add_footer,
|
||||
get_avatar,
|
||||
get_v4_title,
|
||||
)
|
||||
|
||||
GREY = (189, 189, 189)
|
||||
TEXT_PATH = Path(__file__).parent / 'texture2d'
|
||||
yes = Image.open(TEXT_PATH / 'yes.png')
|
||||
no = Image.open(TEXT_PATH / 'no.png')
|
||||
|
||||
|
||||
def convert_timestamp_to_string(timestamp):
|
||||
days = timestamp // (24 * 3600)
|
||||
hours = (timestamp % (24 * 3600)) // 3600
|
||||
minutes = (timestamp % 3600) // 60
|
||||
|
||||
result = f"{days}天{hours}时{minutes}分"
|
||||
return result
|
||||
|
||||
|
||||
async def draw_act(act: Union[Act, FixedAct]):
|
||||
act_bg = Image.open(TEXT_PATH / 'act_bg.png')
|
||||
|
||||
fg = yes if act['is_finished'] else no
|
||||
t = '已完成' if act['is_finished'] else '未完成'
|
||||
|
||||
act_bg.paste(fg, (0, 0), fg)
|
||||
act_draw = ImageDraw.Draw(act_bg)
|
||||
|
||||
act_name = act['name']
|
||||
limit_time = '剩余时间:' + convert_timestamp_to_string(
|
||||
act['countdown_seconds']
|
||||
)
|
||||
|
||||
act_draw.text((94, 60), act_name, 'white', gs_font_38, 'lm')
|
||||
act_draw.text((130, 102), limit_time, GREY, gs_font_26, 'lm')
|
||||
|
||||
act_draw.text((840, 81), t, 'white', gs_font_30, 'mm')
|
||||
|
||||
for j, reward in enumerate(act['reward_list'][:2]):
|
||||
icon_bg = Image.open(TEXT_PATH / 'icon_bg.png')
|
||||
icon_fg = Image.open(TEXT_PATH / 'icon_fg.png')
|
||||
ipath = TEMP_PATH / f'{reward["name"]}.png'
|
||||
num = reward['num']
|
||||
|
||||
if not ipath.exists():
|
||||
await download(reward['icon'], 16, f'{reward["name"]}.png')
|
||||
|
||||
icon = Image.open(ipath)
|
||||
icon = icon.resize((100, 100))
|
||||
|
||||
icon_bg.paste(icon, (0, 0), icon)
|
||||
icon_bg.paste(icon_fg, (0, 0), icon_fg)
|
||||
icon_draw = ImageDraw.Draw(icon_bg)
|
||||
|
||||
icon_draw.text((50, 81), str(num), 'white', gs_font_26, 'mm')
|
||||
|
||||
act_bg.paste(icon_bg, (539 + j * 105, 31), icon_bg)
|
||||
|
||||
return act_bg
|
||||
|
||||
|
||||
async def draw_cale_img(ev: Event, uid: str):
|
||||
data = await mys_api.get_calendar_data(uid)
|
||||
if isinstance(data, int):
|
||||
return get_error(data)
|
||||
|
||||
raw_data = await get_base_data(uid)
|
||||
if isinstance(raw_data, (str, bytes)):
|
||||
return raw_data
|
||||
elif isinstance(raw_data, (bytearray, memoryview)):
|
||||
return bytes(raw_data)
|
||||
|
||||
char_pic = await get_avatar(ev, 377, False)
|
||||
title_img = get_v4_title(char_pic, uid, raw_data)
|
||||
|
||||
title_img = title_img.resize((1058, 441))
|
||||
|
||||
w, h = 1000, title_img.size[1] + 385
|
||||
|
||||
if data['avatar_card_pool_list']:
|
||||
h += 270
|
||||
if data['weapon_card_pool_list']:
|
||||
h += 270
|
||||
|
||||
h += len(data['act_list']) * 160
|
||||
h += len(data['fixed_act_list']) * 160
|
||||
|
||||
img = get_v4_bg(w, h)
|
||||
|
||||
img.paste(title_img, (-30, 34), title_img)
|
||||
|
||||
p = title_img.size[1] + 60
|
||||
bar1 = Image.open(TEXT_PATH / 'bar1.png')
|
||||
img.paste(bar1, (0, p), bar1)
|
||||
|
||||
p += 60
|
||||
if data['avatar_card_pool_list']:
|
||||
pool_bg = Image.open(TEXT_PATH / 'pool_bg.png')
|
||||
pool_draw = ImageDraw.Draw(pool_bg)
|
||||
|
||||
fd = data['avatar_card_pool_list'][0]
|
||||
pool_draw.text((159, 61), fd['pool_name'], 'white', gs_font_38, 'lm')
|
||||
pool_draw.text(
|
||||
(110, 61), fd['version_name'], 'white', gs_font_30, 'mm'
|
||||
)
|
||||
limit_time = '剩余时间:' + convert_timestamp_to_string(
|
||||
fd['countdown_seconds']
|
||||
)
|
||||
pool_draw.text((110, 106), limit_time, GREY, gs_font_26, 'lm')
|
||||
|
||||
for index, ap in enumerate(data['avatar_card_pool_list']):
|
||||
avatars = ap['avatars']
|
||||
|
||||
for aindex, avatar in enumerate(avatars):
|
||||
cpath = CHAR_PATH / f"{avatar['id']}.png"
|
||||
if not cpath.exists():
|
||||
await download(avatar['icon'], 1, f"{avatar['id']}.png")
|
||||
char = Image.open(cpath)
|
||||
char_bg = gen_char(char, avatar['rarity'])
|
||||
char_bg = char_bg.resize((105, 105))
|
||||
|
||||
pool_bg.paste(
|
||||
char_bg,
|
||||
(64 + aindex * 105 + index * 444, 136),
|
||||
char_bg,
|
||||
)
|
||||
img.paste(pool_bg, (0, p), pool_bg)
|
||||
p += 270
|
||||
|
||||
if data['weapon_card_pool_list']:
|
||||
pool_bg = Image.open(TEXT_PATH / 'pool_bg.png')
|
||||
pool_draw = ImageDraw.Draw(pool_bg)
|
||||
|
||||
fd = data['weapon_card_pool_list'][0]
|
||||
pool_draw.text((159, 61), fd['pool_name'], 'white', gs_font_38, 'lm')
|
||||
pool_draw.text(
|
||||
(110, 61), fd['version_name'], 'white', gs_font_30, 'mm'
|
||||
)
|
||||
limit_time = '剩余时间:' + convert_timestamp_to_string(
|
||||
fd['countdown_seconds']
|
||||
)
|
||||
pool_draw.text((110, 106), limit_time, GREY, gs_font_26, 'lm')
|
||||
|
||||
for index, wp in enumerate(data['weapon_card_pool_list']):
|
||||
weapon = wp['weapon']
|
||||
|
||||
for windex, weapon in enumerate(weapon):
|
||||
cpath = WEAPON_PATH / f"{weapon['name']}.png"
|
||||
if not cpath.exists():
|
||||
await download(weapon['icon'], 5, f"{weapon['name']}.png")
|
||||
char = Image.open(cpath)
|
||||
char_bg = gen_char(char, weapon['rarity'])
|
||||
char_bg = char_bg.resize((105, 105))
|
||||
|
||||
pool_bg.paste(
|
||||
char_bg,
|
||||
(64 + windex * 105 + index * 444, 136),
|
||||
char_bg,
|
||||
)
|
||||
img.paste(pool_bg, (0, p), pool_bg)
|
||||
p += 270
|
||||
|
||||
p += 30
|
||||
bar2 = Image.open(TEXT_PATH / 'bar2.png')
|
||||
img.paste(bar2, (0, p), bar2)
|
||||
p += 60
|
||||
|
||||
for act in data['act_list']:
|
||||
act_bg = await draw_act(act)
|
||||
img.paste(act_bg, (0, p), act_bg)
|
||||
p += 160
|
||||
|
||||
p += 30
|
||||
bar3 = Image.open(TEXT_PATH / 'bar3.png')
|
||||
img.paste(bar3, (0, p), bar3)
|
||||
p += 60
|
||||
|
||||
for fixed_act in data['fixed_act_list']:
|
||||
fixed_act_bg = await draw_act(fixed_act)
|
||||
img.paste(fixed_act_bg, (0, p), fixed_act_bg)
|
||||
p += 160
|
||||
|
||||
img = add_footer(img, 1000)
|
||||
return await convert_img(img)
|
BIN
GenshinUID/genshinuid_cale/texture2d/act_bg.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
GenshinUID/genshinuid_cale/texture2d/bar1.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
GenshinUID/genshinuid_cale/texture2d/bar2.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
GenshinUID/genshinuid_cale/texture2d/bar3.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
GenshinUID/genshinuid_cale/texture2d/icon_bg.png
Normal file
After Width: | Height: | Size: 627 B |
BIN
GenshinUID/genshinuid_cale/texture2d/icon_fg.png
Normal file
After Width: | Height: | Size: 402 B |
BIN
GenshinUID/genshinuid_cale/texture2d/no.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
GenshinUID/genshinuid_cale/texture2d/pool_bg.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
GenshinUID/genshinuid_cale/texture2d/yes.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
@ -1,6 +1,7 @@
|
||||
from gsuid_core.sv import SV
|
||||
from gsuid_core.bot import Bot
|
||||
from gsuid_core.models import Event
|
||||
from gsuid_core.logger import logger
|
||||
from gsuid_core.message_models import Button
|
||||
from gsuid_core.utils.error_reply import UID_HINT
|
||||
|
||||
@ -16,7 +17,7 @@ async def send_gcg_pic(bot: Bot, ev: Event):
|
||||
uid = await get_uid(bot, ev)
|
||||
if uid is None:
|
||||
return await bot.send(UID_HINT)
|
||||
await bot.logger.info('[七圣召唤]uid: {}'.format(uid))
|
||||
logger.info(f'[七圣召唤] uid: {uid}')
|
||||
|
||||
im = await draw_gcg_info(uid)
|
||||
await bot.send_option(im, [Button('✅我的卡组', '我的卡组')])
|
||||
@ -27,7 +28,7 @@ async def send_deck_pic(bot: Bot, ev: Event):
|
||||
uid = await get_uid(bot, ev)
|
||||
if uid is None:
|
||||
return await bot.send(UID_HINT)
|
||||
await bot.logger.info('[我的卡组]uid: {}'.format(uid))
|
||||
logger.info(f'[我的卡组] uid: {uid}')
|
||||
if not ev.text:
|
||||
deck_id = 1
|
||||
elif ev.text.strip().isdigit():
|
||||
|
@ -42,7 +42,8 @@ async def send_abyss_pic(bot: Bot, ev: Event):
|
||||
|
||||
|
||||
@sv_get_abyss_database.on_fullmatch(
|
||||
('深渊队伍', '深渊队伍统计', '深渊队伍推荐', '深渊组队'), block=True
|
||||
('深渊队伍', '深渊队伍统计', '深渊队伍推荐', '深渊组队', '深渊配队'),
|
||||
block=True,
|
||||
)
|
||||
async def send_abyss_team_pic(bot: Bot, ev: Event):
|
||||
# await draw_xk_abyss_img()
|
||||
|
@ -46,6 +46,14 @@ async def get_teyvat_title(data: TeyvatAbyssRank):
|
||||
return title
|
||||
|
||||
|
||||
def gen_char(char: Image.Image, char_star: int):
|
||||
char = char.resize((128, 128))
|
||||
char = char.convert('RGBA')
|
||||
char_bg = Image.open(TEXT_PATH / f'char{char_star}_bg.png')
|
||||
char_bg.paste(char, (11, 11), char)
|
||||
return char_bg
|
||||
|
||||
|
||||
async def draw_teyvat_team_img():
|
||||
data = await teyvat_api.get_abyss_rank()
|
||||
if isinstance(data, int):
|
||||
@ -92,9 +100,7 @@ async def draw_teyvat_team_img():
|
||||
await download(icon_url, 16, char_name)
|
||||
|
||||
char = Image.open(char_path).convert('RGBA')
|
||||
char = char.resize((128, 128))
|
||||
char_bg = Image.open(TEXT_PATH / f'char{role["star"]}_bg.png')
|
||||
char_bg.paste(char, (11, 11), char)
|
||||
char_bg = gen_char(char, role["star"])
|
||||
bar.paste(char_bg, (55 + rindex * 150, 20), char_bg)
|
||||
|
||||
bar_draw = ImageDraw.Draw(bar)
|
||||
|
@ -1,4 +1,5 @@
|
||||
base_url = 'https://api-takumi-record.mihoyo.com'
|
||||
widget_url = f'{base_url}/game_record/genshin/aapi/widget/v2'
|
||||
new_abyss_url = f'{base_url}/game_record/app/genshin/api/role_combat'
|
||||
char_detail_url = f'{base_url}/game_record/app/genshin/api/character/detail'
|
||||
widget_url = '/game_record/genshin/aapi/widget/v2'
|
||||
new_abyss_url = '/game_record/app/genshin/api/role_combat'
|
||||
char_detail_url = '/game_record/app/genshin/api/character/detail'
|
||||
|
||||
calendar_url = '/game_record/app/genshin/api/act_calendar'
|
||||
|
@ -1,4 +1,114 @@
|
||||
from typing import List, Literal, TypedDict
|
||||
from typing import List, Literal, Optional, TypedDict
|
||||
|
||||
|
||||
class PoetryAbyssDateTime(TypedDict):
|
||||
year: int
|
||||
month: int
|
||||
day: int
|
||||
hour: int
|
||||
minute: int
|
||||
second: int
|
||||
|
||||
|
||||
class AvatarPool(TypedDict):
|
||||
id: int
|
||||
icon: str
|
||||
name: str
|
||||
element: str
|
||||
rarity: int
|
||||
is_invisible: bool
|
||||
|
||||
|
||||
class WeaponPool(TypedDict):
|
||||
id: int
|
||||
icon: str
|
||||
rarity: int
|
||||
name: str
|
||||
wiki_url: str
|
||||
|
||||
|
||||
class RewardItem(TypedDict):
|
||||
item_id: int
|
||||
name: str
|
||||
icon: str
|
||||
wiki_url: str
|
||||
num: int
|
||||
rarity: str
|
||||
homepage_show: bool
|
||||
|
||||
|
||||
class Act(TypedDict):
|
||||
id: int
|
||||
name: str
|
||||
type: str
|
||||
start_timestamp: int
|
||||
start_time: PoetryAbyssDateTime
|
||||
end_timestamp: int
|
||||
end_time: PoetryAbyssDateTime
|
||||
desc: str
|
||||
strategy: str
|
||||
countdown_seconds: int
|
||||
status: int
|
||||
reward_list: List[RewardItem]
|
||||
is_finished: bool
|
||||
|
||||
|
||||
class RoleCombatDetail(TypedDict):
|
||||
is_unlock: bool
|
||||
max_round_id: int
|
||||
has_data: bool
|
||||
|
||||
|
||||
class TowerDetail(TypedDict):
|
||||
is_unlock: bool
|
||||
max_star: int
|
||||
total_star: int
|
||||
has_data: bool
|
||||
|
||||
|
||||
class FixedAct(TypedDict):
|
||||
id: int
|
||||
name: str
|
||||
type: str
|
||||
start_timestamp: int
|
||||
start_time: PoetryAbyssDateTime
|
||||
end_timestamp: int
|
||||
end_time: PoetryAbyssDateTime
|
||||
desc: str
|
||||
strategy: str
|
||||
countdown_seconds: int
|
||||
status: int
|
||||
reward_list: List[RewardItem]
|
||||
is_finished: bool
|
||||
role_combat_detail: Optional[RoleCombatDetail]
|
||||
tower_detail: Optional[TowerDetail]
|
||||
|
||||
|
||||
class Pool(TypedDict):
|
||||
pool_id: int
|
||||
version_name: str
|
||||
pool_name: str
|
||||
pool_type: int
|
||||
avatars: List[AvatarPool]
|
||||
weapon: List[WeaponPool]
|
||||
start_timestamp: int
|
||||
start_time: PoetryAbyssDateTime
|
||||
end_timestamp: int
|
||||
end_time: PoetryAbyssDateTime
|
||||
jump_url: str
|
||||
pool_status: int
|
||||
countdown_seconds: int
|
||||
|
||||
|
||||
class CalendarData(TypedDict):
|
||||
avatar_card_pool_list: List[Pool]
|
||||
weapon_card_pool_list: List[Pool]
|
||||
mixed_card_pool_list: List[Pool]
|
||||
selected_avatar_card_pool_list: List[Pool]
|
||||
selected_mixed_card_pool_list: List[Pool]
|
||||
act_list: List[Act]
|
||||
fixed_act_list: List[FixedAct] # Updated to include FixedAct
|
||||
selected_act_list: List[Act]
|
||||
|
||||
|
||||
class Expedition(TypedDict):
|
||||
@ -109,15 +219,6 @@ class PoetryAbyssBuff(TypedDict):
|
||||
id: int
|
||||
|
||||
|
||||
class PoetryAbyssDateTime(TypedDict):
|
||||
year: int
|
||||
month: int
|
||||
day: int
|
||||
hour: int
|
||||
minute: int
|
||||
second: int
|
||||
|
||||
|
||||
class PoetryAbyssSchedule(TypedDict):
|
||||
start_time: int
|
||||
end_time: int
|
||||
|
@ -2,10 +2,11 @@ from copy import deepcopy
|
||||
from typing import Dict, List, Union, cast
|
||||
|
||||
from gsuid_core.utils.api.mys_api import _MysApi
|
||||
from gsuid_core.utils.api.mys.api import RECORD_BASE, RECORD_BASE_OS
|
||||
from gsuid_core.utils.api.mys.tools import get_ds_token, get_web_ds_token
|
||||
|
||||
from .api import widget_url, new_abyss_url, char_detail_url
|
||||
from .models import Character, WidgetResin, PoetryAbyssDatas
|
||||
from .api import widget_url, calendar_url, new_abyss_url, char_detail_url
|
||||
from .models import Character, WidgetResin, CalendarData, PoetryAbyssDatas
|
||||
|
||||
|
||||
class GsMysAPI(_MysApi):
|
||||
@ -14,6 +15,26 @@ class GsMysAPI(_MysApi):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
async def get_calendar_data(self, uid: str) -> Union[CalendarData, int]:
|
||||
server_id = self.RECOGNIZE_SERVER.get(uid[0])
|
||||
header = deepcopy(self._HEADER)
|
||||
ck = await self.get_ck(uid, 'OWNER')
|
||||
if ck is None:
|
||||
return -51
|
||||
header['Cookie'] = ck
|
||||
base = RECORD_BASE_OS if self.check_os(uid) else RECORD_BASE
|
||||
data = await self._mys_request(
|
||||
calendar_url,
|
||||
'POST',
|
||||
header,
|
||||
data={"role_id": uid, "server": server_id},
|
||||
base_url=base,
|
||||
)
|
||||
|
||||
if isinstance(data, Dict):
|
||||
data = cast(CalendarData, data['data'])
|
||||
return data
|
||||
|
||||
async def get_widget_resin_data(self, uid: str) -> Union[WidgetResin, int]:
|
||||
header = deepcopy(self._HEADER)
|
||||
sk = await self.get_stoken(uid)
|
||||
@ -22,8 +43,13 @@ class GsMysAPI(_MysApi):
|
||||
header['Cookie'] = sk
|
||||
header['DS'] = get_web_ds_token(True)
|
||||
header['x-rpc-channel'] = 'miyousheluodi'
|
||||
base = RECORD_BASE_OS if self.check_os(uid) else RECORD_BASE
|
||||
data = await self._mys_request(
|
||||
widget_url, 'GET', header, {'game_id': 2}
|
||||
widget_url,
|
||||
'GET',
|
||||
header,
|
||||
{'game_id': 2},
|
||||
base_url=base,
|
||||
)
|
||||
if isinstance(data, Dict):
|
||||
data = cast(WidgetResin, data['data'])
|
||||
@ -33,6 +59,7 @@ class GsMysAPI(_MysApi):
|
||||
self, uid: str
|
||||
) -> Union[PoetryAbyssDatas, int]:
|
||||
server_id = self.RECOGNIZE_SERVER.get(uid[0])
|
||||
base = RECORD_BASE_OS if self.check_os(uid) else RECORD_BASE
|
||||
HEADER = deepcopy(self._HEADER)
|
||||
ck = await self.get_ck(uid, 'OWNER')
|
||||
if ck is None:
|
||||
@ -51,6 +78,7 @@ class GsMysAPI(_MysApi):
|
||||
'GET',
|
||||
HEADER,
|
||||
params,
|
||||
base_url=base,
|
||||
)
|
||||
if isinstance(data, Dict):
|
||||
data = cast(PoetryAbyssDatas, data['data'])
|
||||
@ -66,6 +94,7 @@ class GsMysAPI(_MysApi):
|
||||
return -51
|
||||
HEADER['Cookie'] = ck
|
||||
|
||||
base = RECORD_BASE_OS if self.check_os(uid) else RECORD_BASE
|
||||
data = await self._mys_request(
|
||||
char_detail_url,
|
||||
'POST',
|
||||
@ -75,6 +104,7 @@ class GsMysAPI(_MysApi):
|
||||
"server": server_id,
|
||||
"character_ids": char_id_list,
|
||||
},
|
||||
base_url=base,
|
||||
)
|
||||
|
||||
if isinstance(data, Dict):
|
||||
|