diff --git a/core/assets/__init__.py b/core/assets/__init__.py new file mode 100644 index 0000000..2ff27ec --- /dev/null +++ b/core/assets/__init__.py @@ -0,0 +1,8 @@ +from core.service import init_service +from .service import AssetsService + + +@init_service +def create_wiki_service(): + _service = AssetsService() + return _service diff --git a/core/assets/service.py b/core/assets/service.py new file mode 100644 index 0000000..acf9052 --- /dev/null +++ b/core/assets/service.py @@ -0,0 +1,164 @@ +from abc import ABC, abstractmethod +from pathlib import Path +from ssl import SSLZeroReturnError +from typing import ClassVar, Optional, Union + +from aiofiles import open as async_open +from httpx import AsyncClient, HTTPError + +from core.service import Service +from metadata.honey import HONEY_RESERVED_ID_MAP +from metadata.shortname import roleToId, roles +from modules.wiki.base import SCRAPE_HOST +from utils.const import PROJECT_ROOT +from utils.helpers import mkdir +from utils.typedefs import StrOrURL + +ASSETS_PATH = PROJECT_ROOT.joinpath('resources/assets') +ASSETS_PATH.mkdir(exist_ok=True) + + +class _AssetsService(ABC): + _dir: ClassVar[Path] + + id: str + type: str + + @property + def path(self) -> Path: + return mkdir(self._dir.joinpath(self.id)) + + def __init__(self, client: AsyncClient): + self._client = client + + @abstractmethod + def __call__(self, target): + pass + + def __init_subclass__(cls, **kwargs): + cls.type = cls.__name__.lstrip('_').split('Assets')[0].lower() + cls._dir = ASSETS_PATH.joinpath(cls.type) + cls._dir.mkdir(exist_ok=True) + return cls + + async def _download(self, url: StrOrURL, path: Path, retry: int = 5) -> Optional[Path]: + import asyncio + for _ in range(retry): + try: + response = await self._client.get(url, follow_redirects=False) + except (HTTPError, SSLZeroReturnError): + await asyncio.sleep(1) + continue + if response.status_code != 200: + return None + async with async_open(path, 'wb') as file: + await file.write(response.content) + return path + + @abstractmethod + async def icon(self) -> Path: + pass + + +class _CharacterAssets(_AssetsService): + # noinspection SpellCheckingInspection + def __call__(self, target: Union[str, int]) -> "_CharacterAssets": + if isinstance(target, int): + if target == 10000005: + self.id = 'playerboy_005' + elif target == 10000007: + self.id = 'playergirl_007' + else: + self.id = f"{roles[target][2]}_{str(target)[-3:]}" + elif not target[-1].isdigit(): + target = roleToId(target) + self.id = f"{roles[target][2]}_{str(target)[-3:]}" + else: + self.id = target + return self + + async def icon(self) -> Path: + if (path := self.path.joinpath('icon.webp')).exists(): + return path + + return await self._download(SCRAPE_HOST.join(SCRAPE_HOST.join(f'/img/{self.id}_icon.webp')), path) + + async def side(self) -> Path: + if (path := self.path.joinpath('side.webp')).exists(): + return path + + return await self._download(SCRAPE_HOST.join(SCRAPE_HOST.join(f'/img/{self.id}_side_icon.webp')), path) + + async def gacha(self) -> Path: + if (path := self.path.joinpath('gacha.webp')).exists(): + return path + + return await self._download(SCRAPE_HOST.join(SCRAPE_HOST.join(f'/img/{self.id}_gacha_card.webp')), path) + + async def splash(self) -> Optional[Path]: + if (path := self.path.joinpath('splash.webp')).exists(): + return path + + return await self._download(SCRAPE_HOST.join(SCRAPE_HOST.join(f'/img/{self.id}_gacha_splash.webp')), path) + + +class _WeaponAssets(_AssetsService): + def __call__(self, target: str) -> '_WeaponAssets': + if not target[-1].isdigit(): + self.id = HONEY_RESERVED_ID_MAP['weapon'][target][0] + else: + self.id = target + return self + + async def icon(self) -> Path: + if (path := self.path.joinpath('icon.webp')).exists(): + return path + + return await self._download(SCRAPE_HOST.join(SCRAPE_HOST.join(f'/img/{self.id}.webp')), path) + + async def awakened(self) -> Path: + if (path := self.path.joinpath('awakened.webp')).exists(): + return path + + return await self._download(SCRAPE_HOST.join(SCRAPE_HOST.join(f'/img/{self.id}_awaken_icon.webp')), path) + + async def gacha(self) -> Path: + if (path := self.path.joinpath('gacha.webp')).exists(): + return path + + return await self._download(SCRAPE_HOST.join(SCRAPE_HOST.join(f'/img/{self.id}_gacha_icon.webp')), path) + + +class _MaterialAssets(_AssetsService): + + def __call__(self, target) -> "_MaterialAssets": + if not target[-1].isdigit(): + self.id = HONEY_RESERVED_ID_MAP['material'][target][0] + else: + self.id = target + return self + + async def icon(self) -> Path: + if (path := self.path.joinpath('icon.webp')).exists(): + return path + + return await self._download(SCRAPE_HOST.join(SCRAPE_HOST.join(f'/img/{self.id}.webp')), path) + + +class AssetsService(Service): + """asset服务 + + 用于储存和管理 asset : + 当对应的 asset (如某角色图标)不存在时,该服务会先查找本地。 + 若本地不存在,则从网络上下载;若存在,则返回其路径 + """ + + character: _CharacterAssets + weapon: _WeaponAssets + material: _MaterialAssets + + def __init__(self): + self.client = AsyncClient() + self.character = _CharacterAssets(self.client) + self.weapon = _WeaponAssets(self.client) + self.material = _MaterialAssets(self.client) diff --git a/core/bot.py b/core/bot.py index 20a7b11..5a0c11f 100644 --- a/core/bot.py +++ b/core/bot.py @@ -1,14 +1,23 @@ import asyncio import inspect import os +from asyncio import CancelledError from importlib import import_module from multiprocessing import RLock as Lock from pathlib import Path from typing import Any, Callable, ClassVar, Dict, Iterator, List, NoReturn, Optional, TYPE_CHECKING, Type, TypeVar import pytz +from async_timeout import timeout from telegram.error import NetworkError, TimedOut -from telegram.ext import AIORateLimiter, Application as TgApplication, Defaults, JobQueue, MessageHandler +from telegram.ext import ( + AIORateLimiter, + Application as TgApplication, + CallbackContext, + Defaults, + JobQueue, + MessageHandler, +) from telegram.ext.filters import StatusUpdate from core.config import BotConfig, config # pylint: disable=W0611 @@ -21,7 +30,6 @@ from utils.log import logger if TYPE_CHECKING: from telegram import Update - from telegram.ext import CallbackContext __all__ = ['bot'] @@ -45,12 +53,7 @@ class Bot: _services: Dict[Type[T], T] = {} _running: bool = False - def init_inject(self, target: Callable[..., T]) -> T: - """用于实例化Plugin的方法。用于给插件传入一些必要组件,如 MySQL、Redis等""" - if isinstance(target, type): - signature = inspect.signature(target.__init__) - else: - signature = inspect.signature(target) + def _inject(self, signature: inspect.Signature, target: Callable[..., T]) -> T: kwargs = {} for name, parameter in signature.parameters.items(): if name != 'self' and parameter.annotation != inspect.Parameter.empty: @@ -58,6 +61,17 @@ class Bot: kwargs[name] = value return target(**kwargs) + def init_inject(self, target: Callable[..., T]) -> T: + """用于实例化Plugin的方法。用于给插件传入一些必要组件,如 MySQL、Redis等""" + if isinstance(target, type): + signature = inspect.signature(target.__init__) + else: + signature = inspect.signature(target) + return self._inject(signature, target) + + async def async_inject(self, target: Callable[..., T]) -> T: + return await self._inject(inspect.signature(target), target) + def _gen_pkg(self, root: Path) -> Iterator[str]: """生成可以用于 import_module 导入的字符串""" for path in root.iterdir(): @@ -84,7 +98,7 @@ class Bot: try: plugin: PluginType = self.init_inject(plugin_cls) if hasattr(plugin, '__async_init__'): - await plugin.__async_init__() + await self.async_inject(plugin.__async_init__) handlers = plugin.handlers self.app.add_handlers(handlers) if handlers: @@ -170,18 +184,22 @@ class Bot: if not self._services: return logger.info('正在关闭服务') - for _, service in self._services.items(): - try: - if hasattr(service, 'stop'): - if inspect.iscoroutinefunction(service.stop): - await service.stop() - else: - service.stop() - logger.success(f'服务 "{service.__class__.__name__}" 关闭成功') - except Exception as e: # pylint: disable=W0703 - logger.exception(f"服务 \"{service.__class__.__name__}\" 关闭失败: \n{type(e).__name__}: {e}") + for _, service in filter(lambda x: not isinstance(x[1], TgApplication), self._services.items()): + async with timeout(5): + try: + if hasattr(service, 'stop'): + if inspect.iscoroutinefunction(service.stop): + await service.stop() + else: + service.stop() + logger.success(f'服务 "{service.__class__.__name__}" 关闭成功') + except CancelledError: + logger.warning(f'服务 "{service.__class__.__name__}" 关闭超时') + except Exception as e: # pylint: disable=W0703 + logger.exception(f"服务 \"{service.__class__.__name__}\" 关闭失败: \n{type(e).__name__}: {e}") - async def _post_init(self, _) -> NoReturn: + async def _post_init(self, context: CallbackContext) -> NoReturn: + self._services.update({CallbackContext: context}) logger.info('开始初始化服务') await self.start_services() logger.info('开始安装插件') diff --git a/metadata/honey.py b/metadata/honey.py new file mode 100644 index 0000000..3608f28 --- /dev/null +++ b/metadata/honey.py @@ -0,0 +1,464 @@ +"""此文件用于储存 honey impact 中的部分基础数据""" +__all__ = [ + 'HONEY_ID_MAP', 'HONEY_RESERVED_ID_MAP', + 'HONEY_ROLE_NAME_MAP' +] + +# noinspection SpellCheckingInspection +HONEY_ID_MAP = { + 'character': { + 'ayaka_002': ['神里绫华', 5], + 'xiangling_023': ['香菱', 4], + 'xingqiu_025': ['行秋', 4], + 'albedo_038': ['阿贝多', 5], + 'lisa_006': ['丽莎', 4], + 'sucrose_043': ['砂糖', 4], + 'mona_041': ['莫娜', 5], + 'diona_039': ['迪奥娜', 4], + 'venti_022': ['温迪', 5], + 'xinyan_044': ['辛焱', 4], + 'rosaria_045': ['罗莎莉亚', 4], + 'hutao_046': ['胡桃', 5], + 'zhongli_030': ['钟离', 5], + 'ningguang_027': ['凝光', 4], + 'eula_051': ['优菈', 5], + 'shougun_052': ['雷电将军', 5], + 'sayu_053': ['早柚', 4], + 'keqing_042': ['刻晴', 5], + 'ganyu_037': ['甘雨', 5], + 'gorou_055': ['五郎', 4], + 'tartaglia_033': ['达达利亚', 5], + 'beidou_024': ['北斗', 4], + 'itto_057': ['荒泷一斗', 5], + 'ambor_021': ['安柏', 4], + 'diluc_016': ['迪卢克', 5], + 'chongyun_036': ['重云', 4], + 'kaeya_015': ['凯亚', 4], + 'aloy_062': ['埃洛伊', 4], + 'yunjin_064': ['云堇', 4], + 'shinobu_065': ['久岐忍', 4], + 'ayato_066': ['神里绫人', 5], + 'collei_067': ['柯莱', 4], + 'feiyan_048': ['烟绯', 4], + 'razor_020': ['雷泽', 4], + 'barbara_014': ['芭芭拉', 4], + 'dori_068': ['多莉', 4], + 'noel_034': ['诺艾尔', 4], + 'tighnari_069': ['提纳里', 5], + 'kazuha_047': ['枫原万叶', 5], + 'qiqi_035': ['七七', 5], + 'bennett_032': ['班尼特', 4], + 'nilou_070': ['妮露', 5], + 'fischl_031': ['菲谢尔', 4], + 'klee_029': ['可莉', 5], + 'cyno_071': ['赛诺', 5], + 'candace_072': ['坎蒂丝', 4], + 'qin_003': ['琴', 5], + 'xiao_026': ['魈', 5], + 'playergirl_007': ['荧', 5], + 'heizo_059': ['鹿野院平藏', 4], + 'yoimiya_049': ['宵宫', 5], + 'playerboy_005': ['空', 5], + 'sara_056': ['九条裟罗', 4], + 'tohma_050': ['托马', 4], + 'kokomi_054': ['珊瑚宫心海', 5], + 'shenhe_063': ['申鹤', 5], + 'yae_058': ['八重神子', 5], + 'yelan_060': ['夜兰', 5] + }, + 'weapon': { + 'i_n11401': ['西风剑', 4], + 'i_n11305': ['吃虎鱼刀', 3], + 'i_n11101': ['无锋剑', 1], + 'i_n11303': ['旅行剑', 3], + 'i_n11410': ['暗巷闪光', 4], + 'i_n11301': ['冷刃', 3], + 'i_n11416': ['笼钓瓶一心', 4], + 'i_n11407': ['铁蜂刺', 4], + 'i_n11501': ['风鹰剑', 5], + 'i_n11419': ['「一心传」名刀', 4], + 'i_n13409': ['龙脊长枪', 4], + 'i_n13406': ['千岩长枪', 4], + 'i_n11412': ['降临之剑', 4], + 'i_n13505': ['和璞鸢', 5], + 'i_n11504': ['斫峰之刃', 5], + 'i_n11417': ['原木刀', 4], + 'i_n13101': ['新手长枪', 1], + 'i_n11509': ['雾切之回光', 5], + 'i_n11502': ['天空之刃', 5], + 'i_n14304': ['翡玉法球', 3], + 'i_n13401': ['匣里灭辰', 4], + 'i_n11413': ['腐殖之剑', 4], + 'i_n11404': ['宗室长剑', 4], + 'i_n11418': ['西福斯的月光', 4], + 'i_n11415': ['辰砂之纺锤', 4], + 'i_n11503': ['苍古自由之誓', 5], + 'i_n13402': ['试作星镰', 4], + 'i_n11511': ['圣显之钥', 5], + 'i_n11409': ['黑剑', 4], + 'i_n11414': ['天目影打刀', 4], + 'i_n11405': ['匣里龙吟', 4], + 'i_n11510': ['波乱月白经津', 5], + 'i_n13405': ['决斗之枪', 4], + 'i_n13407': ['西风长枪', 4], + 'i_n11408': ['黑岩长剑', 4], + 'i_n14306': ['琥珀玥', 3], + 'i_n11505': ['磐岩结绿', 5], + 'i_n14408': ['黑岩绯玉', 4], + 'i_n14417': ['盈满之实', 4], + 'i_n14416': ['流浪的晚星', 4], + 'i_n12301': ['铁影阔剑', 3], + 'i_n14506': ['不灭月华', 5], + 'i_n14305': ['甲级宝珏', 3], + 'i_n13415': ['「渔获」', 4], + 'i_n14402': ['流浪乐章', 4], + 'i_n12402': ['钟剑', 4], + 'i_n12201': ['佣兵重剑', 2], + 'i_n14403': ['祭礼残章', 4], + 'i_n14405': ['匣里日月', 4], + 'i_n12101': ['训练大剑', 1], + 'i_n14501': ['天空之卷', 5], + 'i_n14413': ['嘟嘟可故事集', 4], + 'i_n14302': ['讨龙英杰谭', 3], + 'i_n14303': ['异世界行记', 3], + 'i_n12303': ['白铁大剑', 3], + 'i_n14409': ['昭心', 4], + 'i_n13502': ['天空之脊', 5], + 'i_n14404': ['宗室秘法录', 4], + 'i_n14401': ['西风秘典', 4], + 'i_n14415': ['证誓之明瞳', 4], + 'i_n13301': ['白缨枪', 3], + 'i_n13404': ['黑岩刺枪', 4], + 'i_n13408': ['宗室猎枪', 4], + 'i_n13201': ['铁尖枪', 2], + 'i_n13511': ['赤沙之杖', 5], + 'i_n13416': ['断浪长鳍', 4], + 'i_n13509': ['薙草之稻光', 5], + 'i_n13403': ['流月针', 4], + 'i_n13417': ['贯月矢', 4], + 'i_n13419': ['风信之锋', 4], + 'i_n13302': ['钺矛', 3], + 'i_n11406': ['试作斩岩', 4], + 'i_n13414': ['喜多院十文字', 4], + 'i_n14201': ['口袋魔导书', 2], + 'i_n13501': ['护摩之杖', 5], + 'i_n13303': ['黑缨枪', 3], + 'i_n14101': ['学徒笔记', 1], + 'i_n12401': ['西风大剑', 4], + 'i_n12304': ['石英大剑', 3], + 'i_n14412': ['忍冬之果', 4], + 'i_n14414': ['白辰之环', 4], + 'i_n14509': ['神乐之真意', 5], + 'i_n14406': ['试作金珀', 4], + 'i_n14502': ['四风原典', 5], + 'i_n12305': ['以理服人', 3], + 'i_n14504': ['尘世之锁', 5], + 'i_n12306': ['飞天大御剑', 3], + 'i_n14301': ['魔导绪论', 3], + 'i_n12302': ['沐浴龙血的剑', 3], + 'i_n14407': ['万国诸海图谱', 4], + 'i_n13504': ['贯虹之槊', 5], + 'i_n12416': ['恶王丸', 4], + 'i_n12409': ['螭骨剑', 4], + 'i_n12404': ['宗室大剑', 4], + 'i_n12405': ['雨裁', 4], + 'i_n12414': ['桂木斩长正', 4], + 'i_n12408': ['黑岩斩刀', 4], + 'i_n12410': ['千岩古剑', 4], + 'i_n12406': ['试作古华', 4], + 'i_n12415': ['玛海菈的水色', 4], + 'i_n12403': ['祭礼大剑', 4], + 'i_n12411': ['雪葬的星银', 4], + 'i_n12412': ['衔珠海皇', 4], + 'i_n12407': ['白影剑', 4], + 'i_n11201': ['银剑', 2], + 'i_n12504': ['无工之剑', 5], + 'i_n15305': ['信使', 3], + 'i_n15411': ['落霞', 4], + 'i_n15413': ['风花之颂', 4], + 'i_n15401': ['西风猎弓', 4], + 'i_n12510': ['赤角石溃杵', 5], + 'i_n15405': ['弓藏', 4], + 'i_n15403': ['祭礼弓', 4], + 'i_n15201': ['历练的猎弓', 2], + 'i_n15404': ['宗室长弓', 4], + 'i_n15302': ['神射手之誓', 3], + 'i_n12501': ['天空之傲', 5], + 'i_n15402': ['绝弦', 4], + 'i_n12417': ['森林王器', 4], + 'i_n11306': ['飞天御剑', 3], + 'i_n15410': ['暗巷猎手', 4], + 'i_n15414': ['破魔之弓', 4], + 'i_n15101': ['猎弓', 1], + 'i_n15415': ['掠食者', 4], + 'i_n15301': ['鸦羽弓', 3], + 'i_n11304': ['暗铁剑', 3], + 'i_n15303': ['反曲弓', 3], + 'i_n15306': ['黑檀弓', 3], + 'i_n15408': ['黑岩战弓', 4], + 'i_n15304': ['弹弓', 3], + 'i_n15409': ['苍翠猎弓', 4], + 'i_n15412': ['幽夜华尔兹', 4], + 'i_n15406': ['试作澹月', 4], + 'i_n15417': ['王下近侍', 4], + 'i_n15501': ['天空之翼', 5], + 'i_n15418': ['竭泽', 4], + 'i_n15507': ['冬极白星', 5], + 'i_n15508': ['若水', 5], + 'i_n15503': ['终末嗟叹之诗', 5], + 'i_n15509': ['飞雷之弦振', 5], + 'i_n15511': ['猎人之径', 5], + 'i_n12502': ['狼的末路', 5], + 'i_n15407': ['钢轮弓', 4], + 'i_n12503': ['松籁响起之时', 5], + 'i_n15416': ['曚云之月', 4], + 'i_n11402': ['笛剑', 4], + 'i_n15502': ['阿莫斯之弓', 5], + 'i_n11302': ['黎明神剑', 3], + 'i_n13507': ['息灾', 5], + 'i_n11403': ['祭礼剑', 4], + 'i_n14410': ['暗巷的酒与诗', 4] + }, + 'material': { + 'i_413': ['「勤劳」的哲学', 4], + 'i_411': ['「勤劳」的教导', 2], + 'i_n104333': ['「巧思」的指引', 3], + 'i_584': ['今昔剧画之鬼人', 5], + 'i_427': ['「天光」的指引', 3], + 'i_453': ['「抗争」的哲学', 4], + 'i_n112063': ['休眠菌核', 3], + 'i_408': ['「浮世」的哲学', 4], + 'i_n104336': ['「笃行」的指引', 3], + 'i_n104337': ['「笃行」的哲学', 4], + 'i_421': ['「自由」的教导', 2], + 'i_n112068': ['混沌容器', 2], + 'i_n104331': ['「诤言」的哲学', 4], + 'i_n104330': ['「诤言」的指引', 3], + 'i_402': ['「诗文」的指引', 3], + 'i_416': ['「风雅」的教导', 2], + 'i_423': ['「自由」的哲学', 4], + 'i_407': ['「浮世」的指引', 3], + 'i_581': ['今昔剧画之恶尉', 2], + 'i_401': ['「诗文」的教导', 2], + 'i_441': ['「繁荣」的教导', 2], + 'i_n104335': ['「笃行」的教导', 2], + 'i_422': ['「自由」的指引', 3], + 'i_432': ['「黄金」的指引', 3], + 'i_n104332': ['「巧思」的教导', 2], + 'i_53': ['历战的箭簇', 3], + 'i_417': ['「风雅」的指引', 3], + 'i_431': ['「黄金」的教导', 2], + 'i_403': ['「诗文」的哲学', 4], + 'i_451': ['「抗争」的教导', 2], + 'i_462': ['东风之爪', 5], + 'i_461': ['东风之翎', 5], + 'i_524': ['凛风奔狼的怀乡', 5], + 'i_433': ['「黄金」的哲学', 4], + 'i_406': ['「浮世」的教导', 2], + 'i_452': ['「抗争」的指引', 3], + 'i_n104334': ['「巧思」的哲学', 4], + 'i_133': ['原素花蜜', 3], + 'i_582': ['今昔剧画之虎啮', 3], + 'i_442': ['「繁荣」的指引', 3], + 'i_483': ['凶将之手眼', 5], + 'i_583': ['今昔剧画之一角', 4], + 'i_418': ['「风雅」的哲学', 4], + 'i_463': ['东风的吐息', 5], + 'i_443': ['「繁荣」的哲学', 4], + 'i_521': ['凛风奔狼的始龀', 2], + 'i_n104329': ['「诤言」的教导', 2], + 'i_464': ['北风之尾', 5], + 'i_61': ['沉重号角', 2], + 'i_523': ['凛风奔狼的断牙', 4], + 'i_485': ['万劫之真意', 5], + 'i_33': ['不祥的面具', 3], + 'i_467': ['吞天之鲸·只角', 5], + 'i_465': ['北风之环', 5], + 'i_21': ['史莱姆凝液', 1], + 'i_513': ['孤云寒林的圣骸', 4], + 'i_185': ['浮游干核', 1], + 'i_511': ['孤云寒林的光砂', 2], + 'i_522': ['凛风奔狼的裂齿', 3], + 'i_514': ['孤云寒林的神体', 5], + 'i_412': ['「勤劳」的指引', 3], + 'i_466': ['北风的魂匣', 5], + 'i_173': ['混沌真眼', 4], + 'i_73': ['地脉的新芽', 4], + 'i_183': ['偏光棱镜', 4], + 'i_n112061': ['孢囊晶尘', 3], + 'i_83': ['混沌炉心', 4], + 'i_142': ['结实的骨片', 3], + 'i_22': ['史莱姆清', 2], + 'i_112': ['士官的徽记', 2], + 'i_23': ['史莱姆原浆', 3], + 'i_163': ['名刀镡', 3], + 'i_n112062': ['失活菌核', 2], + 'i_n112072': ['混浊棱晶', 3], + 'i_428': ['「天光」的哲学', 4], + 'i_552': ['漆黑陨铁的一片', 3], + 'i_172': ['混沌枢纽', 3], + 'i_72': ['地脉的枯叶', 3], + 'i_426': ['「天光」的教导', 2], + 'i_n112070': ['混沌锚栓', 4], + 'i_491': ['智识之冕', 5], + 'i_123': ['攫金鸦印', 3], + 'i_121': ['寻宝鸦印', 1], + 'i_153': ['幽邃刻像', 4], + 'i_41': ['导能绘卷', 1], + 'i_187': ['浮游晶化核', 3], + 'i_32': ['污秽的面具', 2], + 'i_469': ['武炼之魂·孤影', 5], + 'i_553': ['漆黑陨铁的一角', 4], + 'i_151': ['晦暗刻像', 2], + 'i_182': ['水晶棱镜', 3], + 'i_71': ['地脉的旧枝', 2], + 'i_512': ['孤云寒林的辉岩', 3], + 'i_42': ['封魔绘卷', 2], + 'i_132': ['微光花蜜', 2], + 'i_152': ['夤夜刻像', 3], + 'i_186': ['浮游幽核', 2], + 'i_482': ['灰烬之心', 5], + 'i_81': ['混沌装置', 2], + 'i_82': ['混沌回路', 3], + 'i_n114045': ['烈日威权的残响', 2], + 'i_541': ['狮牙斗士的枷锁', 2], + 'i_n114046': ['烈日威权的余光', 3], + 'i_171': ['混沌机关', 2], + 'i_51': ['牢固的箭簇', 1], + 'i_542': ['狮牙斗士的铁链', 3], + 'i_111': ['新兵的徽记', 1], + 'i_n112069': ['混沌模块', 3], + 'i_n114048': ['烈日威权的旧日', 5], + 'i_162': ['影打刀镡', 2], + 'i_481': ['狱火之蝶', 5], + 'i_554': ['漆黑陨铁的一块', 5], + 'i_n112071': ['破缺棱晶', 2], + 'i_143': ['石化的骨片', 4], + 'i_103': ['督察长祭刀', 4], + 'i_31': ['破损的面具', 1], + 'i_43': ['禁咒绘卷', 3], + 'i_n114043': ['绿洲花园的哀思', 4], + 'i_n112067': ['织金红绸', 3], + 'i_n114042': ['绿洲花园的恩惠', 3], + 'i_n114044': ['绿洲花园的真谛', 5], + 'i_n112060': ['荧光孢粉', 2], + 'i_122': ['藏银鸦印', 2], + 'i_n112065': ['褪色红绸', 1], + 'i_141': ['脆弱的骨片', 2], + 'i_n114040': ['谧林涓露的金符', 5], + 'i_n114039': ['谧林涓露的银符', 4], + 'i_562': ['远海夷地的玉枝', 3], + 'i_563': ['远海夷地的琼枝', 4], + 'i_564': ['远海夷地的金枝', 5], + 'i_n114038': ['谧林涓露的铁符', 3], + 'i_n114037': ['谧林涓露的铜符', 2], + 'i_52': ['锐利的箭簇', 2], + 'i_n112066': ['镶边红绸', 2], + 'i_174': ['隐兽指爪', 2], + 'i_176': ['隐兽鬼爪', 4], + 'i_534': ['雾海云间的转还', 5], + 'i_531': ['雾海云间的铅丹', 2], + 'i_503': ['高塔孤王的断片', 4], + 'i_91': ['雾虚花粉', 2], + 'i_92': ['雾虚草囊', 3], + 'i_501': ['高塔孤王的破瓦', 2], + 'i_504': ['高塔孤王的碎梦', 5], + 'i_468': ['魔王之刃·残片', 5], + 'i_544': ['狮牙斗士的理想', 5], + 'i_470': ['龙王之冕', 5], + 'i_572': ['鸣神御灵的欢喜', 3], + 'i_574': ['鸣神御灵的勇武', 5], + 'i_573': ['鸣神御灵的亲爱', 4], + 'i_480': ['熔毁之刻', 5], + 'i_62': ['黑铜号角', 3], + 'i_101': ['猎兵祭刀', 2], + 'i_551': ['漆黑陨铁的一粒', 2], + 'i_484': ['祸神之禊泪', 5], + 'i_n114041': ['绿洲花园的追忆', 2], + 'i_n112059': ['蕈兽孢子', 1], + 'i_561': ['远海夷地的瑚枝', 2], + 'i_n112073': ['辉光棱晶', 4], + 'i_175': ['隐兽利爪', 3], + 'i_532': ['雾海云间的汞丹', 3], + 'i_93': ['雾虚灯芯', 4], + 'i_131': ['骗骗花蜜', 1], + 'i_571': ['鸣神御灵的明惠', 2], + 'i_n114047': ['烈日威权的梦想', 4], + 'i_63': ['黑晶号角', 4], + 'i_543': ['狮牙斗士的镣铐', 4], + 'i_n112064': ['茁壮菌核', 4], + 'i_161': ['破旧的刀镡', 1], + 'i_472': ['鎏金之鳞', 5], + 'i_113': ['尉官的徽记', 3], + 'i_502': ['高塔孤王的残垣', 3], + 'i_181': ['黯淡棱镜', 2], + 'i_102': ['特工祭刀', 3], + 'i_471': ['血玉之枝', 5], + 'i_533': ['雾海云间的金丹', 4] + } +} + +HONEY_RESERVED_ID_MAP = { + k: {j[0]: [i, j[1]] for i, j in v.items()} for k, v in HONEY_ID_MAP.items() +} +# noinspection SpellCheckingInspection +HONEY_ROLE_NAME_MAP = { + 10000002: ['ayaka_002', '神里绫华', 'ayaka'], + 10000042: ['keqing_042', '刻晴', 'keqing'], + 10000030: ['zhongli_030', '钟离', 'zhongli'], + 10000026: ['xiao_026', '魈', 'xiao'], + 10000020: ['razor_020', '雷泽', 'razor'], + 10000015: ['kaeya_015', '凯亚', 'kaeya'], + 10000037: ['ganyu_037', '甘雨', 'ganyu'], + 10000041: ['mona_041', '莫娜', 'mona'], + 10000038: ['albedo_038', '阿贝多', 'albedo'], + 10000014: ['barbara_014', '芭芭拉', 'barbara'], + 10000027: ['ningguang_027', '凝光', 'ningguang'], + 10000054: ['kokomi_054', '珊瑚宫心海', 'kokomi'], + 10000044: ['xinyan_044', '辛焱', 'xinyan'], + 10000056: ['sara_056', '九条裟罗', 'sara'], + 10000053: ['sayu_053', '早柚', 'sayu'], + 10000043: ['sucrose_043', '砂糖', 'sucrose'], + 10000059: ['heizo_059', '鹿野院平藏', 'heizo'], + 10000060: ['yelan_060', '夜兰', 'yelan'], + 10000064: ['yunjin_064', '云堇', 'yunjin'], + 10000050: ['tohma_050', '托马', 'tohma'], + 10000066: ['ayato_066', '神里绫人', 'ayato'], + 10000067: ['collei_067', '柯莱', 'collei'], + 10000052: ['shougun_052', '雷电将军', 'shougun'], + 10000069: ['tighnari_069', '提纳里', 'tighnari'], + 10000007: ['playergirl_007', '荧', 'playergirl'], + 10000016: ['diluc_016', '迪卢克', 'diluc'], + 10000070: ['nilou_070', '妮露', 'nilou'], + 10000047: ['kazuha_047', '枫原万叶', 'kazuha'], + 10000055: ['gorou_055', '五郎', 'gorou'], + 10000034: ['noel_034', '诺艾尔', 'noel'], + 10000024: ['beidou_024', '北斗', 'beidou'], + 10000032: ['bennett_032', '班尼特', 'bennett'], + 10000062: ['aloy_062', '埃洛伊', 'aloy'], + 10000025: ['xingqiu_025', '行秋', 'xingqiu'], + 10000022: ['venti_022', '温迪', 'venti'], + 10000036: ['chongyun_036', '重云', 'chongyun'], + 10000049: ['yoimiya_049', '宵宫', 'yoimiya'], + 10000029: ['klee_029', '可莉', 'klee'], + 10000006: ['lisa_006', '丽莎', 'lisa'], + 10000033: ['tartaglia_033', '达达利亚', 'tartaglia'], + 10000039: ['diona_039', '迪奥娜', 'diona'], + 10000063: ['shenhe_063', '申鹤', 'shenhe'], + 10000072: ['candace_072', '坎蒂丝', 'candace'], + 10000045: ['rosaria_045', '罗莎莉亚', 'rosaria'], + 10000051: ['eula_051', '优菈', 'eula'], + 10000035: ['qiqi_035', '七七', 'qiqi'], + 10000057: ['itto_057', '荒泷一斗', 'itto'], + 10000005: ['playerboy_005', '空', 'playerboy'], + 10000048: ['feiyan_048', '烟绯', 'feiyan'], + 10000003: ['qin_003', '琴', 'qin'], + 10000023: ['xiangling_023', '香菱', 'xiangling'], + 10000071: ['cyno_071', '赛诺', 'cyno'], + 10000031: ['fischl_031', '菲谢尔', 'fischl'], + 10000046: ['hutao_046', '胡桃', 'hutao'], + 10000021: ['ambor_021', '安柏', 'ambor'], + 10000068: ['dori_068', '多莉', 'dori'], + 10000065: ['shinobu_065', '久岐忍', 'shinobu'], + 10000058: ['yae_058', '八重神子', 'yae'] +} diff --git a/metadata/shortname.py b/metadata/shortname.py index c2d41a9..38d4e27 100644 --- a/metadata/shortname.py +++ b/metadata/shortname.py @@ -1,22 +1,27 @@ from typing import Optional +__all__ = [ + 'roles', 'weapons', + 'roleToId', 'roleToName', 'weaponToName', +] + # noinspection SpellCheckingInspection roles = { 20000000: [ '主角', '旅行者', '卑鄙的外乡人', '荣誉骑士', '爷', '风主', '岩主', '雷主', '草主', '履刑者', '抽卡不歪真君' ], - 10000002: ['神里绫华', 'Kamisato Ayaka', 'Ayaka', 'ayaka', '神里', '绫华', '神里凌华', '凌华', '白鹭公主', + 10000002: ['神里绫华', 'Ayaka', 'ayaka', 'Kamisato Ayaka', '神里', '绫华', '神里凌华', '凌华', '白鹭公主', '神里大小 姐'], 10000003: ['琴', 'Jean', 'jean', '团长', '代理团长', '琴团长', '蒲公英骑士'], - 10000005: ['空', '男主', '男主角', '龙哥', '空哥'], + 10000005: ['空', 'Aether', 'aether', '男主', '男主角', '龙哥', '空哥'], 10000006: ['丽莎', 'Lisa', 'lisa', '图书管理员', '图书馆管理员', '蔷薇魔女'], - 10000007: ['荧', '女主', '女主角', '莹', '萤', '黄毛阿姨', '荧妹'], + 10000007: ['荧', 'Lumine', 'lumine', '女主', '女主角', '莹', '萤', '黄毛阿姨', '荧妹'], 10000014: ['芭芭拉', 'Barbara', 'barbara', '巴巴拉', '拉粑粑', '拉巴巴', '内鬼', '加湿器', '闪耀偶像', '偶像'], 10000015: ['凯亚', 'Kaeya', 'kaeya', '盖亚', '凯子哥', '凯鸭', '矿工', '矿工头子', '骑兵队长', '凯子', '凝冰渡海真君'], - 10000016: ['迪卢克', 'diluc', 'Diluc', '卢姥爷', '姥爷', '卢老爷', '卢锅巴', '正义人', '正e人', '正E人', '卢本伟', + 10000016: ['迪卢克', 'Diluc', 'diluc', '卢姥爷', '姥爷', '卢老爷', '卢锅巴', '正义人', '正e人', '正E人', '卢本伟', '暗夜英雄', '卢卢伯爵', '落魄了', '落魄了家人们'], - 10000020: ['雷泽', 'razor', 'Razor', '狼少年', '狼崽子', '狼崽', '卢皮卡', '小狼', '小狼狗'], + 10000020: ['雷泽', 'Razor', 'razor', '狼少年', '狼崽子', '狼崽', '卢皮卡', '小狼', '小狼狗'], 10000021: ['安柏', 'Amber', 'amber', '安伯', '兔兔伯爵', '飞行冠军', '侦查骑士', '点火姬', '点火机', '打火机', '打火姬'], 10000022: ['温迪', 'Venti', 'venti', '温蒂', '风神', '卖唱的', '巴巴托斯', '巴巴脱丝', '芭芭托斯', '芭芭脱丝', @@ -53,9 +58,9 @@ roles = { 10000045: ['罗莎莉亚', 'Rosaria', 'rosaria', '罗莎莉娅', '白色史莱姆', '白史莱姆', '修女', '罗莎利亚', '罗莎利娅', '罗沙莉亚', '罗沙莉娅', '罗沙利亚', '罗沙利娅', '萝莎莉亚', '萝莎莉娅', '萝莎利亚', '萝莎利娅', '萝沙莉亚', '萝沙莉娅', '萝沙利亚', '萝沙利娅'], - 10000046: ['胡桃', 'Hu Tao', 'hu tao', 'HuTao', 'hutao', 'Hutao', '胡 淘', '往生堂堂主', '火化', '抬棺的', '蝴蝶', + 10000046: ['胡桃', 'HuTao', 'hutao', 'Hu Tao', 'hu tao', 'Hutao', '胡 淘', '往生堂堂主', '火化', '抬棺的', '蝴蝶', '核桃', '堂主', '胡堂主', '雪霁梅香'], - 10000047: ['枫原万叶', 'Kaedehara Kazuha', 'Kazuha', 'kazuha', '万叶', '叶天帝', '天帝', '叶师傅'], + 10000047: ['枫原万叶', 'Kazuha', 'kazuha', 'Kaedehara Kazuha', '万叶', '叶天帝', '天帝', '叶师傅'], 10000048: ['烟绯', 'Yanfei', 'yanfei', '烟老师', '律师', '罗翔'], 10000049: ['宵宫', 'Yoimiya', 'yoimiya', '霄宫', '烟花', '肖宫', '肖工', '绷带女孩'], 10000050: ['托马', 'Thoma', 'thoma', '家政官', '太郎丸', '地头蛇', '男仆', '拖马'], @@ -63,27 +68,30 @@ roles = { 10000052: ['雷电将军', 'Raiden Shogun', 'Raiden', 'raiden', '雷神', '将军', '雷军', '巴尔', '阿影', '影', '巴尔泽布', '煮饭婆', '奶香一刀', '无想一刀', '宅女'], 10000053: ['早柚', 'Sayu', 'sayu', '小狸猫', '狸 猫', '忍者'], - 10000054: ['珊瑚宫心海', 'Sangonomiya Kokomi', 'Kokomi', 'kokomi', '心海', '军师', '珊瑚宫', '书记', '观赏鱼', + 10000054: ['珊瑚宫心海', 'Kokomi', 'kokomi', 'Sangonomiya Kokomi', '心海', '军师', '珊瑚宫', '书记', '观赏鱼', '水母', '鱼', '美人鱼'], 10000055: ['五郎', 'Gorou', 'gorou', '柴犬', '土狗', '希娜', '希娜小姐'], - 10000056: ['九条裟罗', 'Kujou Sara', 'Sara', 'sara', '九条', '九条沙罗', '裟罗', '沙罗', '天狗'], - 10000057: ['荒泷一斗', 'Arataki Itto', 'Itto', 'itto', '荒龙一斗', '荒泷天下第一斗', '一斗', '一抖', '荒泷', '1斗', + 10000056: ['九条裟罗', 'Sara', 'sara', 'Kujou Sara', '九条', '九条沙罗', '裟罗', '沙罗', '天狗'], + 10000057: ['荒泷一斗', 'Itto', 'itto', 'Arataki Itto', '荒龙一斗', '荒泷天下第一斗', '一斗', '一抖', '荒泷', '1斗', '牛牛', '斗子哥', '牛子哥', '牛子', '孩子 王', '斗虫', '巧乐兹', '放牛的'], - 10000058: ['八重神子', 'Yae Miko', 'Miko', 'miko', '八重', '神子', '狐狸', '想得美哦', '巫女', '屑狐狸', '骚狐狸', + 10000058: ['八重神子', 'Miko', 'miko', 'Yae Miko', '八重', '神子', '狐狸', '想得美哦', '巫女', '屑狐狸', '骚狐狸', '八重宫司', '婶子', '小八'], - 10000059: ['鹿野院平藏', 'shikanoin heizou', 'Heizou', 'heizou', 'heizo', '鹿野苑', '鹿野院', '平藏', '鹿野苑平藏', + 10000059: ['鹿野院平藏', 'Heizou', 'heizou', 'shikanoin heizou', 'heizo', '鹿野苑', '鹿野院', '平藏', '鹿野苑平藏', '鹿野', '小鹿'], 10000060: ['夜兰', 'Yelan', 'yelan', '夜阑', '叶 澜', '腋兰', '夜天后'], 10000062: ['埃洛伊', 'Aloy', 'aloy'], 10000063: ['申鹤', 'Shenhe', 'shenhe', '神鹤', '小姨', '小姨子', '审鹤'], - 10000064: ['云堇', 'Yun Jin', 'yunjin', 'yun jin', '云瑾', '云先生', '云锦', '神女劈观'], - 10000065: ['久岐忍', 'Kuki Shinobu', 'Kuki', 'kuki', 'Shinobu', 'shinobu', '97忍', '小忍', '久歧忍', '97', '茄忍', + 10000064: ['云堇', 'YunJin', 'yunjin', 'Yun Jin', 'yun jin', '云瑾', '云先生', '云锦', '神女劈观'], + 10000065: ['久岐忍', 'Kuki', 'kuki', 'Kuki Shinobu', 'Shinobu', 'shinobu', '97忍', '小忍', '久歧忍', '97', '茄忍', '阿忍', '忍姐'], - 10000066: ['神里绫人', 'Kamisato Ayato', 'Ayato', 'ayato', '绫人', '神里凌人', '凌人', '0人', '神人', '零人', + 10000066: ['神里绫人', 'Ayato', 'ayato', 'Kamisato Ayato', '绫人', '神里凌人', '凌人', '0人', '神人', '零人', '大舅哥'], 10000067: ['柯莱', 'Collei', 'collei', '克莱', '科莱', '须弥飞行冠军', '草安伯'], 10000068: ['多莉', 'Dori', 'dori', '多利', '多丽'], - 10000069: ['提纳里', 'Tighnari', 'tighnari', '巡林官', '小提', '缇娜里', '提哪里', '提那里'] + 10000069: ['提纳里', 'Tighnari', 'tighnari', '巡林官', '小提', '缇娜里', '提哪里', '提那里'], + 10000070: ['妮露', 'Nilou', 'nilou'], + 10000071: ['赛诺', 'Cyno', 'cyno'], + 10000072: ['坎蒂丝', 'Candace', 'candace'], } weapons = { "磐岩结绿": ["绿箭", "绿剑"], @@ -166,16 +174,19 @@ weapons = { } +# noinspection PyPep8Naming def roleToName(shortname: str) -> str: """讲角色昵称转为正式名""" return next((value[0] for value in roles.values() for name in value if name == shortname), shortname) +# noinspection PyPep8Naming def roleToId(name: str) -> Optional[int]: """获取角色ID""" return next((key for key, value in roles.items() for n in value if n == name), None) +# noinspection PyPep8Naming def weaponToName(shortname: str) -> str: """讲武器昵称转为正式名""" return next((key for key, value in weapons.items() if shortname == key or shortname in value), shortname) diff --git a/modules/wiki/material.py b/modules/wiki/material.py index ef71f4a..c0c215b 100644 --- a/modules/wiki/material.py +++ b/modules/wiki/material.py @@ -1,56 +1,79 @@ import re -from typing import List +from typing import List, Optional, Tuple, Union from bs4 import BeautifulSoup from httpx import URL -from typing_extensions import Self from modules.wiki.base import SCRAPE_HOST, WikiModel __all__ = ['Material'] +WEEKDAYS = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'] + class Material(WikiModel): - """材料 - - Attributes: - type: 类型 - source: 获取方式 - description: 描述 - serise: 材料系列 - """ + # noinspection PyUnresolvedReferences + """武器、角色培养素材 + Attributes: + type: 类型 + weekdays: 每周开放的时间 + source: 获取方式 + description: 描述 + """ type: str - source: List[str] + source: Optional[List[str]] = None + weekdays: Optional[List[int]] = None description: str @staticmethod def scrape_urls() -> List[URL]: - return [SCRAPE_HOST.join(f'fam_wep_{i}/?lang=CHS') for i in ['primary', 'secondary', 'common']] + weapon = [SCRAPE_HOST.join(f'fam_wep_{i}/?lang=CHS') for i in ['primary', 'secondary', 'common']] + talent = [SCRAPE_HOST.join(f'fam_talent_{i}/?lang=CHS') for i in ['book', 'boss', 'common', 'reward']] + return weapon + talent @classmethod - async def _parse_soup(cls, soup: BeautifulSoup) -> Self: - """解析材料页""" + async def get_name_list(cls, *, with_url: bool = False) -> List[Union[str, Tuple[str, URL]]]: + return list(sorted(set(await super(Material, cls).get_name_list(with_url=with_url)), key=lambda x: x[0])) + + @classmethod + async def _parse_soup(cls, soup: BeautifulSoup) -> "Material": + """解析突破素材页""" soup = soup.select('.wp-block-post-content')[0] tables = soup.find_all('table') table_rows = tables[0].find_all('tr') + def get_table_row(target: str): + """一个便捷函数,用于返回对应表格头的对应行的最后一个单元格中的文本""" + for row in table_rows: + if target in row.find('td').text: + return row.find_all('td')[-1] + def get_table_text(row_num: int) -> str: - """一个快捷函数,用于返回表格对应行的最后一个单元格中的文本""" + """一个便捷函数,用于返回表格对应行的最后一个单元格中的文本""" return table_rows[row_num].find_all('td')[-1].text.replace('\xa0', '') id_ = re.findall(r'/img/(.*?)\.webp', str(table_rows[0]))[0] name = get_table_text(0) rarity = len(table_rows[3].find_all('img')) type_ = get_table_text(1) - source = list( - filter( - lambda x: x, # filter 在这里的作用是过滤掉为空的数据 - table_rows[-2].find_all('td')[-1].encode_contents().decode().split('
') + if (item_source := get_table_row('Item Source')) is not None: + item_source = list( + # filter 在这里的作用是过滤掉为空的数据 + filter(lambda x: x, item_source.encode_contents().decode().split('
')) ) - ) + if (alter_source := get_table_row('Alternative Item')) is not None: + alter_source = list( + # filter 在这里的作用是过滤掉为空的数据 + filter(lambda x: x, alter_source.encode_contents().decode().split('
')) + ) + source = list(sorted(set((item_source or []) + (alter_source or [])))) + if (weekdays := get_table_row('Weekday')) is not None: + weekdays = [*(WEEKDAYS.index(weekdays.text.replace('\xa0', '').split(',')[0]) + 3 * i for i in range(2)), 6] description = get_table_text(-1) - return Material(id=id_, name=name, rarity=rarity, type=type_, source=source, description=description) + return Material( + id=id_, name=name, rarity=rarity, type=type_, description=description, source=source, weekdays=weekdays + ) @property def icon(self) -> str: diff --git a/plugins/genshin/daily/__init__.py b/plugins/genshin/daily/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugins/genshin/daily/material.py b/plugins/genshin/daily/material.py new file mode 100644 index 0000000..fbcbd47 --- /dev/null +++ b/plugins/genshin/daily/material.py @@ -0,0 +1,367 @@ +import asyncio +import itertools +import os +import re +from asyncio import Lock +from ctypes import c_double +from datetime import datetime +from multiprocessing import Value +from pathlib import Path +from typing import Any, Dict, Iterable, List, Literal, Optional, Tuple, Union + +import ujson as json +from aiofiles import open as async_open +from bs4 import BeautifulSoup +from genshin import Client +from httpx import AsyncClient, HTTPError +from pydantic import BaseModel +from telegram import InputMediaDocument, InputMediaPhoto, Message, Update, User +from telegram.constants import ChatAction, ParseMode +from telegram.error import RetryAfter, TimedOut +from telegram.ext import CallbackContext + +from core.assets import AssetsService +from core.baseplugin import BasePlugin +from core.cookies.error import CookiesNotFoundError +from core.plugin import Plugin, handler +from core.template import TemplateService +from core.user.error import UserNotFoundError +from metadata.honey import HONEY_ID_MAP, HONEY_ROLE_NAME_MAP +from utils.bot import get_all_args +from utils.const import RESOURCE_DIR +from utils.decorators.admins import bot_admins_rights_check +from utils.decorators.error import error_callable +from utils.decorators.restricts import restricts +from utils.helpers import get_genshin_client +from utils.log import logger + +DATA_TYPE = Dict[str, List[List[str]]] +DATA_FILE_PATH = Path(__file__).joinpath('../daily.json').resolve() +AREA = ['蒙德', '璃月', '稻妻', '须弥'] +DOMAINS = ['忘却之峡', '太山府', '菫色之庭', '昏识塔', '塞西莉亚苗圃', '震雷连山密宫', '砂流之庭', '有顶塔'] +DOMAIN_AREA_MAP = dict(zip(DOMAINS, AREA * 2)) + +WEEK_MAP = ['一', '二', '三', '四', '五', '六', '日'] + + +def convert_path(path: Union[str, Path]) -> str: + return f"..{os.sep}..{os.sep}" + str(path.relative_to(RESOURCE_DIR)) + + +def sort_item(items: List['ItemData']) -> Iterable['ItemData']: + result_a = [] + for _, group_a in itertools.groupby(sorted(items, key=lambda x: x.rarity, reverse=True), lambda x: x.rarity): + result_b = [] + for _, group_b in itertools.groupby( + sorted(group_a, key=lambda x: x.level or -1, reverse=True), lambda x: x.level or -1 + ): + result_b.append(sorted(group_b, key=lambda x: x.constellation or x.refinement or -1, reverse=True)) + result_a.append(itertools.chain(*result_b)) + return itertools.chain(*result_a) + + +class DailyMaterial(Plugin, BasePlugin): + """每日素材表""" + data: DATA_TYPE + locks: Tuple[Lock] = (Lock(), Lock()) + + def __init__(self, assets: AssetsService, template: TemplateService): + self.assets_service = assets + self.template_service = template + self.client = AsyncClient() + + async def __async_init__(self): + data = None + if not DATA_FILE_PATH.exists(): + async def task_daily(): + async with self.locks[0]: + logger.info("正在开始获取每日素材缓存") + self.data = await self._refresh_data() + + self.refresh_task = asyncio.create_task(task_daily()) + if not data and DATA_FILE_PATH.exists(): + async with async_open(DATA_FILE_PATH) as file: + data = json.loads(await file.read()) + self.data = data + + async def _get_data_from_user(self, user: User) -> Tuple[Optional[Client], Dict[str, List[Any]]]: + client = None + user_data = {'character': [], 'weapon': []} + try: + logger.debug("尝试获取已绑定的原神账号") + client = await get_genshin_client(user.id) + logger.debug(f"获取成功, UID: {client.uid}") + characters = await client.get_genshin_characters(client.uid) + for character in characters: + cid = HONEY_ROLE_NAME_MAP[character.id][0] + weapon = character.weapon + user_data['character'].append( + ItemData( + id=cid, name=character.name, rarity=character.rarity, level=character.level, + constellation=character.constellation, + icon=convert_path(await self.assets_service.character(cid).icon()) + ) + ) + user_data['weapon'].append( + ItemData( + id=(wid := f"i_n{weapon.id}"), name=weapon.name, level=weapon.level, rarity=weapon.rarity, + refinement=weapon.refinement, + icon=convert_path( + await getattr( + self.assets_service.weapon(wid), 'icon' if weapon.ascension < 2 else 'awakened' + )() + ), + c_path=convert_path(await self.assets_service.character(cid).side()) + ) + ) + except (UserNotFoundError, CookiesNotFoundError): + logger.info(f"未查询到用户({user.full_name} {user.id}) 所绑定的账号信息") + return client, user_data + + @handler.command('daily_material', block=False) + @restricts(restricts_time_of_groups=20, without_overlapping=True) + @error_callable + async def daily_material(self, update: Update, context: CallbackContext): + user = update.effective_user + args = get_all_args(context) + now = datetime.now() + + if args and str(args[0]).isdigit(): + weekday = int(args[0]) - 1 + if weekday < 0: + weekday = 0 + elif weekday > 6: + weekday = 6 + time = title = f"星期{WEEK_MAP[weekday]}" + else: # 获取今日是星期几,判定了是否过了凌晨4点 + title = "今日" + weekday = now.weekday() - (1 if now.hour < 4 else 0) + weekday = 6 if weekday < 0 else weekday + time = now.strftime("%m-%d %H:%M") + " 星期" + WEEK_MAP[weekday] + full = args and args[-1] == 'full' + + if weekday == 6: + notice = await update.message.reply_text( + ("今天" if title == '今日' else '这天') + "是星期天, 全部素材都可以刷哦~", + parse_mode=ParseMode.HTML + ) + self._add_delete_message_job(context, notice.chat_id, notice.message_id, 5) + return + + if self.locks[0].locked(): + notice = await update.message.reply_text("派蒙正在摘抄每日素材表,以后再来探索吧~") + self._add_delete_message_job(context, notice.chat_id, notice.message_id, 5) + return + + if self.locks[1].locked(): + notice = await update.message.reply_text("派蒙正在搬运每日素材的图标,以后再来探索吧~") + self._add_delete_message_job(context, notice.chat_id, notice.message_id, 5) + return + + notice = await update.message.reply_text("派蒙可能需要找找图标素材,还请耐心等待哦~") + await update.message.reply_chat_action(ChatAction.TYPING) + + # 获取已经缓存至本地的秘境素材信息 + local_data = {'character': [], 'weapon': []} + if not self.data: + logger.info("正在获取每日素材缓存") + await self._refresh_data() + for domain, sche in self.data.items(): + area = DOMAIN_AREA_MAP[domain] + type_ = 'character' if DOMAINS.index(domain) < 4 else 'weapon' + local_data[type_].append({'name': area, 'materials': sche[weekday][0], 'items': sche[weekday][1]}) + + # 尝试获取用户已绑定的原神账号信息 + client, user_data = self._get_data_from_user(user) + + await update.message.reply_chat_action(ChatAction.TYPING) + render_data = RenderData(title=title, time=time, uid=client.uid if client else client) + for type_ in ['character', 'weapon']: + areas = [] + for area_data in local_data[type_]: + items = [] + for id_ in area_data['items']: + added = False + for i in user_data[type_]: + if id_ == i.id: + if i.rarity > 3: # 跳过 3 星及以下的武器 + items.append(i) + added = True + break + if added: + continue + item = HONEY_ID_MAP[type_][id_] + if item[1] < 4: # 跳过 3 星及以下的武器 + continue + items.append(ItemData( + id=id_, name=item[0], rarity=item[1], + icon=convert_path(await getattr(self.assets_service, f'{type_}')(id_).icon()) + )) + materials = [] + for mid in area_data['materials']: + path = convert_path(await self.assets_service.material(mid).icon()) + material = HONEY_ID_MAP['material'][mid] + materials.append(ItemData(id=mid, icon=path, name=material[0], rarity=material[1])) + areas.append(AreaData(name=area_data['name'], materials=materials, items=sort_item(items))) + setattr(render_data, type_, areas) + await update.message.reply_chat_action(ChatAction.TYPING) + character_img_data = await self.template_service.render( + 'genshin/daily_material', 'character.html', {'data': render_data}, {'width': 1164, 'height': 500} + ) + weapon_img_data = await self.template_service.render( + 'genshin/daily_material', 'weapon.html', {'data': render_data}, {'width': 1164, 'height': 500} + ) + await update.message.reply_chat_action(ChatAction.UPLOAD_PHOTO) + if full: + await update.message.reply_media_group([ + InputMediaDocument(character_img_data, filename="可培养角色.png"), + InputMediaDocument(weapon_img_data, filename="可培养武器.png") + ]) + else: + await update.message.reply_media_group( + [InputMediaPhoto(character_img_data), InputMediaPhoto(weapon_img_data)] + ) + await notice.delete() + + @handler.command('refresh_daily_material', block=False) + @bot_admins_rights_check + async def refresh(self, update: Update, context: CallbackContext): + message = update.effective_message + if self.locks[0].locked(): + notice = await message.reply_text("派蒙还在抄每日素材表呢,我有在好好工作哦~") + self._add_delete_message_job(context, notice.chat_id, notice.message_id, 10) + return + if self.locks[1].locked(): + notice = await message.reply_text("派蒙正在搬运每日素材图标,在努力工作呢!") + self._add_delete_message_job(context, notice.chat_id, notice.message_id, 10) + return + async with self.locks[1]: + notice = await message.reply_text("派蒙正在重新摘抄每日素材表,请稍等~", parse_mode=ParseMode.HTML) + async with self.locks[0]: + data = await self._refresh_data() + notice = await notice.edit_text( + "每日素材表" + + ("摘抄完成!" if data else "坏掉了!等会它再长好了之后我再抄。。。") + + '\n正搬运每日素材的图标中。。。', + parse_mode=ParseMode.HTML + ) + self.data = data or self.data + await self._download_icon(notice) + notice = await notice.edit_text( + notice.text_html.split('\n')[0] + "\n每日素材图标搬运完成!", + parse_mode=ParseMode.HTML + ) + self._add_delete_message_job(context, notice.chat_id, notice.message_id, 10) + + async def _refresh_data(self, retry: int = 5) -> DATA_TYPE: + from bs4 import Tag + from asyncio import sleep + result = {} + for i in range(retry): + try: + response = await self.client.get("https://genshin.honeyhunterworld.com/?lang=CHS") + soup = BeautifulSoup(response.text, 'lxml') + calendar = soup.select(".calendar_day_wrap")[0] + key: str = '' + for tag in calendar: + tag: Tag + if tag.name == 'span': + key = tag.find('a').text + result[key] = [[[], []] for _ in range(7)] + for day, div in enumerate(tag.find_all('div')): + result[key][day][0] = [re.findall(r"/(.*)?/", a['href'])[0] for a in div.find_all('a')] + else: + id_ = re.findall(r"/(.*)?/", tag['href'])[0] + if tag.text.strip() == '旅行者': + continue + for day in map(int, tag.find('div')['data-days']): + result[key][day][1].append(id_) + for stage, schedules in result.items(): + for day, _ in enumerate(schedules): + result[stage][day][1] = list(set(result[stage][day][1])) + async with async_open(DATA_FILE_PATH, 'w', encoding='utf-8') as file: + await file.write(json.dumps(result)) # pylint: disable=PY-W0079 + logger.info("每日素材刷新成功") + break + except HTTPError: + await sleep(1) + if i <= retry - 1: + logger.warning("每日素材刷新失败, 正在重试") + else: + logger.error("每日素材刷新失败, 请稍后重试") + continue + # noinspection PyTypeChecker + return result + + async def _download_icon(self, message: Optional[Message] = None): + from time import time as time_ + lock = asyncio.Lock() + the_time = Value(c_double, time_() - 1) + interval = 0.2 + + async def task(_id, _item, _type): + logger.debug(f"正在开始下载 \"{_item[0]}\" 的图标素材") + async with lock: + if message is not None and time_() >= the_time.value + interval: + text = '\n'.join(message.text_html.split('\n')[:2]) + f"\n正在搬运 {_item[0]} 的图标素材。。。" + try: + await message.edit_text(text, parse_mode=ParseMode.HTML) + except (TimedOut, RetryAfter): + pass + the_time.value = time_() + asset = getattr(self.assets_service, _type)(_id) + icon_types = list(filter( + lambda x: not x.startswith('_') and x not in ['path'] and callable(getattr(asset, x)), + dir(asset) + )) + icon_coroutines = map(lambda x: getattr(asset, x), icon_types) + for coroutine in icon_coroutines: + await coroutine() + logger.debug(f"\"{_item[0]}\" 的图标素材下载成功") + async with lock: + if message is not None and time_() >= the_time.value + interval: + text = ( + '\n'.join(message.text_html.split('\n')[:2]) + + f"\n正在搬运 {_item[0]} 的图标素材。。。成功!" + ) + try: + await message.edit_text(text, parse_mode=ParseMode.HTML) + except (TimedOut, RetryAfter): + pass + the_time.value = time_() + + for type_, items in HONEY_ID_MAP.items(): + task_list = [] + for id_, item in items.items(): + task_list.append(asyncio.create_task(task(id_, item, type_))) + await asyncio.gather(*task_list) + + logger.info("图标素材下载完成") + + +class ItemData(BaseModel): + id: str + name: str + rarity: int + icon: str + level: Optional[int] = None + constellation: Optional[int] = None + refinement: Optional[int] = None + c_path: Optional[str] = None + + +class AreaData(BaseModel): + name: Literal['蒙德', '璃月', '稻妻', '须弥'] + materials: List[ItemData] = [] + items: Iterable[ItemData] = [] + + +class RenderData(BaseModel): + title: str + time: str + uid: Optional[int] = None + character: List[AreaData] = [] + weapon: List[AreaData] = [] + + def __getitem__(self, item): + return self.__getattribute__(item) diff --git a/resources/genshin/daily_material/bg/area/0.png b/resources/genshin/daily_material/bg/area/0.png new file mode 100644 index 0000000..23162f7 Binary files /dev/null and b/resources/genshin/daily_material/bg/area/0.png differ diff --git a/resources/genshin/daily_material/bg/area/1.png b/resources/genshin/daily_material/bg/area/1.png new file mode 100644 index 0000000..7a40aa3 Binary files /dev/null and b/resources/genshin/daily_material/bg/area/1.png differ diff --git a/resources/genshin/daily_material/bg/area/2.png b/resources/genshin/daily_material/bg/area/2.png new file mode 100644 index 0000000..979a9f5 Binary files /dev/null and b/resources/genshin/daily_material/bg/area/2.png differ diff --git a/resources/genshin/daily_material/bg/area/3.png b/resources/genshin/daily_material/bg/area/3.png new file mode 100644 index 0000000..18e5bcc Binary files /dev/null and b/resources/genshin/daily_material/bg/area/3.png differ diff --git a/resources/genshin/daily_material/bg/rarity/full/1.png b/resources/genshin/daily_material/bg/rarity/full/1.png new file mode 100644 index 0000000..5da532e Binary files /dev/null and b/resources/genshin/daily_material/bg/rarity/full/1.png differ diff --git a/resources/genshin/daily_material/bg/rarity/full/2.png b/resources/genshin/daily_material/bg/rarity/full/2.png new file mode 100644 index 0000000..82cb30c Binary files /dev/null and b/resources/genshin/daily_material/bg/rarity/full/2.png differ diff --git a/resources/genshin/daily_material/bg/rarity/full/3.png b/resources/genshin/daily_material/bg/rarity/full/3.png new file mode 100644 index 0000000..5753e70 Binary files /dev/null and b/resources/genshin/daily_material/bg/rarity/full/3.png differ diff --git a/resources/genshin/daily_material/bg/rarity/full/4.png b/resources/genshin/daily_material/bg/rarity/full/4.png new file mode 100644 index 0000000..36cb6cd Binary files /dev/null and b/resources/genshin/daily_material/bg/rarity/full/4.png differ diff --git a/resources/genshin/daily_material/bg/rarity/full/5.png b/resources/genshin/daily_material/bg/rarity/full/5.png new file mode 100644 index 0000000..efb48ab Binary files /dev/null and b/resources/genshin/daily_material/bg/rarity/full/5.png differ diff --git a/resources/genshin/daily_material/bg/rarity/half/1.png b/resources/genshin/daily_material/bg/rarity/half/1.png new file mode 100644 index 0000000..5811598 Binary files /dev/null and b/resources/genshin/daily_material/bg/rarity/half/1.png differ diff --git a/resources/genshin/daily_material/bg/rarity/half/2.png b/resources/genshin/daily_material/bg/rarity/half/2.png new file mode 100644 index 0000000..c1d5363 Binary files /dev/null and b/resources/genshin/daily_material/bg/rarity/half/2.png differ diff --git a/resources/genshin/daily_material/bg/rarity/half/3.png b/resources/genshin/daily_material/bg/rarity/half/3.png new file mode 100644 index 0000000..44aa8c3 Binary files /dev/null and b/resources/genshin/daily_material/bg/rarity/half/3.png differ diff --git a/resources/genshin/daily_material/bg/rarity/half/4.png b/resources/genshin/daily_material/bg/rarity/half/4.png new file mode 100644 index 0000000..8079547 Binary files /dev/null and b/resources/genshin/daily_material/bg/rarity/half/4.png differ diff --git a/resources/genshin/daily_material/bg/rarity/half/5.png b/resources/genshin/daily_material/bg/rarity/half/5.png new file mode 100644 index 0000000..2630db5 Binary files /dev/null and b/resources/genshin/daily_material/bg/rarity/half/5.png differ diff --git a/resources/genshin/daily_material/bg/rarity/star.webp b/resources/genshin/daily_material/bg/rarity/star.webp new file mode 100644 index 0000000..c58ea8e Binary files /dev/null and b/resources/genshin/daily_material/bg/rarity/star.webp differ diff --git a/resources/genshin/daily_material/bg/title/01.png b/resources/genshin/daily_material/bg/title/01.png new file mode 100644 index 0000000..af014b2 Binary files /dev/null and b/resources/genshin/daily_material/bg/title/01.png differ diff --git a/resources/genshin/daily_material/bg/title/02.png b/resources/genshin/daily_material/bg/title/02.png new file mode 100644 index 0000000..55e797e Binary files /dev/null and b/resources/genshin/daily_material/bg/title/02.png differ diff --git a/resources/genshin/daily_material/character.html b/resources/genshin/daily_material/character.html new file mode 100644 index 0000000..2242f0d --- /dev/null +++ b/resources/genshin/daily_material/character.html @@ -0,0 +1,74 @@ + + + + + + 每日素材 + + + +
+
{{ data.title }}可培养角色
+
+ {% if data.uid != none %} + UID: {{ data.uid }} + {% else %} + 暂未绑定UID + {% endif %} + {{ data.time }} +
+
+
+ {% for area in data.character %} +
+
+ {{ area.name }} + {{ area.name }} + + {% for material in area.materials %} +
+
+ {{ material.name }} +
+
+ {% for _ in range(material.rarity) %} + star + {% endfor %} +
+
+ {% endfor %} +
+
+
+ {% for item in area.items %} +
+
+ {% if item.level != none %} +
Lv.{{ item.level }}
+ {% endif %} + {% if item.constellation != none %} + {% if item.constellation == 6 %} +
+ 6命 +
+ {% elif item.constellation != 0 %} +
{{ item.constellation }}命
+ {% endif %} + {% endif %} + {{ item.name }} +
+
+
{{ item.name }}
+
+
+ {% endfor %} +
+
+ {% endfor %} +
+ +
+ + \ No newline at end of file diff --git a/resources/genshin/daily_material/example.html b/resources/genshin/daily_material/example.html new file mode 100644 index 0000000..3079359 --- /dev/null +++ b/resources/genshin/daily_material/example.html @@ -0,0 +1,316 @@ + + + + + + 每日素材 + + + +
+
今日素材表
+
+ UID: 100206192 + 09-15 01:05 星期四 +
+
+
+
角色培养素材
+
+
+ 蒙德 + 蒙德 + +
+
+ +
+
+ star + star +
+
+
+
+ +
+
+ star + star + star +
+
+
+
+ +
+
+ star + star + star + star +
+
+
+
+ +
+
+ star + star + star + star + star +
+
+
+
+
+
+
+
Lv.90
+
6命
+ 神里绫华 +
+
+
神里绫华
+
+
+
+
+
Lv.90
+
6命
+ 神里绫华 +
+
+
神里绫华
+
+
+
+
+
Lv.90
+
6命
+ 神里绫华 +
+
+
神里绫华
+
+
+
+
+
Lv.90
+ 神里绫华 +
+
+
神里绫华
+
+
+
+
+ 神里绫华 +
+
+
神里绫华
+
+
+
+
+ 神里绫华 +
+
+
神里绫华
+
+
+
+
+
+
+ 蒙德 + 蒙德 + +
+
+ +
+
+ star + star + star +
+
+
+
+ +
+
+ star + star + star + star +
+
+
+
+ +
+
+ star + star + star + star + star +
+
+
+
+
+
+
+
Lv.90
+
6命
+ 神里绫华 +
+
+
神里绫华
+
+
+
+
+
Lv.90
+
6命
+ 神里绫华 +
+
+
神里绫华
+
+
+
+
+
+
+
+
武器培养素材
+
+
+ 蒙德 + 蒙德 + +
+
+ +
+
+ star + star +
+
+
+
+ +
+
+ star + star + star +
+
+
+
+ +
+
+ star + star + star + star +
+
+
+
+ +
+
+ star + star + star + star + star +
+
+
+
+
+
+
+ 神里绫华 +
+
+
Lv.90
+
精炼5
+ 天空之刃 +
+
+
天空之刃
+
+
+
+
+ 神里绫华 +
+
+
Lv.90
+
精炼5
+ 天空之刃 +
+
+
天空之刃
+
+
+
+
+ 神里绫华 +
+
+
Lv.90
+
精炼5
+ 天空之刃 +
+
+
天空之刃
+
+
+
+
+ 神里绫华 +
+
+
Lv.90
+
精炼5
+ 天空之刃 +
+
+
天空之刃
+
+
+
+
+ 天空之刃 +
+
+
天空之刃
+
+
+
+
+ 天空之刃 +
+
+
天空之刃
+
+
+
+
+
+
+ + \ No newline at end of file diff --git a/resources/genshin/daily_material/style.css b/resources/genshin/daily_material/style.css new file mode 100644 index 0000000..1accaed --- /dev/null +++ b/resources/genshin/daily_material/style.css @@ -0,0 +1,258 @@ +@font-face { + font-family: 'HYWH'; + src: url('../../fonts/汉仪文黑-85W.ttf') format('truetype'); +} + +:root { + --bg-color: #ebe5d9; + --font-color: #514e49; + --color-1: #d6ba92; + --shadow: #726c65; + +} + +* { + font-family: 'HYWH', serif; + color: var(--font-color); + background-repeat: no-repeat; + +} + +body { + margin: 0; + padding: 0; + +} + +.container { + width: 1104px; + background-color: var(--bg-color); + position: absolute; + left: 50%; + transform: translateX(-50%); + padding: 15px 30px; +} + +.container > div.title { + width: 100%; + font-size: 40px; + text-align: center; +} + +.user-info { + margin-top: 20px; + font-size: 25px; + border-left-color: var(--color-1); + border-left-style: solid; + border-left-width: 10px; + padding: 0; + font-style: italic; +} + +.user-info > span:last-child { + float: right; +} + +.box { + width: 100%; + margin: 10px 0; +} + +.title { + font-size: 30px; + width: 100%; + text-align: center; + position: relative; + margin: 20px 0; +} + +.area { + width: calc(100% - 20px); + padding: 0 10px; +} + +.area-head { + background-image: url("bg/title/01.png"); + background-size: contain; + width: calc(100% - 20px); + height: 80px; + filter: drop-shadow(5px 5px 5px var(--shadow)); + position: relative; + padding-left: 15px; +} + +.area-head > img { + height: inherit; + filter: drop-shadow(3px 3px 5px black); +} + +.area-head > span { + font-size: 23px; + position: absolute; + top: 50%; + transform: translateY(-50%); +} + +.materials { + width: 335px; + height: inherit; + right: 0; + display: flex; + align-items: center; + justify-content: center; + background-image: url("bg/title/02.png"); + background-size: 100% 100%; + position: relative; + top: 50%; + transform: translateY(-50%); +} + +.material { + position: relative; + top: 5px; + width: 60px; + margin: 0 5px; + filter: drop-shadow(1px 1px 5px #838383); +} + +.material-icon { + width: 60px; + height: 60px; + background-size: 100% auto; + display: flex; + justify-content: center; + align-items: center; + overflow: hidden; + border-radius: 5px; +} + +.material-icon > img { + height: calc(100% - 10px); +} + +.material-star { + position: relative; + top: -5px; + width: 100%; + display: flex; + justify-content: center; + align-items: center; + filter: drop-shadow(1px 1px 2px #6c6c6c); +} + +.material-star > img { + width: 10px; +} + +.area-content { + margin: 20px 0; + padding: 0 20px; + display: flex; + flex-flow: wrap; +} + +.item { + width: 150px; + background-color: var(--bg-color); + border-radius: 10px; + box-shadow: 3px 3px 10px var(--shadow); + margin: 10px 12px; + justify-content: center; +} + +.item-icon { + width: inherit; + height: 152px; + background-size: 104% auto; + background-position: center; + overflow: hidden; + border-radius: 10px 10px 35px 0; + position: relative; +} + +.item-icon > img { + max-height: 100%; + max-width: 150px; + position: absolute; + top: 50%; + left: 50%; + transform: translateX(-50%) translateY(-50%); +} + +.item-name { + text-align: center; + display: flex; + align-items: center; + height: 35px; +} + +.item-name > div { + width: 100%; + font-size: 22px; + text-align: center; +} + +.item > .item-icon > div { + position: absolute; + z-index: 1; + color: white; +} + +.item > .item-icon > div:first-child { + background-color: rgba(100, 149, 237, 0.7); + backdrop-filter: blur(3px); + border-radius: 0 0 5px 0; + box-shadow: 1px 1px 5px var(--shadow); + left: 0; + font-size: 18px; + padding: 4px 5px; +} + +.character { + overflow: hidden; +} + +.character > .item-icon > div:nth-child(2) { + border-radius: 0 0 0 5px; + box-shadow: -1px 1px 5px var(--shadow); + right: 0; + font-size: 20px; + padding: 3px; +} + +.weapon > .item-icon > div:nth-child(2) { + border-radius: 0 5px 0 0; + box-shadow: -1px -1px 5px var(--shadow); + bottom: 0; + font-size: 18px; + padding: 3px; +} + +.weapon { + overflow: unset; + position: relative; +} + +.weapon > .role { + background-color: rgb(76 82 107 / 80%); + backdrop-filter: blur(3px); + position: absolute; + right: -10px; + top: -10px; + z-index: 2; + width: 30px; + height: 30px; + padding: 5px; + border-radius: 100%; + border-style: solid; + border-width: 3px; + border-color: white; +} + +.weapon > .role > img { + height: 50px; + position: absolute; + left: 58%; + transform: translateX(-50%); + bottom: 0; +} \ No newline at end of file diff --git a/resources/genshin/daily_material/weapon.html b/resources/genshin/daily_material/weapon.html new file mode 100644 index 0000000..4d64e9b --- /dev/null +++ b/resources/genshin/daily_material/weapon.html @@ -0,0 +1,80 @@ + + + + + + 每日素材 + + + +
+
{{ data.title }}可培养武器
+
+ {% if data.uid != none %} + UID: {{ data.uid }} + {% else %} + 暂未绑定UID + {% endif %} + {{ data.time }} +
+
+
+ {% for area in data.weapon %} +
+
+ {{ area.name }} + {{ area.name }} + + {% for material in area.materials %} +
+
+ {{ material.name }} +
+
+ {% for _ in range(material.rarity) %} + star + {% endfor %} +
+
+ {% endfor %} +
+
+
+ {% for item in area.items %} +
+ {% if item.c_path != none %} +
+ +
+ {% endif %} +
+ {% if item.level != none %} +
Lv.{{ item.level }}
+ {% endif %} + {% if item.refinement != none %} + {% if item.refinement == 5 %} +
+ 精炼5 +
+ {% else %} +
+ 精炼{{ item.refinement }}
+ {% endif %} + {% endif %} + {{ item.name }} +
+
+
{{ item.name }}
+
+
+ {% endfor %} +
+
+ {% endfor %} +
+ +
+ + \ No newline at end of file diff --git a/tests/test_wiki.py b/tests/test_wiki.py index 21f08ad..834c5dd 100644 --- a/tests/test_wiki.py +++ b/tests/test_wiki.py @@ -102,6 +102,12 @@ class TestMaterial: assert '合成获得' in material.source assert '巴巴托斯' in material.description + material = await Material.get_by_id('i_483') + assert material.name == '凶将之手眼' + assert material.type == '角色培养素材' + assert '70级以上永恒的守护者挑战奖励' in material.source + assert '所见即所为' in material.description + @staticmethod @flaky(3, 1) async def test_get_by_name(): @@ -111,6 +117,12 @@ class TestMaterial: assert '60级以上深渊法师掉落' in material.source assert '勃发' in material.description + material = await Material.get_by_name('「黄金」的教导') + assert material.id == 'i_431' + assert material.type == '天赋培养素材' + assert 2 in material.weekdays + assert '土的象' in material.description + @staticmethod @flaky(3, 1) async def test_name_list(): diff --git a/utils/const.py b/utils/const.py index eadd69d..84220fd 100644 --- a/utils/const.py +++ b/utils/const.py @@ -2,7 +2,7 @@ from pathlib import Path __all__ = [ - 'PROJECT_ROOT', 'PLUGIN_DIR', + 'PROJECT_ROOT', 'PLUGIN_DIR', 'RESOURCE_DIR', 'NOT_SET', ] @@ -10,5 +10,7 @@ __all__ = [ PROJECT_ROOT = Path(__file__).joinpath('../..').resolve() # 插件目录 PLUGIN_DIR = PROJECT_ROOT / 'plugins' +# 资源目录 +RESOURCE_DIR = PROJECT_ROOT / 'resources' NOT_SET = object() diff --git a/utils/helpers.py b/utils/helpers.py index a2b3439..0b810f7 100644 --- a/utils/helpers.py +++ b/utils/helpers.py @@ -1,7 +1,9 @@ +import asyncio import hashlib import os +from multiprocessing import RLock from pathlib import Path -from typing import Tuple, Union, Optional, cast +from typing import Optional, Tuple, Union, cast import aiofiles import genshin @@ -130,3 +132,42 @@ def region_server(uid: Union[int, str]) -> RegionEnum: return region else: raise TypeError(f"UID {uid} isn't associated with any region") + + +def mkdir(path: Path) -> Path: + """根据路径依次创建文件夹""" + path_list = [] + + parent = path.parent if path.suffix else path + while not parent.exists(): + path_list.append(parent) + try: + parent.mkdir(exist_ok=True) + except FileNotFoundError: + parent = parent.parent + + while path_list: + path_list.pop().mkdir(exist_ok=True) + + return path + + +class Event: + """一个线程安装的事件对象""" + _event: asyncio.Event = asyncio.Event() + _lock = RLock() + + async def wait(self) -> bool: + return await self._event.wait() + + def set(self): + with self._lock: + self._event.set() + + def clear(self): + with self._lock: + self._event.clear() + + def is_set(self) -> bool: + with self._lock: + return self._event.is_set() diff --git a/utils/typedefs.py b/utils/typedefs.py index 01f7f04..38fce49 100644 --- a/utils/typedefs.py +++ b/utils/typedefs.py @@ -1,14 +1,17 @@ from pathlib import Path from types import TracebackType -from typing import Optional, Tuple, Type, Union, Dict, Any +from typing import Any, Dict, Optional, Tuple, Type, Union + +from httpx import URL __all__ = [ - 'StrOrPath', + 'StrOrPath', 'StrOrURL', 'SysExcInfoType', 'ExceptionInfoType', 'JSONDict', ] StrOrPath = Union[str, Path] +StrOrURL = Union[str, URL] SysExcInfoType = Union[ Tuple[Type[BaseException], BaseException, Optional[TracebackType]], Tuple[None, None, None]