diff --git a/core/dependence/assets.py b/core/dependence/assets.py deleted file mode 100644 index 553a2f6..0000000 --- a/core/dependence/assets.py +++ /dev/null @@ -1,566 +0,0 @@ -"""用于下载和管理角色、武器、材料等的图标""" - -from __future__ import annotations - -import asyncio -import re -from abc import ABC, abstractmethod -from functools import cached_property, lru_cache, partial -from multiprocessing import RLock as Lock -from pathlib import Path -from ssl import SSLZeroReturnError -from typing import AsyncIterator, Awaitable, Callable, ClassVar, Dict, Optional, TYPE_CHECKING, TypeVar, Union - -from aiofiles import open as async_open -from aiofiles.os import remove as async_remove -from enkanetwork import Assets as EnkaAssets -from enkanetwork.model.assets import CharacterAsset as EnkaCharacterAsset -from httpx import AsyncClient, HTTPError, HTTPStatusError, TransportError, URL -from typing_extensions import Self - -from core.base_service import BaseService -from core.config import config -from metadata.genshin import AVATAR_DATA, HONEY_DATA, MATERIAL_DATA, NAMECARD_DATA, WEAPON_DATA -from metadata.scripts.honey import update_honey_metadata -from metadata.scripts.metadatas import update_metadata_from_ambr, update_metadata_from_github -from metadata.shortname import roleToId, weaponToId -from modules.wiki.base import HONEY_HOST -from utils.const import AMBR_HOST, ENKA_HOST, PROJECT_ROOT -from utils.log import logger -from utils.typedefs import StrOrInt, StrOrURL - -if TYPE_CHECKING: - from httpx import Response - from multiprocessing.synchronize import RLock - -__all__ = ("AssetsServiceType", "AssetsService", "AssetsServiceError", "AssetsCouldNotFound", "DEFAULT_EnkaAssets") - -ICON_TYPE = Union[Callable[[bool], Awaitable[Optional[Path]]], Callable[..., Awaitable[Optional[Path]]]] -NAME_MAP_TYPE = Dict[str, StrOrURL] - -ASSETS_PATH = PROJECT_ROOT.joinpath("resources/assets") -ASSETS_PATH.mkdir(exist_ok=True, parents=True) - -DATA_MAP = {"avatar": AVATAR_DATA, "weapon": WEAPON_DATA, "material": MATERIAL_DATA} - -DEFAULT_EnkaAssets = EnkaAssets(lang="chs") - - -class AssetsServiceError(Exception): - pass - - -class AssetsCouldNotFound(AssetsServiceError): - def __init__(self, message: str, target: str): - self.message = message - self.target = target - super().__init__(f"{message}: target={target}") - - -class _AssetsService(ABC): - _lock: ClassVar["RLock"] = Lock() - _dir: ClassVar[Path] - icon_types: ClassVar[list[str]] - - _client: Optional[AsyncClient] = None - _links: dict[str, str] = {} - - id: int - type: str - - icon: ICON_TYPE - """图标""" - - @abstractmethod - @cached_property - def game_name(self) -> str: - """游戏数据中的名称""" - - @cached_property - def honey_id(self) -> str: - """当前资源在 Honey Impact 所对应的 ID""" - return HONEY_DATA[self.type].get(str(self.id), [""])[0] - - @property - def path(self) -> Path: - """当前资源的文件夹""" - result = self._dir.joinpath(str(self.id)).resolve() - result.mkdir(exist_ok=True, parents=True) - return result - - @property - def client(self) -> AsyncClient: - with self._lock: - if self._client is None or self._client.is_closed: - self._client = AsyncClient() - return self._client - - def __init__(self, client: Optional[AsyncClient] = None) -> None: - self._client = client - - def __call__(self, target: int) -> Self: - """用于生成与 target 对应的 assets""" - result = self.__class__(self.client) - result.id = target - return result - - def __init_subclass__(cls, **kwargs) -> None: - """初始化一些类变量""" - from itertools import chain - - cls.icon_types = [ # 支持的图标类型 - k - for k, v in chain(cls.__annotations__.items(), *map(lambda x: x.__annotations__.items(), cls.__bases__)) - if v in [ICON_TYPE, "ICON_TYPE"] - ] - cls.type = cls.__name__.lstrip("_").split("Assets")[0].lower() # 当前 assert 的类型 - cls._dir = ASSETS_PATH.joinpath(cls.type) # 图标保存的文件夹 - cls._dir.mkdir(exist_ok=True, parents=True) - - async def _request(self, url: str, interval: float = 0.2) -> "Response": - error = None - for _ in range(5): - try: - response = await self.client.get(url, follow_redirects=False) - if response.headers.get("content-length", None) == "2358": - continue - return response - except (TransportError, SSLZeroReturnError) as e: - error = e - await asyncio.sleep(interval) - continue - if error is not None: - raise error - - async def _download(self, url: StrOrURL, path: Path, retry: int = 5) -> Path | None: - """从 url 下载图标至 path""" - logger.debug("正在从 %s 下载图标至 %s", url, path) - headers = None - if config.enka_network_api_agent is not None and URL(url).host == "enka.network": - headers = {"user-agent": config.enka_network_api_agent} - for time in range(retry): - try: - response = await self.client.get(url, follow_redirects=False, headers=headers) - except Exception as error: # pylint: disable=W0703 - if not isinstance(error, (HTTPError, SSLZeroReturnError)): - logger.error(error) # 打印未知错误 - if time != retry - 1: # 未达到重试次数 - await asyncio.sleep(1) - else: - raise error - continue - if response.status_code != 200: # 判定页面是否正常 - return None - async with async_open(path, "wb") as file: - await file.write(response.content) # 保存图标 - return path.resolve() - - async def _get_from_ambr(self, item: str) -> AsyncIterator[str | None]: # pylint: disable=W0613,R0201 - """从 ambr.top 上获取目标链接""" - yield None - - async def _get_from_enka(self, item: str) -> AsyncIterator[str | None]: # pylint: disable=W0613,R0201 - """从 enke.network 上获取目标链接""" - yield None - - async def _get_from_honey(self, item: str) -> AsyncIterator[str | None]: - """从 honey 上获取目标链接""" - if (honey_name := self.honey_name_map.get(item, None)) is not None: - yield HONEY_HOST.join(f"img/{honey_name}.png") - yield HONEY_HOST.join(f"img/{honey_name}.webp") - - async def _download_url_generator(self, item: str) -> AsyncIterator[str]: - # 获取当前 `AssetsService` 的所有爬虫 - for func in map(lambda x: getattr(self, x), sorted(filter(lambda x: x.startswith("_get_from_"), dir(self)))): - async for url in func(item): - if url is not None: - try: - if (response := await self._request(url := str(url))) is None: - continue - response.raise_for_status() - yield url - except HTTPStatusError: - continue - - async def _get_download_url(self, item: str) -> str | None: - """获取图标的下载链接""" - async for url in self._download_url_generator(item): - if url is not None: - return url - - async def _get_img(self, overwrite: bool = False, *, item: str) -> Path | None: - """获取图标""" - path = next(filter(lambda x: x.stem == item, self.path.iterdir()), None) - if not overwrite and path: # 如果需要下载的图标存在且不覆盖( overwrite ) - return path.resolve() - if path is not None and path.exists(): - if overwrite: # 如果覆盖 - await async_remove(path) # 删除已存在的图标 - else: - return path - # 依次从使用当前 assets class 中的爬虫下载图标,顺序为爬虫名的字母顺序 - async for url in self._download_url_generator(item): - if url is not None: - path = self.path.joinpath(f"{item}{Path(url).suffix}") - if (result := await self._download(url, path)) is not None: - return result - - @lru_cache - async def get_link(self, item: str) -> str | None: - """获取相应图标链接""" - return await self._get_download_url(item) - - def __getattr__(self, item: str): - """魔法""" - if item in self.icon_types: - return partial(self._get_img, item=item) - object.__getattribute__(self, item) - return None - - @abstractmethod - @cached_property - def game_name_map(self) -> dict[str, str]: - """游戏中的图标名""" - - @abstractmethod - @cached_property - def honey_name_map(self) -> dict[str, str]: - """来自honey的图标名""" - - -class _AvatarAssets(_AssetsService): - enka: EnkaCharacterAsset | None - - side: ICON_TYPE - """侧视图图标""" - - card: ICON_TYPE - """卡片图标""" - - gacha: ICON_TYPE - """抽卡立绘""" - - gacha_card: ICON_TYPE - """抽卡卡片""" - - AVATAR_DEFAULT: int = 10000005 - """默认角色ID""" - - @cached_property - def game_name(self) -> str: - icon = "UI_AvatarIcon_" - if (avatar := AVATAR_DATA.get(str(self.id), None)) is not None: - icon = avatar["icon"] - else: - for aid, avatar in AVATAR_DATA.items(): - if aid.startswith(str(self.id)): - icon = avatar["icon"] - return re.findall(r"UI_AvatarIcon_(.*)", icon)[0] - - @cached_property - def honey_id(self) -> str: - return HONEY_DATA["avatar"].get(str(self.id), "")[0] - - @cached_property - def enka(self) -> Optional[EnkaCharacterAsset]: - api = getattr(self, "_enka_api", None) - cid = getattr(self, "id", None) - return None if api is None or cid is None else api.character(cid) - - def __init__(self, client: Optional[AsyncClient] = None, enka: Optional[EnkaAssets] = None): - super().__init__(client) - self._enka_api = enka or DEFAULT_EnkaAssets - - def __call__(self, target: StrOrInt) -> "_AvatarAssets": - if target == 0: - target = self.AVATAR_DEFAULT - temp = target - result = _AvatarAssets(self.client) - if isinstance(target, str): - try: - target = int(target) - except ValueError: - target = roleToId(target) - if isinstance(target, str) or target is None: - raise AssetsCouldNotFound("找不到对应的角色", temp) - result.id = target - result._enka_api = self._enka_api - return result - - async def _get_from_ambr(self, item: str) -> AsyncIterator[str | None]: - if item in {"icon", "side", "gacha"}: - yield str(AMBR_HOST.join(f"assets/UI/{self.game_name_map[item]}.png")) - - async def _get_from_enka(self, item: str) -> AsyncIterator[str | None]: - if (item_id := self.game_name_map.get(item)) is not None: - yield str(ENKA_HOST.join(f"ui/{item_id}.png")) - - @cached_property - def honey_name_map(self) -> dict[str, str]: - return { - "icon": f"{self.honey_id}_icon", - "side": f"{self.honey_id}_side_icon", - "gacha": f"{self.honey_id}_gacha_splash", - "gacha_card": f"{self.honey_id}_gacha_card", - } - - @cached_property - def game_name_map(self) -> dict[str, str]: - return { - "icon": f"UI_AvatarIcon_{self.game_name}", - "card": f"UI_AvatarIcon_{self.game_name}_Card", - "side": f"UI_AvatarIcon_Side_{self.game_name}", - "gacha": f"UI_Gacha_AvatarImg_{self.game_name}", - } - - -class _WeaponAssets(_AssetsService): - awaken: ICON_TYPE - """突破后图标""" - - gacha: ICON_TYPE - """抽卡立绘""" - - @cached_property - def game_name(self) -> str: - return re.findall(r"UI_EquipIcon_(.*)", WEAPON_DATA[str(self.id)]["icon"])[0] - - @cached_property - def game_name_map(self) -> dict[str, str]: - return { - "icon": f"UI_EquipIcon_{self.game_name}", - "awaken": f"UI_EquipIcon_{self.game_name}_Awaken", - "gacha": f"UI_Gacha_EquipIcon_{self.game_name}", - } - - @cached_property - def honey_id(self) -> str: - return f"i_n{self.id}" - - def __call__(self, target: StrOrInt) -> Self: - temp = target - result = _WeaponAssets(self.client) - if isinstance(target, str): - target = int(target) if target.isnumeric() else weaponToId(target) - if isinstance(target, str) or target is None: - raise AssetsCouldNotFound("找不到对应的武器", temp) - result.id = target - return result - - async def _get_from_ambr(self, item: str) -> AsyncIterator[str | None]: - if item == "icon": - yield str(AMBR_HOST.join(f"assets/UI/{self.game_name_map.get(item)}.png")) - - async def _get_from_enka(self, item: str) -> AsyncIterator[str | None]: - if item in self.game_name_map: - yield str(ENKA_HOST.join(f"ui/{self.game_name_map.get(item)}.png")) - - @cached_property - def honey_name_map(self) -> dict[str, str]: - return { - "icon": f"{self.honey_id}", - "awaken": f"{self.honey_id}_awaken_icon", - "gacha": f"{self.honey_id}_gacha_icon", - } - - -class _MaterialAssets(_AssetsService): - @cached_property - def game_name(self) -> str: - return str(self.id) - - @cached_property - def game_name_map(self) -> dict[str, str]: - return {"icon": f"UI_ItemIcon_{self.game_name}"} - - @cached_property - def honey_name_map(self) -> dict[str, str]: - return {"icon": self.honey_id} - - def __call__(self, target: StrOrInt) -> Self: - temp = target - result = _MaterialAssets(self.client) - if isinstance(target, str): - if target.isnumeric(): - target = int(target) - else: - target = {v["name"]: int(k) for k, v in MATERIAL_DATA.items()}.get(target) - if isinstance(target, str) or target is None: - raise AssetsCouldNotFound("找不到对应的素材", temp) - result.id = target - return result - - async def _get_from_ambr(self, item: str) -> AsyncIterator[str | None]: - if item == "icon": - yield str(AMBR_HOST.join(f"assets/UI/{self.game_name_map.get(item)}.png")) - - async def _get_from_honey(self, item: str) -> AsyncIterator[str | None]: - yield HONEY_HOST.join(f"/img/{self.honey_name_map.get(item)}.png") - yield HONEY_HOST.join(f"/img/{self.honey_name_map.get(item)}.webp") - - -class _ArtifactAssets(_AssetsService): - flower: ICON_TYPE - """生之花""" - - plume: ICON_TYPE - """死之羽""" - - sands: ICON_TYPE - """时之沙""" - - goblet: ICON_TYPE - """空之杯""" - - circlet: ICON_TYPE - """理之冠""" - - @cached_property - def honey_id(self) -> str: - return HONEY_DATA["artifact"][str(self.id)][0] - - @cached_property - def game_name(self) -> str: - return f"UI_RelicIcon_{self.id}" - - async def _get_from_enka(self, item: str) -> AsyncIterator[str | None]: - if item in self.game_name_map: - yield str(ENKA_HOST.join(f"ui/{self.game_name_map.get(item)}.png")) - - async def _get_from_ambr(self, item: str) -> AsyncIterator[str | None]: - if item in self.game_name_map: - yield str(AMBR_HOST.join(f"assets/UI/reliquary/{self.game_name_map[item]}.png")) - - @cached_property - def game_name_map(self) -> dict[str, str]: - return { - "icon": f"UI_RelicIcon_{self.id}_4", - "flower": f"UI_RelicIcon_{self.id}_4", - "plume": f"UI_RelicIcon_{self.id}_2", - "sands": f"UI_RelicIcon_{self.id}_5", - "goblet": f"UI_RelicIcon_{self.id}_1", - "circlet": f"UI_RelicIcon_{self.id}_3", - } - - @cached_property - def honey_name_map(self) -> dict[str, str]: - first_id = int(re.findall(r"\d+", HONEY_DATA["artifact"][str(self.id)][-1])[0]) - return { - "icon": f"i_n{first_id + 30}", - "flower": f"i_n{first_id + 30}", - "plume": f"i_n{first_id + 10}", - "sands": f"i_n{first_id + 40}", - "goblet": f"i_n{first_id}", - "circlet": f"i_n{first_id + 20}", - } - - -class _NamecardAssets(_AssetsService): - enka: EnkaCharacterAsset | None - - navbar: ICON_TYPE - """好友名片背景""" - - profile: ICON_TYPE - """个人资料名片背景""" - - NAME_CARD_DEFAULT: int = 210189 - """默认名片 ID""" - - @cached_property - def honey_id(self) -> str: - return HONEY_DATA["namecard"][str(self.id)][0] - - @cached_property - def game_name(self) -> str: - return NAMECARD_DATA[str(self.id)]["icon"] - - @lru_cache - def _get_id_from_avatar_id(self, avatar_id: Union[int, str]) -> int: - avatar_icon_name = AVATAR_DATA[str(avatar_id)]["icon"].split("_")[-1] - fallback = None - for namecard_id, namecard_data in NAMECARD_DATA.items(): - if namecard_data["icon"].split("_")[-1] == avatar_icon_name: - return int(namecard_id) - if avatar_icon_name in namecard_data["icon"].split("_")[-1]: - fallback = int(namecard_id) - if fallback: - return fallback - raise ValueError(avatar_id) - - def __call__(self, target: int) -> "_NamecardAssets": - if target == 0: - target = self.NAME_CARD_DEFAULT - result = _NamecardAssets(self.client) - target = int(target) if not isinstance(target, int) else target - if target > 10000000: - target = self._get_id_from_avatar_id(target) - result.id = target - result.enka = DEFAULT_EnkaAssets.namecards(target) - return result - - async def _get_from_ambr(self, item: str) -> AsyncIterator[str | None]: - if item == "profile": - yield AMBR_HOST.join(f"assets/UI/namecard/{self.game_name_map[item]}.png.png") - - async def _get_from_enka(self, item: str) -> AsyncIterator[str | None]: - if (url := getattr(self.enka, {"profile": "banner"}.get(item, item), None)) is not None: - yield url.url - - @cached_property - def game_name_map(self) -> dict[str, str]: - return { - "icon": self.game_name, - "navbar": NAMECARD_DATA[str(self.id)]["navbar"], - "profile": NAMECARD_DATA[str(self.id)]["profile"], - } - - @cached_property - def honey_name_map(self) -> dict[str, str]: - return { - "icon": self.honey_id, - "navbar": f"{self.honey_id}_back", - "profile": f"{self.honey_id}_profile", - } - - -class AssetsService(BaseService.Dependence): - """asset服务 - - 用于储存和管理 asset : - 当对应的 asset (如某角色图标)不存在时,该服务会先查找本地。 - 若本地不存在,则从网络上下载;若存在,则返回其路径 - """ - - avatar: _AvatarAssets - """角色""" - - weapon: _WeaponAssets - """武器""" - - material: _MaterialAssets - """素材""" - - artifact: _ArtifactAssets - """圣遗物""" - - namecard: _NamecardAssets - """名片""" - - def __init__(self): - for attr, assets_type_name in filter( - lambda x: (not x[0].startswith("_")) and x[1].endswith("Assets"), self.__annotations__.items() - ): - setattr(self, attr, globals()[assets_type_name]()) - - async def initialize(self) -> None: # pylint: disable=R0201 - """启动 AssetsService 服务,刷新元数据""" - logger.info("正在刷新元数据") - # todo 这3个任务同时异步下载 - await update_metadata_from_github(False) - await update_metadata_from_ambr(False) - await update_honey_metadata(False) - logger.info("刷新元数据成功") - - -AssetsServiceType = TypeVar("AssetsServiceType", bound=_AssetsService) diff --git a/core/dependence/assets.pyi b/core/dependence/assets.pyi deleted file mode 100644 index dc1a470..0000000 --- a/core/dependence/assets.pyi +++ /dev/null @@ -1,175 +0,0 @@ -from __future__ import annotations - -from abc import ABC, abstractmethod -from functools import partial -from pathlib import Path -from typing import Awaitable, Callable, ClassVar, TypeVar - -from enkanetwork import Assets as EnkaAssets -from enkanetwork.model.assets import CharacterAsset as EnkaCharacterAsset -from httpx import AsyncClient -from typing_extensions import Self - -from core.base_service import BaseService -from utils.typedefs import StrOrInt - -__all__ = ("AssetsServiceType", "AssetsService", "AssetsServiceError", "AssetsCouldNotFound", "DEFAULT_EnkaAssets") - -ICON_TYPE = Callable[[bool], Awaitable[Path | None]] | Callable[..., Awaitable[Path | None]] -DEFAULT_EnkaAssets: EnkaAssets -_GET_TYPE = partial | list[str] | int | str | ICON_TYPE | Path | AsyncClient | None | Self | dict[str, str] - -class AssetsServiceError(Exception): ... - -class AssetsCouldNotFound(AssetsServiceError): - message: str - target: str - def __init__(self, message: str, target: str): ... - -class _AssetsService(ABC): - icon_types: ClassVar[list[str]] - id: int - type: str - - icon: ICON_TYPE - """图标""" - - @abstractmethod - @property - def game_name(self) -> str: - """游戏数据中的名称""" - - @property - def honey_id(self) -> str: - """当前资源在 Honey Impact 所对应的 ID""" - - @property - def path(self) -> Path: - """当前资源的文件夹""" - - @property - def client(self) -> AsyncClient: - """当前的 http client""" - - def __init__(self, client: AsyncClient | None = None) -> None: ... - def __call__(self, target: int) -> Self: - """用于生成与 target 对应的 assets""" - - def __getattr__(self, item: str) -> _GET_TYPE: - """魔法""" - - async def get_link(self, item: str) -> str | None: - """获取相应图标链接""" - - @abstractmethod - @property - def game_name_map(self) -> dict[str, str]: - """游戏中的图标名""" - - @abstractmethod - @property - def honey_name_map(self) -> dict[str, str]: - """来自honey的图标名""" - -class _AvatarAssets(_AssetsService): - enka: EnkaCharacterAsset | None - - side: ICON_TYPE - """侧视图图标""" - - card: ICON_TYPE - """卡片图标""" - - gacha: ICON_TYPE - """抽卡立绘""" - - gacha_card: ICON_TYPE - """抽卡卡片""" - - @property - def honey_name_map(self) -> dict[str, str]: ... - @property - def game_name_map(self) -> dict[str, str]: ... - @property - def enka(self) -> EnkaCharacterAsset | None: ... - def __init__(self, client: AsyncClient | None = None, enka: EnkaAssets | None = None) -> None: ... - def __call__(self, target: StrOrInt) -> Self: ... - def __getitem__(self, item: str) -> _GET_TYPE | EnkaCharacterAsset: ... - def game_name(self) -> str: ... - -class _WeaponAssets(_AssetsService): - awaken: ICON_TYPE - """突破后图标""" - - gacha: ICON_TYPE - """抽卡立绘""" - - @property - def honey_name_map(self) -> dict[str, str]: ... - @property - def game_name_map(self) -> dict[str, str]: ... - def __call__(self, target: StrOrInt) -> Self: ... - def game_name(self) -> str: ... - -class _MaterialAssets(_AssetsService): - @property - def honey_name_map(self) -> dict[str, str]: ... - @property - def game_name_map(self) -> dict[str, str]: ... - def __call__(self, target: StrOrInt) -> Self: ... - def game_name(self) -> str: ... - -class _ArtifactAssets(_AssetsService): - flower: ICON_TYPE - """生之花""" - - plume: ICON_TYPE - """死之羽""" - - sands: ICON_TYPE - """时之沙""" - - goblet: ICON_TYPE - """空之杯""" - - circlet: ICON_TYPE - """理之冠""" - - @property - def honey_name_map(self) -> dict[str, str]: ... - @property - def game_name_map(self) -> dict[str, str]: ... - def game_name(self) -> str: ... - -class _NamecardAssets(_AssetsService): - enka: EnkaCharacterAsset | None - - navbar: ICON_TYPE - """好友名片背景""" - - profile: ICON_TYPE - """个人资料名片背景""" - - @property - def honey_name_map(self) -> dict[str, str]: ... - @property - def game_name_map(self) -> dict[str, str]: ... - def game_name(self) -> str: ... - -class AssetsService(BaseService.Dependence): - avatar: _AvatarAssets - """角色""" - - weapon: _WeaponAssets - """武器""" - - material: _MaterialAssets - """素材""" - - artifact: _ArtifactAssets - """圣遗物""" - - namecard: _NamecardAssets - """名片""" - -AssetsServiceType = TypeVar("AssetsServiceType", bound=_AssetsService) diff --git a/core/services/game/__init__.py b/core/services/game/__init__.py deleted file mode 100644 index 78e0e2f..0000000 --- a/core/services/game/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""GameService""" diff --git a/core/services/game/cache.py b/core/services/game/cache.py deleted file mode 100644 index 48531ca..0000000 --- a/core/services/game/cache.py +++ /dev/null @@ -1,29 +0,0 @@ -from typing import List - -from core.base_service import BaseService -from core.dependence.redisdb import RedisDB - -__all__ = ["GameCache", "GameCacheForStrategy"] - - -class GameCache: - qname: str - - def __init__(self, redis: RedisDB, ttl: int = 3600): - self.client = redis.client - self.ttl = ttl - - async def get_url_list(self, character_name: str): - qname = f"{self.qname}:{character_name}" - return [str(str_data, encoding="utf-8") for str_data in await self.client.lrange(qname, 0, -1)][::-1] - - async def set_url_list(self, character_name: str, str_list: List[str]): - qname = f"{self.qname}:{character_name}" - await self.client.ltrim(qname, 1, 0) - await self.client.lpush(qname, *str_list) - await self.client.expire(qname, self.ttl) - return await self.client.llen(qname) - - -class GameCacheForStrategy(BaseService.Component, GameCache): - qname = "game:strategy" diff --git a/core/services/game/services.py b/core/services/game/services.py deleted file mode 100644 index e411525..0000000 --- a/core/services/game/services.py +++ /dev/null @@ -1,52 +0,0 @@ -from typing import List, Optional - -from core.base_service import BaseService -from core.services.game.cache import GameCacheForStrategy -from modules.apihelper.client.components.hyperion import Hyperion - -__all__ = "GameStrategyService" - - -class GameStrategyService(BaseService): - def __init__(self, cache: GameCacheForStrategy, collections: Optional[List[int]] = None): - self._cache = cache - self._hyperion = Hyperion() - if collections is None: - self._collections = [2319292, 2319293, 2319294, 2319295, 2319296, 2319298, 2319299] - else: - self._collections = collections - self._special_posts = {"达达利亚": "21272578"} - - async def _get_strategy_from_hyperion(self, collection_id: int, character_name: str) -> int: - if character_name in self._special_posts: - return self._special_posts[character_name] - post_id: int = -1 - post_full_in_collection = await self._hyperion.get_post_full_in_collection(collection_id) - for post_data in post_full_in_collection["posts"]: - title = post_data["post"]["subject"] - topics = post_data["topics"] - for topic in topics: - if character_name == topic["name"]: - post_id = int(post_data["post"]["post_id"]) - break - if post_id == -1 and title and character_name in title: - post_id = int(post_data["post"]["post_id"]) - if post_id != -1: - break - return post_id - - async def get_strategy(self, character_name: str) -> str: - cache = await self._cache.get_url_list(character_name) - if len(cache) >= 1: - return cache[0] - - for collection_id in self._collections: - post_id = await self._get_strategy_from_hyperion(collection_id, character_name) - if post_id != -1: - break - else: - return "" - - artwork_info = await self._hyperion.get_post_info(2, post_id) - await self._cache.set_url_list(character_name, artwork_info.image_urls) - return artwork_info.image_urls[0] diff --git a/core/services/players/services.py b/core/services/players/services.py index f44ca0a..b52a56b 100644 --- a/core/services/players/services.py +++ b/core/services/players/services.py @@ -1,23 +1,11 @@ from datetime import datetime, timedelta from typing import Optional -from enkanetwork import ( - VaildateUIDError, - HTTPException, - EnkaPlayerNotFound, - PlayerInfo as EnkaPlayerInfo, - TimedOut, - EnkaServerMaintanance, -) - from core.base_service import BaseService -from core.config import config from core.dependence.redisdb import RedisDB from core.services.players.models import PlayersDataBase as Player, PlayerInfoSQLModel, PlayerInfo from core.services.players.repositories import PlayerInfoRepository from gram_core.services.players.services import PlayersService -from utils.enkanetwork import RedisCache, EnkaNetworkAPI -from utils.log import logger __all__ = ("PlayersService", "PlayerInfoService") @@ -26,8 +14,6 @@ class PlayerInfoService(BaseService): def __init__(self, redis: RedisDB, players_info_repository: PlayerInfoRepository): self.cache = redis.client self._players_info_repository = players_info_repository - self.enka_client = EnkaNetworkAPI(lang="chs", user_agent=config.enka_network_api_agent) - self.enka_client.set_cache(RedisCache(redis.client, key="players_info:enka_network", ex=60)) self.qname = "players_info" async def get_form_cache(self, player: Player): @@ -42,22 +28,7 @@ class PlayerInfoService(BaseService): qname = f"{self.qname}:{player.user_id}:{player.player_id}" await self.cache.set(qname, player.json(), ex=60) - async def get_player_info_from_enka(self, player_id: int) -> Optional[EnkaPlayerInfo]: - try: - response = await self.enka_client.fetch_user(player_id, info=True) - return response.player - except VaildateUIDError: - logger.warning("Enka.Network 请求失败 UID 不正确") - except EnkaPlayerNotFound: - logger.warning("Enka.Network 请求失败 玩家不存在") - except EnkaServerMaintanance: - logger.warning("Enka.Network 正在进行服务器维护,请耐心等待5至8小时或者1天。") - except TimedOut: - logger.warning("Enka.Network 请求超时") - except HTTPException as exc: - logger.warning("Enka.Network 请求失败: %s", str(exc)) - except Exception as exc: - logger.error("Enka.Network 请求失败: %s", exc_info=exc) + async def get_player_info_from_enka(self, player_id: int) -> None: return None async def get(self, player: Player) -> Optional[PlayerInfo]: diff --git a/core/services/quiz/__init__.py b/core/services/quiz/__init__.py deleted file mode 100644 index eff5d65..0000000 --- a/core/services/quiz/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""QuizService""" diff --git a/core/services/quiz/cache.py b/core/services/quiz/cache.py deleted file mode 100644 index 22830dc..0000000 --- a/core/services/quiz/cache.py +++ /dev/null @@ -1,69 +0,0 @@ -from typing import List - -from core.base_service import BaseService -from core.dependence.redisdb import RedisDB -from core.services.quiz.models import Answer, Question - -__all__ = ("QuizCache",) - - -class QuizCache(BaseService.Component): - def __init__(self, redis: RedisDB): - self.client = redis.client - self.question_qname = "quiz:question" - self.answer_qname = "quiz:answer" - - async def get_all_question(self) -> List[Question]: - temp_list = [] - qname = self.question_qname + "id_list" - data_list = [self.question_qname + f":{question_id}" for question_id in await self.client.lrange(qname, 0, -1)] - data = await self.client.mget(data_list) - for i in data: - temp_list.append(Question.parse_raw(i)) - return temp_list - - async def get_all_question_id_list(self) -> List[str]: - qname = self.question_qname + ":id_list" - return await self.client.lrange(qname, 0, -1) - - async def get_one_question(self, question_id: int) -> Question: - qname = f"{self.question_qname}:{question_id}" - data = await self.client.get(qname) - json_data = str(data, encoding="utf-8") - return Question.parse_raw(json_data) - - async def get_one_answer(self, answer_id: int) -> Answer: - qname = f"{self.answer_qname}:{answer_id}" - data = await self.client.get(qname) - json_data = str(data, encoding="utf-8") - return Answer.parse_raw(json_data) - - async def add_question(self, question_list: List[Question] = None) -> int: - if not question_list: - return 0 - for question in question_list: - await self.client.set(f"{self.question_qname}:{question.question_id}", question.json()) - question_id_list = [question.question_id for question in question_list] - await self.client.lpush(f"{self.question_qname}:id_list", *question_id_list) - return await self.client.llen(f"{self.question_qname}:id_list") - - async def del_all_question(self): - keys = await self.client.keys(f"{self.question_qname}*") - if keys is not None: - for key in keys: - await self.client.delete(key) - - async def del_all_answer(self): - keys = await self.client.keys(f"{self.answer_qname}*") - if keys is not None: - for key in keys: - await self.client.delete(key) - - async def add_answer(self, answer_list: List[Answer] = None) -> int: - if not answer_list: - return 0 - for answer in answer_list: - await self.client.set(f"{self.answer_qname}:{answer.answer_id}", answer.json()) - answer_id_list = [answer.answer_id for answer in answer_list] - await self.client.lpush(f"{self.answer_qname}:id_list", *answer_id_list) - return await self.client.llen(f"{self.answer_qname}:id_list") diff --git a/core/services/quiz/models.py b/core/services/quiz/models.py deleted file mode 100644 index 4689495..0000000 --- a/core/services/quiz/models.py +++ /dev/null @@ -1,53 +0,0 @@ -from typing import List, Optional - -from pydantic import BaseModel -from sqlmodel import Column, Field, ForeignKey, Integer, SQLModel - -__all__ = ("Answer", "AnswerDB", "Question", "QuestionDB") - - -class AnswerDB(SQLModel, table=True): - __tablename__ = "answer" - __table_args__ = dict(mysql_charset="utf8mb4", mysql_collate="utf8mb4_general_ci") - - id: Optional[int] = Field(default=None, sa_column=Column(Integer, primary_key=True, autoincrement=True)) - question_id: Optional[int] = Field( - sa_column=Column(Integer, ForeignKey("question.id", ondelete="RESTRICT", onupdate="RESTRICT")) - ) - is_correct: Optional[bool] = Field() - text: Optional[str] = Field() - - -class QuestionDB(SQLModel, table=True): - __tablename__ = "question" - __table_args__ = dict(mysql_charset="utf8mb4", mysql_collate="utf8mb4_general_ci") - - id: Optional[int] = Field(default=None, sa_column=Column(Integer, primary_key=True, autoincrement=True)) - text: Optional[str] = Field() - - -class Answer(BaseModel): - answer_id: int = 0 - question_id: int = 0 - is_correct: bool = True - text: str = "" - - def to_database_data(self) -> AnswerDB: - return AnswerDB(id=self.answer_id, question_id=self.question_id, text=self.text, is_correct=self.is_correct) - - @classmethod - def de_database_data(cls, data: AnswerDB) -> Optional["Answer"]: - return cls(answer_id=data.id, question_id=data.question_id, text=data.text, is_correct=data.is_correct) - - -class Question(BaseModel): - question_id: int = 0 - text: str = "" - answers: List[Answer] = [] - - def to_database_data(self) -> QuestionDB: - return QuestionDB(text=self.text, id=self.question_id) - - @classmethod - def de_database_data(cls, data: QuestionDB) -> Optional["Question"]: - return cls(question_id=data.id, text=data.text) diff --git a/core/services/quiz/repositories.py b/core/services/quiz/repositories.py deleted file mode 100644 index a8a1fa9..0000000 --- a/core/services/quiz/repositories.py +++ /dev/null @@ -1,57 +0,0 @@ -from typing import List - -from sqlmodel import select -from sqlmodel.ext.asyncio.session import AsyncSession - -from core.base_service import BaseService -from core.dependence.database import Database -from core.services.quiz.models import AnswerDB, QuestionDB - -__all__ = ("QuizRepository",) - - -class QuizRepository(BaseService.Component): - def __init__(self, database: Database): - self.engine = database.engine - - async def get_question_list(self) -> List[QuestionDB]: - async with AsyncSession(self.engine) as session: - query = select(QuestionDB) - results = await session.exec(query) - return results.all() - - async def get_answers_from_question_id(self, question_id: int) -> List[AnswerDB]: - async with AsyncSession(self.engine) as session: - query = select(AnswerDB).where(AnswerDB.question_id == question_id) - results = await session.exec(query) - return results.all() - - async def add_question(self, question: QuestionDB): - async with AsyncSession(self.engine) as session: - session.add(question) - await session.commit() - - async def get_question_by_text(self, text: str) -> QuestionDB: - async with AsyncSession(self.engine) as session: - query = select(QuestionDB).where(QuestionDB.text == text) - results = await session.exec(query) - return results.first() - - async def add_answer(self, answer: AnswerDB): - async with AsyncSession(self.engine) as session: - session.add(answer) - await session.commit() - - async def delete_question_by_id(self, question_id: int): - async with AsyncSession(self.engine) as session: - statement = select(QuestionDB).where(QuestionDB.id == question_id) - results = await session.exec(statement) - question = results.one() - await session.delete(question) - - async def delete_answer_by_id(self, answer_id: int): - async with AsyncSession(self.engine) as session: - statement = select(AnswerDB).where(AnswerDB.id == answer_id) - results = await session.exec(statement) - answer = results.one() - await session.delete(answer) diff --git a/core/services/quiz/services.py b/core/services/quiz/services.py deleted file mode 100644 index d3e658f..0000000 --- a/core/services/quiz/services.py +++ /dev/null @@ -1,64 +0,0 @@ -import asyncio -from typing import List - -from core.base_service import BaseService -from core.services.quiz.cache import QuizCache -from core.services.quiz.models import Answer, Question -from core.services.quiz.repositories import QuizRepository - -__all__ = ("QuizService",) - - -class QuizService(BaseService): - def __init__(self, repository: QuizRepository, cache: QuizCache): - self._repository = repository - self._cache = cache - self.lock = asyncio.Lock() - - async def get_quiz_from_database(self) -> List[Question]: - """从数据库获取问题列表 - :return: Question List - """ - temp: list = [] - question_list = await self._repository.get_question_list() - for question in question_list: - question_id = question.id - answers = await self._repository.get_answers_from_question_id(question_id) - data = Question.de_database_data(question) - data.answers = [Answer.de_database_data(a) for a in answers] - temp.append(data) - return temp - - async def save_quiz(self, data: Question): - await self._repository.get_question_by_text(data.text) - for answers in data.answers: - await self._repository.add_answer(answers.to_database_data()) - - async def refresh_quiz(self) -> int: - """从数据库刷新问题到Redis缓存 线程安全 - :return: 已经缓存问题的数量 - """ - # 只允许一个线程访问该区域 让数据被安全有效的访问 - async with self.lock: - question_list = await self.get_quiz_from_database() - await self._cache.del_all_question() - question_count = await self._cache.add_question(question_list) - await self._cache.del_all_answer() - for question in question_list: - await self._cache.add_answer(question.answers) - return question_count - - async def get_question_id_list(self) -> List[int]: - return [int(question_id) for question_id in await self._cache.get_all_question_id_list()] - - async def get_answer(self, answer_id: int) -> Answer: - return await self._cache.get_one_answer(answer_id) - - async def get_question(self, question_id: int) -> Question: - return await self._cache.get_one_question(question_id) - - async def delete_question_by_id(self, question_id: int): - return await self._repository.delete_question_by_id(question_id) - - async def delete_answer_by_id(self, answer_id: int): - return await self._repository.delete_answer_by_id(answer_id) diff --git a/core/services/search/models.py b/core/services/search/models.py index 03e875e..c89eca9 100644 --- a/core/services/search/models.py +++ b/core/services/search/models.py @@ -20,6 +20,7 @@ class BaseEntry(BaseModel): parse_mode: Optional[str] = None photo_url: Optional[str] = None photo_file_id: Optional[str] = None + document_file_id: Optional[str] = None @abstractmethod def compare_to_query(self, search_query: str) -> float: diff --git a/core/services/wiki/__init__.py b/core/services/wiki/__init__.py deleted file mode 100644 index 042a3d3..0000000 --- a/core/services/wiki/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""WikiService""" diff --git a/core/services/wiki/cache.py b/core/services/wiki/cache.py deleted file mode 100644 index e6fb302..0000000 --- a/core/services/wiki/cache.py +++ /dev/null @@ -1,41 +0,0 @@ -from core.base_service import BaseService -from core.dependence.redisdb import RedisDB -from modules.wiki.base import Model - -try: - import ujson as jsonlib -except ImportError: - import json as jsonlib - - -__all__ = ["WikiCache"] - - -class WikiCache(BaseService.Component): - def __init__(self, redis: RedisDB): - self.client = redis.client - self.qname = "wiki" - - async def set(self, key: str, value): - qname = f"{self.qname}:{key}" - if isinstance(value, Model): - value = value.json() - elif isinstance(value, (dict, list)): - value = jsonlib.dumps(value) - await self.client.set(qname, value) - - async def delete(self, key: str): - qname = f"{self.qname}:{key}" - await self.client.delete(qname) - - async def get(self, key: str) -> dict: - qname = f"{self.qname}:{key}" - # noinspection PyBroadException - try: - result = jsonlib.loads(await self.client.get(qname)) - except Exception: # pylint: disable=W0703 - result = [] - if isinstance(result, list) and len(result) > 0: - for num, item in enumerate(result): - result[num] = jsonlib.loads(item) - return result diff --git a/core/services/wiki/services.py b/core/services/wiki/services.py deleted file mode 100644 index 151c773..0000000 --- a/core/services/wiki/services.py +++ /dev/null @@ -1,103 +0,0 @@ -from typing import List, Optional - -from core.base_service import BaseService -from core.services.wiki.cache import WikiCache -from modules.wiki.character import Character -from modules.wiki.weapon import Weapon -from utils.log import logger - -__all__ = ["WikiService"] - - -class WikiService(BaseService): - def __init__(self, cache: WikiCache): - self._cache = cache - """Redis 在这里的作用是作为持久化""" - self._character_list = [] - self._character_name_list = [] - self._weapon_name_list = [] - self._weapon_list = [] - self.first_run = True - - async def refresh_weapon(self): - weapon_name_list = await Weapon.get_name_list() - logger.info("一共找到 %s 把武器信息", len(weapon_name_list)) - - weapon_list = [] - num = 0 - async for weapon in Weapon.full_data_generator(): - weapon_list.append(weapon) - num += 1 - if num % 10 == 0: - logger.info("现在已经获取到 %s 把武器信息", num) - - logger.info("写入武器信息到Redis") - self._weapon_list = weapon_list - await self._cache.delete("weapon") - await self._cache.set("weapon", [i.json() for i in weapon_list]) - - async def refresh_characters(self): - character_name_list = await Character.get_name_list() - logger.info("一共找到 %s 个角色信息", len(character_name_list)) - - character_list = [] - num = 0 - async for character in Character.full_data_generator(): - character_list.append(character) - num += 1 - if num % 10 == 0: - logger.info("现在已经获取到 %s 个角色信息", num) - - logger.info("写入角色信息到Redis") - self._character_list = character_list - await self._cache.delete("characters") - await self._cache.set("characters", [i.json() for i in character_list]) - - async def refresh_wiki(self): - """ - 用于把Redis的缓存全部加载进Python - :return: - """ - logger.info("正在重新获取Wiki") - logger.info("正在重新获取武器信息") - await self.refresh_weapon() - logger.info("正在重新获取角色信息") - await self.refresh_characters() - logger.info("刷新成功") - - async def init(self): - """ - 用于把Redis的缓存全部加载进Python - :return: - """ - if self.first_run: - weapon_dict = await self._cache.get("weapon") - self._weapon_list = [Weapon.parse_obj(obj) for obj in weapon_dict] - self._weapon_name_list = [weapon.name for weapon in self._weapon_list] - characters_dict = await self._cache.get("characters") - self._character_list = [Character.parse_obj(obj) for obj in characters_dict] - self._character_name_list = [character.name for character in self._character_list] - - self.first_run = False - - async def get_weapons(self, name: str) -> Optional[Weapon]: - await self.init() - if len(self._weapon_list) == 0: - return None - return next((weapon for weapon in self._weapon_list if weapon.name == name), None) - - async def get_weapons_name_list(self) -> List[str]: - await self.init() - return self._weapon_name_list - - async def get_weapons_list(self) -> List[Weapon]: - await self.init() - return self._weapon_list - - async def get_characters_list(self) -> List[Character]: - await self.init() - return self._character_list - - async def get_characters_name_list(self) -> List[str]: - await self.init() - return self._character_name_list diff --git a/pdm.lock b/pdm.lock index 8b8e423..fea3e37 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "genshin-artifact", "pyro", "test"] strategy = ["cross_platform", "inherit_metadata"] lock_version = "4.4.2" -content_hash = "sha256:d7909b325935f473694ce0d17f58f2c7cb59d89b99bcfa6be284426ca92d6254" +content_hash = "sha256:a71862798602dbee31c53d2ad5d361d3fa0ebe0b060d0e81857c047ae3002384" [[package]] name = "aiocsv" @@ -200,7 +200,7 @@ files = [ [[package]] name = "alembic" -version = "1.13.1" +version = "1.13.2" requires_python = ">=3.8" summary = "A database migration tool for SQLAlchemy." groups = ["default"] @@ -212,8 +212,8 @@ dependencies = [ "typing-extensions>=4", ] files = [ - {file = "alembic-1.13.1-py3-none-any.whl", hash = "sha256:2edcc97bed0bd3272611ce3a98d98279e9c209e7186e43e75bbb1b2bdfdbcc43"}, - {file = "alembic-1.13.1.tar.gz", hash = "sha256:4932c8558bf68f2ee92b9bbcb8218671c627064d5b08939437af6d77dc05e595"}, + {file = "alembic-1.13.2-py3-none-any.whl", hash = "sha256:6b8733129a6224a9a711e17c99b08462dbf7cc9670ba8f2e2ae9af860ceb1953"}, + {file = "alembic-1.13.2.tar.gz", hash = "sha256:1ff0ae32975f4fd96028c39ed9bb3c867fe3af956bd7bb37343b54c9fe7445ef"}, ] [[package]] @@ -251,7 +251,7 @@ files = [ [[package]] name = "arko-wrapper" -version = "0.2.8" +version = "0.3.0" requires_python = ">=3.7" summary = "给你的Python迭代器加上魔法" groups = ["default"] @@ -259,8 +259,8 @@ dependencies = [ "typing-extensions", ] files = [ - {file = "arko-wrapper-0.2.8.tar.gz", hash = "sha256:85167bc6f1dd48e3415a23a7f2f193c1544a450fd6d219ce28043af796c9b4c3"}, - {file = "arko_wrapper-0.2.8-py3-none-any.whl", hash = "sha256:c56b8cdbbd273cc1b7737667374ee600766e9e7f9f9546871b20912024aa0fb2"}, + {file = "arko_wrapper-0.3.0-py3-none-any.whl", hash = "sha256:9230d479c5bbdb4b6add8d75def69a8aa5aad4e7057dc92aebe7d0146a4b95d7"}, + {file = "arko_wrapper-0.3.0.tar.gz", hash = "sha256:6e37ab1f2dbdd961f07313c3267c5864f5e71b39de730e384003aa2af1f68357"}, ] [[package]] @@ -441,13 +441,13 @@ files = [ [[package]] name = "certifi" -version = "2024.6.2" +version = "2024.7.4" requires_python = ">=3.6" summary = "Python package for providing Mozilla's CA Bundle." groups = ["default"] files = [ - {file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"}, - {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"}, + {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, + {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, ] [[package]] @@ -716,7 +716,7 @@ files = [ [[package]] name = "fakeredis" -version = "2.23.2" +version = "2.23.3" requires_python = "<4.0,>=3.7" summary = "Python implementation of redis API, can be used for testing purposes." groups = ["default"] @@ -726,8 +726,8 @@ dependencies = [ "typing-extensions<5.0,>=4.7; python_version < \"3.11\"", ] files = [ - {file = "fakeredis-2.23.2-py3-none-any.whl", hash = "sha256:3721946b955930c065231befd24a9cdc68b339746e93848ef01a010d98e4eb4f"}, - {file = "fakeredis-2.23.2.tar.gz", hash = "sha256:d649c409abe46c63690b6c35d3c460e4ce64c69a52cea3f02daff2649378f878"}, + {file = "fakeredis-2.23.3-py3-none-any.whl", hash = "sha256:4779be828f4ebf53e1a286fd11e2ffe0f510d3e5507f143d644e67a07387d759"}, + {file = "fakeredis-2.23.3.tar.gz", hash = "sha256:0c67caa31530114f451f012eca920338c5eb83fa7f1f461dd41b8d2488a99cba"}, ] [[package]] @@ -865,17 +865,6 @@ files = [ {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"}, ] -[[package]] -name = "gcsim-pypi" -version = "2.23.0" -requires_python = "<4.0,>=3.6" -summary = "gcsim binary for pypi" -groups = ["default"] -files = [ - {file = "gcsim_pypi-2.23.0-py3-none-any.whl", hash = "sha256:260b0fab047adc4ac69fc3380284d6635ca390c4c9de07f1a69a477764a40e4d"}, - {file = "gcsim_pypi-2.23.0.tar.gz", hash = "sha256:cff31d76d06ee020254ecdb3990af63108f2498547b976337036372c3310d946"}, -] - [[package]] name = "gitdb" version = "4.0.11" @@ -1480,7 +1469,7 @@ files = [ [[package]] name = "openpyxl" -version = "3.1.4" +version = "3.1.5" requires_python = ">=3.8" summary = "A Python library to read/write Excel 2010 xlsx/xlsm files" groups = ["default"] @@ -1488,63 +1477,68 @@ dependencies = [ "et-xmlfile", ] files = [ - {file = "openpyxl-3.1.4-py2.py3-none-any.whl", hash = "sha256:ec17f6483f2b8f7c88c57e5e5d3b0de0e3fb9ac70edc084d28e864f5b33bbefd"}, - {file = "openpyxl-3.1.4.tar.gz", hash = "sha256:8d2c8adf5d20d6ce8f9bca381df86b534835e974ed0156dacefa76f68c1d69fb"}, + {file = "openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2"}, + {file = "openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050"}, ] [[package]] name = "orjson" -version = "3.10.5" +version = "3.10.6" requires_python = ">=3.8" summary = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" groups = ["default"] files = [ - {file = "orjson-3.10.5-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:545d493c1f560d5ccfc134803ceb8955a14c3fcb47bbb4b2fee0232646d0b932"}, - {file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4324929c2dd917598212bfd554757feca3e5e0fa60da08be11b4aa8b90013c1"}, - {file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c13ca5e2ddded0ce6a927ea5a9f27cae77eee4c75547b4297252cb20c4d30e6"}, - {file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6c8e30adfa52c025f042a87f450a6b9ea29649d828e0fec4858ed5e6caecf63"}, - {file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:338fd4f071b242f26e9ca802f443edc588fa4ab60bfa81f38beaedf42eda226c"}, - {file = "orjson-3.10.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6970ed7a3126cfed873c5d21ece1cd5d6f83ca6c9afb71bbae21a0b034588d96"}, - {file = "orjson-3.10.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:235dadefb793ad12f7fa11e98a480db1f7c6469ff9e3da5e73c7809c700d746b"}, - {file = "orjson-3.10.5-cp310-none-win32.whl", hash = "sha256:be79e2393679eda6a590638abda16d167754393f5d0850dcbca2d0c3735cebe2"}, - {file = "orjson-3.10.5-cp310-none-win_amd64.whl", hash = "sha256:c4a65310ccb5c9910c47b078ba78e2787cb3878cdded1702ac3d0da71ddc5228"}, - {file = "orjson-3.10.5-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:cdf7365063e80899ae3a697def1277c17a7df7ccfc979990a403dfe77bb54d40"}, - {file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b68742c469745d0e6ca5724506858f75e2f1e5b59a4315861f9e2b1df77775a"}, - {file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7d10cc1b594951522e35a3463da19e899abe6ca95f3c84c69e9e901e0bd93d38"}, - {file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcbe82b35d1ac43b0d84072408330fd3295c2896973112d495e7234f7e3da2e1"}, - {file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c0eb7e0c75e1e486c7563fe231b40fdd658a035ae125c6ba651ca3b07936f5"}, - {file = "orjson-3.10.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:53ed1c879b10de56f35daf06dbc4a0d9a5db98f6ee853c2dbd3ee9d13e6f302f"}, - {file = "orjson-3.10.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:099e81a5975237fda3100f918839af95f42f981447ba8f47adb7b6a3cdb078fa"}, - {file = "orjson-3.10.5-cp311-none-win32.whl", hash = "sha256:1146bf85ea37ac421594107195db8bc77104f74bc83e8ee21a2e58596bfb2f04"}, - {file = "orjson-3.10.5-cp311-none-win_amd64.whl", hash = "sha256:36a10f43c5f3a55c2f680efe07aa93ef4a342d2960dd2b1b7ea2dd764fe4a37c"}, - {file = "orjson-3.10.5-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:68f85ecae7af14a585a563ac741b0547a3f291de81cd1e20903e79f25170458f"}, - {file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28afa96f496474ce60d3340fe8d9a263aa93ea01201cd2bad844c45cd21f5268"}, - {file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cd684927af3e11b6e754df80b9ffafd9fb6adcaa9d3e8fdd5891be5a5cad51e"}, - {file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d21b9983da032505f7050795e98b5d9eee0df903258951566ecc358f6696969"}, - {file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ad1de7fef79736dde8c3554e75361ec351158a906d747bd901a52a5c9c8d24b"}, - {file = "orjson-3.10.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d97531cdfe9bdd76d492e69800afd97e5930cb0da6a825646667b2c6c6c0211"}, - {file = "orjson-3.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d69858c32f09c3e1ce44b617b3ebba1aba030e777000ebdf72b0d8e365d0b2b3"}, - {file = "orjson-3.10.5-cp312-none-win32.whl", hash = "sha256:64c9cc089f127e5875901ac05e5c25aa13cfa5dbbbd9602bda51e5c611d6e3e2"}, - {file = "orjson-3.10.5-cp312-none-win_amd64.whl", hash = "sha256:b2efbd67feff8c1f7728937c0d7f6ca8c25ec81373dc8db4ef394c1d93d13dc5"}, - {file = "orjson-3.10.5-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:03b565c3b93f5d6e001db48b747d31ea3819b89abf041ee10ac6988886d18e01"}, - {file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:584c902ec19ab7928fd5add1783c909094cc53f31ac7acfada817b0847975f26"}, - {file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a35455cc0b0b3a1eaf67224035f5388591ec72b9b6136d66b49a553ce9eb1e6"}, - {file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1670fe88b116c2745a3a30b0f099b699a02bb3482c2591514baf5433819e4f4d"}, - {file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:185c394ef45b18b9a7d8e8f333606e2e8194a50c6e3c664215aae8cf42c5385e"}, - {file = "orjson-3.10.5-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ca0b3a94ac8d3886c9581b9f9de3ce858263865fdaa383fbc31c310b9eac07c9"}, - {file = "orjson-3.10.5-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dfc91d4720d48e2a709e9c368d5125b4b5899dced34b5400c3837dadc7d6271b"}, - {file = "orjson-3.10.5-cp38-none-win32.whl", hash = "sha256:c05f16701ab2a4ca146d0bca950af254cb7c02f3c01fca8efbbad82d23b3d9d4"}, - {file = "orjson-3.10.5-cp38-none-win_amd64.whl", hash = "sha256:8a11d459338f96a9aa7f232ba95679fc0c7cedbd1b990d736467894210205c09"}, - {file = "orjson-3.10.5-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:85c89131d7b3218db1b24c4abecea92fd6c7f9fab87441cfc342d3acc725d807"}, - {file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb66215277a230c456f9038d5e2d84778141643207f85336ef8d2a9da26bd7ca"}, - {file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:51bbcdea96cdefa4a9b4461e690c75ad4e33796530d182bdd5c38980202c134a"}, - {file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbead71dbe65f959b7bd8cf91e0e11d5338033eba34c114f69078d59827ee139"}, - {file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5df58d206e78c40da118a8c14fc189207fffdcb1f21b3b4c9c0c18e839b5a214"}, - {file = "orjson-3.10.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c4057c3b511bb8aef605616bd3f1f002a697c7e4da6adf095ca5b84c0fd43595"}, - {file = "orjson-3.10.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b39e006b00c57125ab974362e740c14a0c6a66ff695bff44615dcf4a70ce2b86"}, - {file = "orjson-3.10.5-cp39-none-win32.whl", hash = "sha256:eded5138cc565a9d618e111c6d5c2547bbdd951114eb822f7f6309e04db0fb47"}, - {file = "orjson-3.10.5-cp39-none-win_amd64.whl", hash = "sha256:cc28e90a7cae7fcba2493953cff61da5a52950e78dc2dacfe931a317ee3d8de7"}, - {file = "orjson-3.10.5.tar.gz", hash = "sha256:7a5baef8a4284405d96c90c7c62b755e9ef1ada84c2406c24a9ebec86b89f46d"}, + {file = "orjson-3.10.6-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:fb0ee33124db6eaa517d00890fc1a55c3bfe1cf78ba4a8899d71a06f2d6ff5c7"}, + {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c1c4b53b24a4c06547ce43e5fee6ec4e0d8fe2d597f4647fc033fd205707365"}, + {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eadc8fd310edb4bdbd333374f2c8fec6794bbbae99b592f448d8214a5e4050c0"}, + {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:61272a5aec2b2661f4fa2b37c907ce9701e821b2c1285d5c3ab0207ebd358d38"}, + {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57985ee7e91d6214c837936dc1608f40f330a6b88bb13f5a57ce5257807da143"}, + {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:633a3b31d9d7c9f02d49c4ab4d0a86065c4a6f6adc297d63d272e043472acab5"}, + {file = "orjson-3.10.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1c680b269d33ec444afe2bdc647c9eb73166fa47a16d9a75ee56a374f4a45f43"}, + {file = "orjson-3.10.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f759503a97a6ace19e55461395ab0d618b5a117e8d0fbb20e70cfd68a47327f2"}, + {file = "orjson-3.10.6-cp310-none-win32.whl", hash = "sha256:95a0cce17f969fb5391762e5719575217bd10ac5a189d1979442ee54456393f3"}, + {file = "orjson-3.10.6-cp310-none-win_amd64.whl", hash = "sha256:df25d9271270ba2133cc88ee83c318372bdc0f2cd6f32e7a450809a111efc45c"}, + {file = "orjson-3.10.6-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b1ec490e10d2a77c345def52599311849fc063ae0e67cf4f84528073152bb2ba"}, + {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d43d3feb8f19d07e9f01e5b9be4f28801cf7c60d0fa0d279951b18fae1932b"}, + {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac3045267e98fe749408eee1593a142e02357c5c99be0802185ef2170086a863"}, + {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c27bc6a28ae95923350ab382c57113abd38f3928af3c80be6f2ba7eb8d8db0b0"}, + {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d27456491ca79532d11e507cadca37fb8c9324a3976294f68fb1eff2dc6ced5a"}, + {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05ac3d3916023745aa3b3b388e91b9166be1ca02b7c7e41045da6d12985685f0"}, + {file = "orjson-3.10.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1335d4ef59ab85cab66fe73fd7a4e881c298ee7f63ede918b7faa1b27cbe5212"}, + {file = "orjson-3.10.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4bbc6d0af24c1575edc79994c20e1b29e6fb3c6a570371306db0993ecf144dc5"}, + {file = "orjson-3.10.6-cp311-none-win32.whl", hash = "sha256:450e39ab1f7694465060a0550b3f6d328d20297bf2e06aa947b97c21e5241fbd"}, + {file = "orjson-3.10.6-cp311-none-win_amd64.whl", hash = "sha256:227df19441372610b20e05bdb906e1742ec2ad7a66ac8350dcfd29a63014a83b"}, + {file = "orjson-3.10.6-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ea2977b21f8d5d9b758bb3f344a75e55ca78e3ff85595d248eee813ae23ecdfb"}, + {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b6f3d167d13a16ed263b52dbfedff52c962bfd3d270b46b7518365bcc2121eed"}, + {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f710f346e4c44a4e8bdf23daa974faede58f83334289df80bc9cd12fe82573c7"}, + {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7275664f84e027dcb1ad5200b8b18373e9c669b2a9ec33d410c40f5ccf4b257e"}, + {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0943e4c701196b23c240b3d10ed8ecd674f03089198cf503105b474a4f77f21f"}, + {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:446dee5a491b5bc7d8f825d80d9637e7af43f86a331207b9c9610e2f93fee22a"}, + {file = "orjson-3.10.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:64c81456d2a050d380786413786b057983892db105516639cb5d3ee3c7fd5148"}, + {file = "orjson-3.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:960db0e31c4e52fa0fc3ecbaea5b2d3b58f379e32a95ae6b0ebeaa25b93dfd34"}, + {file = "orjson-3.10.6-cp312-none-win32.whl", hash = "sha256:a6ea7afb5b30b2317e0bee03c8d34c8181bc5a36f2afd4d0952f378972c4efd5"}, + {file = "orjson-3.10.6-cp312-none-win_amd64.whl", hash = "sha256:874ce88264b7e655dde4aeaacdc8fd772a7962faadfb41abe63e2a4861abc3dc"}, + {file = "orjson-3.10.6-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:66680eae4c4e7fc193d91cfc1353ad6d01b4801ae9b5314f17e11ba55e934183"}, + {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:caff75b425db5ef8e8f23af93c80f072f97b4fb3afd4af44482905c9f588da28"}, + {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3722fddb821b6036fd2a3c814f6bd9b57a89dc6337b9924ecd614ebce3271394"}, + {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2c116072a8533f2fec435fde4d134610f806bdac20188c7bd2081f3e9e0133f"}, + {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6eeb13218c8cf34c61912e9df2de2853f1d009de0e46ea09ccdf3d757896af0a"}, + {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:965a916373382674e323c957d560b953d81d7a8603fbeee26f7b8248638bd48b"}, + {file = "orjson-3.10.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:03c95484d53ed8e479cade8628c9cea00fd9d67f5554764a1110e0d5aa2de96e"}, + {file = "orjson-3.10.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:e060748a04cccf1e0a6f2358dffea9c080b849a4a68c28b1b907f272b5127e9b"}, + {file = "orjson-3.10.6-cp38-none-win32.whl", hash = "sha256:738dbe3ef909c4b019d69afc19caf6b5ed0e2f1c786b5d6215fbb7539246e4c6"}, + {file = "orjson-3.10.6-cp38-none-win_amd64.whl", hash = "sha256:d40f839dddf6a7d77114fe6b8a70218556408c71d4d6e29413bb5f150a692ff7"}, + {file = "orjson-3.10.6-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:697a35a083c4f834807a6232b3e62c8b280f7a44ad0b759fd4dce748951e70db"}, + {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd502f96bf5ea9a61cbc0b2b5900d0dd68aa0da197179042bdd2be67e51a1e4b"}, + {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f215789fb1667cdc874c1b8af6a84dc939fd802bf293a8334fce185c79cd359b"}, + {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2debd8ddce948a8c0938c8c93ade191d2f4ba4649a54302a7da905a81f00b56"}, + {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5410111d7b6681d4b0d65e0f58a13be588d01b473822483f77f513c7f93bd3b2"}, + {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb1f28a137337fdc18384079fa5726810681055b32b92253fa15ae5656e1dddb"}, + {file = "orjson-3.10.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bf2fbbce5fe7cd1aa177ea3eab2b8e6a6bc6e8592e4279ed3db2d62e57c0e1b2"}, + {file = "orjson-3.10.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:79b9b9e33bd4c517445a62b90ca0cc279b0f1f3970655c3df9e608bc3f91741a"}, + {file = "orjson-3.10.6-cp39-none-win32.whl", hash = "sha256:30b0a09a2014e621b1adf66a4f705f0809358350a757508ee80209b2d8dae219"}, + {file = "orjson-3.10.6-cp39-none-win_amd64.whl", hash = "sha256:49e3bc615652617d463069f91b867a4458114c5b104e13b7ae6872e5f79d0844"}, + {file = "orjson-3.10.6.tar.gz", hash = "sha256:e54b63d0a7c6c54a5f5f726bc93a2078111ef060fec4ecbf34c5db800ca3b3a7"}, ] [[package]] @@ -1571,80 +1565,91 @@ files = [ [[package]] name = "pillow" -version = "10.3.0" +version = "10.4.0" requires_python = ">=3.8" summary = "Python Imaging Library (Fork)" groups = ["default"] files = [ - {file = "pillow-10.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:90b9e29824800e90c84e4022dd5cc16eb2d9605ee13f05d47641eb183cd73d45"}, - {file = "pillow-10.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2c405445c79c3f5a124573a051062300936b0281fee57637e706453e452746c"}, - {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78618cdbccaa74d3f88d0ad6cb8ac3007f1a6fa5c6f19af64b55ca170bfa1edf"}, - {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:261ddb7ca91fcf71757979534fb4c128448b5b4c55cb6152d280312062f69599"}, - {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ce49c67f4ea0609933d01c0731b34b8695a7a748d6c8d186f95e7d085d2fe475"}, - {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b14f16f94cbc61215115b9b1236f9c18403c15dd3c52cf629072afa9d54c1cbf"}, - {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d33891be6df59d93df4d846640f0e46f1a807339f09e79a8040bc887bdcd7ed3"}, - {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b50811d664d392f02f7761621303eba9d1b056fb1868c8cdf4231279645c25f5"}, - {file = "pillow-10.3.0-cp310-cp310-win32.whl", hash = "sha256:ca2870d5d10d8726a27396d3ca4cf7976cec0f3cb706debe88e3a5bd4610f7d2"}, - {file = "pillow-10.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f0d0591a0aeaefdaf9a5e545e7485f89910c977087e7de2b6c388aec32011e9f"}, - {file = "pillow-10.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:ccce24b7ad89adb5a1e34a6ba96ac2530046763912806ad4c247356a8f33a67b"}, - {file = "pillow-10.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:5f77cf66e96ae734717d341c145c5949c63180842a545c47a0ce7ae52ca83795"}, - {file = "pillow-10.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4b878386c4bf293578b48fc570b84ecfe477d3b77ba39a6e87150af77f40c57"}, - {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdcbb4068117dfd9ce0138d068ac512843c52295ed996ae6dd1faf537b6dbc27"}, - {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9797a6c8fe16f25749b371c02e2ade0efb51155e767a971c61734b1bf6293994"}, - {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9e91179a242bbc99be65e139e30690e081fe6cb91a8e77faf4c409653de39451"}, - {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1b87bd9d81d179bd8ab871603bd80d8645729939f90b71e62914e816a76fc6bd"}, - {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:81d09caa7b27ef4e61cb7d8fbf1714f5aec1c6b6c5270ee53504981e6e9121ad"}, - {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:048ad577748b9fa4a99a0548c64f2cb8d672d5bf2e643a739ac8faff1164238c"}, - {file = "pillow-10.3.0-cp311-cp311-win32.whl", hash = "sha256:7161ec49ef0800947dc5570f86568a7bb36fa97dd09e9827dc02b718c5643f09"}, - {file = "pillow-10.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8eb0908e954d093b02a543dc963984d6e99ad2b5e36503d8a0aaf040505f747d"}, - {file = "pillow-10.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e6f7d1c414191c1199f8996d3f2282b9ebea0945693fb67392c75a3a320941f"}, - {file = "pillow-10.3.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:e46f38133e5a060d46bd630faa4d9fa0202377495df1f068a8299fd78c84de84"}, - {file = "pillow-10.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:50b8eae8f7334ec826d6eeffaeeb00e36b5e24aa0b9df322c247539714c6df19"}, - {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d3bea1c75f8c53ee4d505c3e67d8c158ad4df0d83170605b50b64025917f338"}, - {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19aeb96d43902f0a783946a0a87dbdad5c84c936025b8419da0a0cd7724356b1"}, - {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:74d28c17412d9caa1066f7a31df8403ec23d5268ba46cd0ad2c50fb82ae40462"}, - {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ff61bfd9253c3915e6d41c651d5f962da23eda633cf02262990094a18a55371a"}, - {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d886f5d353333b4771d21267c7ecc75b710f1a73d72d03ca06df49b09015a9ef"}, - {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b5ec25d8b17217d635f8935dbc1b9aa5907962fae29dff220f2659487891cd3"}, - {file = "pillow-10.3.0-cp312-cp312-win32.whl", hash = "sha256:51243f1ed5161b9945011a7360e997729776f6e5d7005ba0c6879267d4c5139d"}, - {file = "pillow-10.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:412444afb8c4c7a6cc11a47dade32982439925537e483be7c0ae0cf96c4f6a0b"}, - {file = "pillow-10.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:798232c92e7665fe82ac085f9d8e8ca98826f8e27859d9a96b41d519ecd2e49a"}, - {file = "pillow-10.3.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4eaa22f0d22b1a7e93ff0a596d57fdede2e550aecffb5a1ef1106aaece48e96b"}, - {file = "pillow-10.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cd5e14fbf22a87321b24c88669aad3a51ec052eb145315b3da3b7e3cc105b9a2"}, - {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1530e8f3a4b965eb6a7785cf17a426c779333eb62c9a7d1bbcf3ffd5bf77a4aa"}, - {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d512aafa1d32efa014fa041d38868fda85028e3f930a96f85d49c7d8ddc0383"}, - {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:339894035d0ede518b16073bdc2feef4c991ee991a29774b33e515f1d308e08d"}, - {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:aa7e402ce11f0885305bfb6afb3434b3cd8f53b563ac065452d9d5654c7b86fd"}, - {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0ea2a783a2bdf2a561808fe4a7a12e9aa3799b701ba305de596bc48b8bdfce9d"}, - {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c78e1b00a87ce43bb37642c0812315b411e856a905d58d597750eb79802aaaa3"}, - {file = "pillow-10.3.0-cp38-cp38-win32.whl", hash = "sha256:72d622d262e463dfb7595202d229f5f3ab4b852289a1cd09650362db23b9eb0b"}, - {file = "pillow-10.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:2034f6759a722da3a3dbd91a81148cf884e91d1b747992ca288ab88c1de15999"}, - {file = "pillow-10.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2ed854e716a89b1afcedea551cd85f2eb2a807613752ab997b9974aaa0d56936"}, - {file = "pillow-10.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dc1a390a82755a8c26c9964d457d4c9cbec5405896cba94cf51f36ea0d855002"}, - {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4203efca580f0dd6f882ca211f923168548f7ba334c189e9eab1178ab840bf60"}, - {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3102045a10945173d38336f6e71a8dc71bcaeed55c3123ad4af82c52807b9375"}, - {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:6fb1b30043271ec92dc65f6d9f0b7a830c210b8a96423074b15c7bc999975f57"}, - {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:1dfc94946bc60ea375cc39cff0b8da6c7e5f8fcdc1d946beb8da5c216156ddd8"}, - {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b09b86b27a064c9624d0a6c54da01c1beaf5b6cadfa609cf63789b1d08a797b9"}, - {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d3b2348a78bc939b4fed6552abfd2e7988e0f81443ef3911a4b8498ca084f6eb"}, - {file = "pillow-10.3.0-cp39-cp39-win32.whl", hash = "sha256:45ebc7b45406febf07fef35d856f0293a92e7417ae7933207e90bf9090b70572"}, - {file = "pillow-10.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:0ba26351b137ca4e0db0342d5d00d2e355eb29372c05afd544ebf47c0956ffeb"}, - {file = "pillow-10.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:50fd3f6b26e3441ae07b7c979309638b72abc1a25da31a81a7fbd9495713ef4f"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6b02471b72526ab8a18c39cb7967b72d194ec53c1fd0a70b050565a0f366d355"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8ab74c06ffdab957d7670c2a5a6e1a70181cd10b727cd788c4dd9005b6a8acd9"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:048eeade4c33fdf7e08da40ef402e748df113fd0b4584e32c4af74fe78baaeb2"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2ec1e921fd07c7cda7962bad283acc2f2a9ccc1b971ee4b216b75fad6f0463"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c8e73e99da7db1b4cad7f8d682cf6abad7844da39834c288fbfa394a47bbced"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:16563993329b79513f59142a6b02055e10514c1a8e86dca8b48a893e33cf91e3"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dd78700f5788ae180b5ee8902c6aea5a5726bac7c364b202b4b3e3ba2d293170"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:aff76a55a8aa8364d25400a210a65ff59d0168e0b4285ba6bf2bd83cf675ba32"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b7bc2176354defba3edc2b9a777744462da2f8e921fbaf61e52acb95bafa9828"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:793b4e24db2e8742ca6423d3fde8396db336698c55cd34b660663ee9e45ed37f"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d93480005693d247f8346bc8ee28c72a2191bdf1f6b5db469c096c0c867ac015"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83341b89884e2b2e55886e8fbbf37c3fa5efd6c8907124aeb72f285ae5696e5"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1a1d1915db1a4fdb2754b9de292642a39a7fb28f1736699527bb649484fb966a"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a0eaa93d054751ee9964afa21c06247779b90440ca41d184aeb5d410f20ff591"}, - {file = "pillow-10.3.0.tar.gz", hash = "sha256:9d2455fbf44c914840c793e89aa82d0e1763a14253a000743719ae5946814b2d"}, + {file = "pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e"}, + {file = "pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc"}, + {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e"}, + {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46"}, + {file = "pillow-10.4.0-cp310-cp310-win32.whl", hash = "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984"}, + {file = "pillow-10.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141"}, + {file = "pillow-10.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1"}, + {file = "pillow-10.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c"}, + {file = "pillow-10.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319"}, + {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d"}, + {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696"}, + {file = "pillow-10.4.0-cp311-cp311-win32.whl", hash = "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496"}, + {file = "pillow-10.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91"}, + {file = "pillow-10.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22"}, + {file = "pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94"}, + {file = "pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a"}, + {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b"}, + {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9"}, + {file = "pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42"}, + {file = "pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a"}, + {file = "pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9"}, + {file = "pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3"}, + {file = "pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc"}, + {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a"}, + {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309"}, + {file = "pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060"}, + {file = "pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea"}, + {file = "pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d"}, + {file = "pillow-10.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8d4d5063501b6dd4024b8ac2f04962d661222d120381272deea52e3fc52d3736"}, + {file = "pillow-10.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c1ee6f42250df403c5f103cbd2768a28fe1a0ea1f0f03fe151c8741e1469c8b"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15e02e9bb4c21e39876698abf233c8c579127986f8207200bc8a8f6bb27acf2"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a8d4bade9952ea9a77d0c3e49cbd8b2890a399422258a77f357b9cc9be8d680"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:43efea75eb06b95d1631cb784aa40156177bf9dd5b4b03ff38979e048258bc6b"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:950be4d8ba92aca4b2bb0741285a46bfae3ca699ef913ec8416c1b78eadd64cd"}, + {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d7480af14364494365e89d6fddc510a13e5a2c3584cb19ef65415ca57252fb84"}, + {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:73664fe514b34c8f02452ffb73b7a92c6774e39a647087f83d67f010eb9a0cf0"}, + {file = "pillow-10.4.0-cp38-cp38-win32.whl", hash = "sha256:e88d5e6ad0d026fba7bdab8c3f225a69f063f116462c49892b0149e21b6c0a0e"}, + {file = "pillow-10.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:5161eef006d335e46895297f642341111945e2c1c899eb406882a6c61a4357ab"}, + {file = "pillow-10.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0ae24a547e8b711ccaaf99c9ae3cd975470e1a30caa80a6aaee9a2f19c05701d"}, + {file = "pillow-10.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:298478fe4f77a4408895605f3482b6cc6222c018b2ce565c2b6b9c354ac3229b"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:134ace6dc392116566980ee7436477d844520a26a4b1bd4053f6f47d096997fd"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:930044bb7679ab003b14023138b50181899da3f25de50e9dbee23b61b4de2126"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:c76e5786951e72ed3686e122d14c5d7012f16c8303a674d18cdcd6d89557fc5b"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b2724fdb354a868ddf9a880cb84d102da914e99119211ef7ecbdc613b8c96b3c"}, + {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dbc6ae66518ab3c5847659e9988c3b60dc94ffb48ef9168656e0019a93dbf8a1"}, + {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:06b2f7898047ae93fad74467ec3d28fe84f7831370e3c258afa533f81ef7f3df"}, + {file = "pillow-10.4.0-cp39-cp39-win32.whl", hash = "sha256:7970285ab628a3779aecc35823296a7869f889b8329c16ad5a71e4901a3dc4ef"}, + {file = "pillow-10.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:961a7293b2457b405967af9c77dcaa43cc1a8cd50d23c532e62d48ab6cdd56f5"}, + {file = "pillow-10.4.0-cp39-cp39-win_arm64.whl", hash = "sha256:32cda9e3d601a52baccb2856b8ea1fc213c90b340c542dcef77140dfa3278a9e"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a02364621fe369e06200d4a16558e056fe2805d3468350df3aef21e00d26214b"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1b5dea9831a90e9d0721ec417a80d4cbd7022093ac38a568db2dd78363b00908"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b885f89040bb8c4a1573566bbb2f44f5c505ef6e74cec7ab9068c900047f04b"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87dd88ded2e6d74d31e1e0a99a726a6765cda32d00ba72dc37f0651f306daaa8"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:2db98790afc70118bd0255c2eeb465e9767ecf1f3c25f9a1abb8ffc8cfd1fe0a"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f7baece4ce06bade126fb84b8af1c33439a76d8a6fd818970215e0560ca28c27"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3"}, + {file = "pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06"}, ] [[package]] @@ -2021,104 +2026,104 @@ files = [ [[package]] name = "rapidfuzz" -version = "3.9.3" +version = "3.9.4" requires_python = ">=3.8" summary = "rapid fuzzy string matching" groups = ["default"] files = [ - {file = "rapidfuzz-3.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bdb8c5b8e29238ec80727c2ba3b301efd45aa30c6a7001123a6647b8e6f77ea4"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b3bd0d9632088c63a241f217742b1cf86e2e8ae573e01354775bd5016d12138c"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:153f23c03d4917f6a1fc2fb56d279cc6537d1929237ff08ee7429d0e40464a18"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a96c5225e840f1587f1bac8fa6f67562b38e095341576e82b728a82021f26d62"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b777cd910ceecd738adc58593d6ed42e73f60ad04ecdb4a841ae410b51c92e0e"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53e06e4b81f552da04940aa41fc556ba39dee5513d1861144300c36c33265b76"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c7ca5b6050f18fdcacdada2dc5fb7619ff998cd9aba82aed2414eee74ebe6cd"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:87bb8d84cb41446a808c4b5f746e29d8a53499381ed72f6c4e456fe0f81c80a8"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:959a15186d18425d19811bea86a8ffbe19fd48644004d29008e636631420a9b7"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a24603dd05fb4e3c09d636b881ce347e5f55f925a6b1b4115527308a323b9f8e"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d055da0e801c71dd74ba81d72d41b2fa32afa182b9fea6b4b199d2ce937450d"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:875b581afb29a7213cf9d98cb0f98df862f1020bce9d9b2e6199b60e78a41d14"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-win32.whl", hash = "sha256:6073a46f61479a89802e3f04655267caa6c14eb8ac9d81a635a13805f735ebc1"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:119c010e20e561249b99ca2627f769fdc8305b07193f63dbc07bca0a6c27e892"}, - {file = "rapidfuzz-3.9.3-cp310-cp310-win_arm64.whl", hash = "sha256:790b0b244f3213581d42baa2fed8875f9ee2b2f9b91f94f100ec80d15b140ba9"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f57e8305c281e8c8bc720515540e0580355100c0a7a541105c6cafc5de71daae"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a4fc7b784cf987dbddc300cef70e09a92ed1bce136f7bb723ea79d7e297fe76d"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b422c0a6fe139d5447a0766268e68e6a2a8c2611519f894b1f31f0a392b9167"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f50fed4a9b0c9825ff37cf0bccafd51ff5792090618f7846a7650f21f85579c9"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b80eb7cbe62348c61d3e67e17057cddfd6defab168863028146e07d5a8b24a89"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f45be77ec82da32ce5709a362e236ccf801615cc7163b136d1778cf9e31b14"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd84b7f652a5610733400307dc732f57c4a907080bef9520412e6d9b55bc9adc"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3e6d27dad8c990218b8cd4a5c99cbc8834f82bb46ab965a7265d5aa69fc7ced7"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:05ee0696ebf0dfe8f7c17f364d70617616afc7dafe366532730ca34056065b8a"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2bc8391749e5022cd9e514ede5316f86e332ffd3cfceeabdc0b17b7e45198a8c"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:93981895602cf5944d89d317ae3b1b4cc684d175a8ae2a80ce5b65615e72ddd0"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:754b719a4990735f66653c9e9261dcf52fd4d925597e43d6b9069afcae700d21"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-win32.whl", hash = "sha256:14c9f268ade4c88cf77ab007ad0fdf63699af071ee69378de89fff7aa3cae134"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:bc1991b4cde6c9d3c0bbcb83d5581dc7621bec8c666c095c65b4277233265a82"}, - {file = "rapidfuzz-3.9.3-cp311-cp311-win_arm64.whl", hash = "sha256:0c34139df09a61b1b557ab65782ada971b4a3bce7081d1b2bee45b0a52231adb"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5d6a210347d6e71234af5c76d55eeb0348b026c9bb98fe7c1cca89bac50fb734"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b300708c917ce52f6075bdc6e05b07c51a085733650f14b732c087dc26e0aaad"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83ea7ca577d76778250421de61fb55a719e45b841deb769351fc2b1740763050"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8319838fb5b7b5f088d12187d91d152b9386ce3979ed7660daa0ed1bff953791"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:505d99131afd21529293a9a7b91dfc661b7e889680b95534756134dc1cc2cd86"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c52970f7784518d7c82b07a62a26e345d2de8c2bd8ed4774e13342e4b3ff4200"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:143caf7247449055ecc3c1e874b69e42f403dfc049fc2f3d5f70e1daf21c1318"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b8ab0fa653d9225195a8ff924f992f4249c1e6fa0aea563f685e71b81b9fcccf"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:57e7c5bf7b61c7320cfa5dde1e60e678d954ede9bb7da8e763959b2138391401"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:51fa1ba84653ab480a2e2044e2277bd7f0123d6693051729755addc0d015c44f"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:17ff7f7eecdb169f9236e3b872c96dbbaf116f7787f4d490abd34b0116e3e9c8"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:afe7c72d3f917b066257f7ff48562e5d462d865a25fbcabf40fca303a9fa8d35"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-win32.whl", hash = "sha256:e53ed2e9b32674ce96eed80b3b572db9fd87aae6742941fb8e4705e541d861ce"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:35b7286f177e4d8ba1e48b03612f928a3c4bdac78e5651379cec59f95d8651e6"}, - {file = "rapidfuzz-3.9.3-cp312-cp312-win_arm64.whl", hash = "sha256:e6e4b9380ed4758d0cb578b0d1970c3f32dd9e87119378729a5340cb3169f879"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a39890013f6d5b056cc4bfdedc093e322462ece1027a57ef0c636537bdde7531"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b5bc0fdbf419493163c5c9cb147c5fbe95b8e25844a74a8807dcb1a125e630cf"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efe6e200a75a792d37b960457904c4fce7c928a96ae9e5d21d2bd382fe39066e"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de077c468c225d4c18f7188c47d955a16d65f21aab121cbdd98e3e2011002c37"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f917eaadf5388466a95f6a236f678a1588d231e52eda85374077101842e794e"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:858ba57c05afd720db8088a8707079e8d024afe4644001fe0dbd26ef7ca74a65"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d36447d21b05f90282a6f98c5a33771805f9222e5d0441d03eb8824e33e5bbb4"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:acbe4b6f1ccd5b90c29d428e849aa4242e51bb6cab0448d5f3c022eb9a25f7b1"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:53c7f27cdf899e94712972237bda48cfd427646aa6f5d939bf45d084780e4c16"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:6175682a829c6dea4d35ed707f1dadc16513270ef64436568d03b81ccb6bdb74"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:5276df395bd8497397197fca2b5c85f052d2e6a66ffc3eb0544dd9664d661f95"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:77b5c4f3e72924d7845f0e189c304270066d0f49635cf8a3938e122c437e58de"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-win32.whl", hash = "sha256:8add34061e5cd561c72ed4febb5c15969e7b25bda2bb5102d02afc3abc1f52d0"}, - {file = "rapidfuzz-3.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:604e0502a39cf8e67fa9ad239394dddad4cdef6d7008fdb037553817d420e108"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:21047f55d674614eb4b0ab34e35c3dc66f36403b9fbfae645199c4a19d4ed447"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a56da3aff97cb56fe85d9ca957d1f55dbac7c27da927a86a2a86d8a7e17f80aa"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:964c08481aec2fe574f0062e342924db2c6b321391aeb73d68853ed42420fd6d"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e2b827258beefbe5d3f958243caa5a44cf46187eff0c20e0b2ab62d1550327a"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c6e65a301fcd19fbfbee3a514cc0014ff3f3b254b9fd65886e8a9d6957fb7bca"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbe93ba1725a8d47d2b9dca6c1f435174859427fbc054d83de52aea5adc65729"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aca21c0a34adee582775da997a600283e012a608a107398d80a42f9a57ad323d"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:256e07d3465173b2a91c35715a2277b1ee3ae0b9bbab4e519df6af78570741d0"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:802ca2cc8aa6b8b34c6fdafb9e32540c1ba05fca7ad60b3bbd7ec89ed1797a87"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:dd789100fc852cffac1449f82af0da139d36d84fd9faa4f79fc4140a88778343"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:5d0abbacdb06e27ff803d7ae0bd0624020096802758068ebdcab9bd49cf53115"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:378d1744828e27490a823fc6fe6ebfb98c15228d54826bf4e49e4b76eb5f5579"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-win32.whl", hash = "sha256:5d0cb272d43e6d3c0dedefdcd9d00007471f77b52d2787a4695e9dd319bb39d2"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:15e4158ac4b3fb58108072ec35b8a69165f651ba1c8f43559a36d518dbf9fb3f"}, - {file = "rapidfuzz-3.9.3-cp39-cp39-win_arm64.whl", hash = "sha256:58c6a4936190c558d5626b79fc9e16497e5df7098589a7e80d8bff68148ff096"}, - {file = "rapidfuzz-3.9.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5410dc848c947a603792f4f51b904a3331cf1dc60621586bfbe7a6de72da1091"}, - {file = "rapidfuzz-3.9.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:282d55700a1a3d3a7980746eb2fcd48c9bbc1572ebe0840d0340d548a54d01fe"}, - {file = "rapidfuzz-3.9.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc1037507810833646481f5729901a154523f98cbebb1157ba3a821012e16402"}, - {file = "rapidfuzz-3.9.3-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e33f779391caedcba2ba3089fb6e8e557feab540e9149a5c3f7fea7a3a7df37"}, - {file = "rapidfuzz-3.9.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41a81a9f311dc83d22661f9b1a1de983b201322df0c4554042ffffd0f2040c37"}, - {file = "rapidfuzz-3.9.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a93250bd8fae996350c251e1752f2c03335bb8a0a5b0c7e910a593849121a435"}, - {file = "rapidfuzz-3.9.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3617d1aa7716c57d120b6adc8f7c989f2d65bc2b0cbd5f9288f1fc7bf469da11"}, - {file = "rapidfuzz-3.9.3-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:ad04a3f5384b82933213bba2459f6424decc2823df40098920856bdee5fd6e88"}, - {file = "rapidfuzz-3.9.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8709918da8a88ad73c9d4dd0ecf24179a4f0ceba0bee21efc6ea21a8b5290349"}, - {file = "rapidfuzz-3.9.3-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b770f85eab24034e6ef7df04b2bfd9a45048e24f8a808e903441aa5abde8ecdd"}, - {file = "rapidfuzz-3.9.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:930b4e6fdb4d914390141a2b99a6f77a52beacf1d06aa4e170cba3a98e24c1bc"}, - {file = "rapidfuzz-3.9.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:c8444e921bfc3757c475c4f4d7416a7aa69b2d992d5114fe55af21411187ab0d"}, - {file = "rapidfuzz-3.9.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2c1d3ef3878f871abe6826e386c3d61b5292ef5f7946fe646f4206b85836b5da"}, - {file = "rapidfuzz-3.9.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:d861bf326ee7dabc35c532a40384541578cd1ec1e1b7db9f9ecbba56eb76ca22"}, - {file = "rapidfuzz-3.9.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cde6b9d9ba5007077ee321ec722fa714ebc0cbd9a32ccf0f4dd3cc3f20952d71"}, - {file = "rapidfuzz-3.9.3-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bb6546e7b6bed1aefbe24f68a5fb9b891cc5aef61bca6c1a7b1054b7f0359bb"}, - {file = "rapidfuzz-3.9.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d8a57261ef7996d5ced7c8cba9189ada3fbeffd1815f70f635e4558d93766cb"}, - {file = "rapidfuzz-3.9.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:67201c02efc596923ad950519e0b75ceb78d524177ea557134d6567b9ac2c283"}, - {file = "rapidfuzz-3.9.3.tar.gz", hash = "sha256:b398ea66e8ed50451bce5997c430197d5e4b06ac4aa74602717f792d8d8d06e2"}, + {file = "rapidfuzz-3.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c9b9793c19bdf38656c8eaefbcf4549d798572dadd70581379e666035c9df781"}, + {file = "rapidfuzz-3.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:015b5080b999404fe06ec2cb4f40b0be62f0710c926ab41e82dfbc28e80675b4"}, + {file = "rapidfuzz-3.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acc5ceca9c1e1663f3e6c23fb89a311f69b7615a40ddd7645e3435bf3082688a"}, + {file = "rapidfuzz-3.9.4-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1424e238bc3f20e1759db1e0afb48a988a9ece183724bef91ea2a291c0b92a95"}, + {file = "rapidfuzz-3.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed01378f605aa1f449bee82cd9c83772883120d6483e90aa6c5a4ce95dc5c3aa"}, + {file = "rapidfuzz-3.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb26d412271e5a76cdee1c2d6bf9881310665d3fe43b882d0ed24edfcb891a84"}, + {file = "rapidfuzz-3.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f37e9e1f17be193c41a31c864ad4cd3ebd2b40780db11cd5c04abf2bcf4201b"}, + {file = "rapidfuzz-3.9.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d070ec5cf96b927c4dc5133c598c7ff6db3b833b363b2919b13417f1002560bc"}, + {file = "rapidfuzz-3.9.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:10e61bb7bc807968cef09a0e32ce253711a2d450a4dce7841d21d45330ffdb24"}, + {file = "rapidfuzz-3.9.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:31a2fc60bb2c7face4140010a7aeeafed18b4f9cdfa495cc644a68a8c60d1ff7"}, + {file = "rapidfuzz-3.9.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:fbebf1791a71a2e89f5c12b78abddc018354d5859e305ec3372fdae14f80a826"}, + {file = "rapidfuzz-3.9.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:aee9fc9e3bb488d040afc590c0a7904597bf4ccd50d1491c3f4a5e7e67e6cd2c"}, + {file = "rapidfuzz-3.9.4-cp310-cp310-win32.whl", hash = "sha256:005a02688a51c7d2451a2d41c79d737aa326ff54167211b78a383fc2aace2c2c"}, + {file = "rapidfuzz-3.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:3a2e75e41ee3274754d3b2163cc6c82cd95b892a85ab031f57112e09da36455f"}, + {file = "rapidfuzz-3.9.4-cp310-cp310-win_arm64.whl", hash = "sha256:2c99d355f37f2b289e978e761f2f8efeedc2b14f4751d9ff7ee344a9a5ca98d9"}, + {file = "rapidfuzz-3.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:07141aa6099e39d48637ce72a25b893fc1e433c50b3e837c75d8edf99e0c63e1"}, + {file = "rapidfuzz-3.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db1664eaff5d7d0f2542dd9c25d272478deaf2c8412e4ad93770e2e2d828e175"}, + {file = "rapidfuzz-3.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc01a223f6605737bec3202e94dcb1a449b6c76d46082cfc4aa980f2a60fd40e"}, + {file = "rapidfuzz-3.9.4-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1869c42e73e2a8910b479be204fa736418741b63ea2325f9cc583c30f2ded41a"}, + {file = "rapidfuzz-3.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:62ea7007941fb2795fff305ac858f3521ec694c829d5126e8f52a3e92ae75526"}, + {file = "rapidfuzz-3.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:698e992436bf7f0afc750690c301215a36ff952a6dcd62882ec13b9a1ebf7a39"}, + {file = "rapidfuzz-3.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b76f611935f15a209d3730c360c56b6df8911a9e81e6a38022efbfb96e433bab"}, + {file = "rapidfuzz-3.9.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129627d730db2e11f76169344a032f4e3883d34f20829419916df31d6d1338b1"}, + {file = "rapidfuzz-3.9.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:90a82143c14e9a14b723a118c9ef8d1bbc0c5a16b1ac622a1e6c916caff44dd8"}, + {file = "rapidfuzz-3.9.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ded58612fe3b0e0d06e935eaeaf5a9fd27da8ba9ed3e2596307f40351923bf72"}, + {file = "rapidfuzz-3.9.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f16f5d1c4f02fab18366f2d703391fcdbd87c944ea10736ca1dc3d70d8bd2d8b"}, + {file = "rapidfuzz-3.9.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:26aa7eece23e0df55fb75fbc2a8fb678322e07c77d1fd0e9540496e6e2b5f03e"}, + {file = "rapidfuzz-3.9.4-cp311-cp311-win32.whl", hash = "sha256:f187a9c3b940ce1ee324710626daf72c05599946bd6748abe9e289f1daa9a077"}, + {file = "rapidfuzz-3.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d8e9130fe5d7c9182990b366ad78fd632f744097e753e08ace573877d67c32f8"}, + {file = "rapidfuzz-3.9.4-cp311-cp311-win_arm64.whl", hash = "sha256:40419e98b10cd6a00ce26e4837a67362f658fc3cd7a71bd8bd25c99f7ee8fea5"}, + {file = "rapidfuzz-3.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b5d5072b548db1b313a07d62d88fe0b037bd2783c16607c647e01b070f6cf9e5"}, + {file = "rapidfuzz-3.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cf5bcf22e1f0fd273354462631d443ef78d677f7d2fc292de2aec72ae1473e66"}, + {file = "rapidfuzz-3.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c8fc973adde8ed52810f590410e03fb6f0b541bbaeb04c38d77e63442b2df4c"}, + {file = "rapidfuzz-3.9.4-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2464bb120f135293e9a712e342c43695d3d83168907df05f8c4ead1612310c7"}, + {file = "rapidfuzz-3.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d9d58689aca22057cf1a5851677b8a3ccc9b535ca008c7ed06dc6e1899f7844"}, + {file = "rapidfuzz-3.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:167e745f98baa0f3034c13583e6302fb69249a01239f1483d68c27abb841e0a1"}, + {file = "rapidfuzz-3.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db0bf0663b4b6da1507869722420ea9356b6195aa907228d6201303e69837af9"}, + {file = "rapidfuzz-3.9.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cd6ac61b74fdb9e23f04d5f068e6cf554f47e77228ca28aa2347a6ca8903972f"}, + {file = "rapidfuzz-3.9.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:60ff67c690acecf381759c16cb06c878328fe2361ddf77b25d0e434ea48a29da"}, + {file = "rapidfuzz-3.9.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:cb934363380c60f3a57d14af94325125cd8cded9822611a9f78220444034e36e"}, + {file = "rapidfuzz-3.9.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fe833493fb5cc5682c823ea3e2f7066b07612ee8f61ecdf03e1268f262106cdd"}, + {file = "rapidfuzz-3.9.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2797fb847d89e04040d281cb1902cbeffbc4b5131a5c53fc0db490fd76b2a547"}, + {file = "rapidfuzz-3.9.4-cp312-cp312-win32.whl", hash = "sha256:52e3d89377744dae68ed7c84ad0ddd3f5e891c82d48d26423b9e066fc835cc7c"}, + {file = "rapidfuzz-3.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:c76da20481c906e08400ee9be230f9e611d5931a33707d9df40337c2655c84b5"}, + {file = "rapidfuzz-3.9.4-cp312-cp312-win_arm64.whl", hash = "sha256:f2d2846f3980445864c7e8b8818a29707fcaff2f0261159ef6b7bd27ba139296"}, + {file = "rapidfuzz-3.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:355fc4a268ffa07bab88d9adee173783ec8d20136059e028d2a9135c623c44e6"}, + {file = "rapidfuzz-3.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4d81a78f90269190b568a8353d4ea86015289c36d7e525cd4d43176c88eff429"}, + {file = "rapidfuzz-3.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e618625ffc4660b26dc8e56225f8b966d5842fa190e70c60db6cd393e25b86e"}, + {file = "rapidfuzz-3.9.4-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b712336ad6f2bacdbc9f1452556e8942269ef71f60a9e6883ef1726b52d9228a"}, + {file = "rapidfuzz-3.9.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fc1ee19fdad05770c897e793836c002344524301501d71ef2e832847425707"}, + {file = "rapidfuzz-3.9.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1950f8597890c0c707cb7e0416c62a1cf03dcdb0384bc0b2dbda7e05efe738ec"}, + {file = "rapidfuzz-3.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a6c35f272ec9c430568dc8c1c30cb873f6bc96be2c79795e0bce6db4e0e101d"}, + {file = "rapidfuzz-3.9.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:1df0f9e9239132a231c86ae4f545ec2b55409fa44470692fcfb36b1bd00157ad"}, + {file = "rapidfuzz-3.9.4-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:d2c51955329bfccf99ae26f63d5928bf5be9fcfcd9f458f6847fd4b7e2b8986c"}, + {file = "rapidfuzz-3.9.4-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:3c522f462d9fc504f2ea8d82e44aa580e60566acc754422c829ad75c752fbf8d"}, + {file = "rapidfuzz-3.9.4-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:d8a52fc50ded60d81117d7647f262c529659fb21d23e14ebfd0b35efa4f1b83d"}, + {file = "rapidfuzz-3.9.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:04dbdfb0f0bfd3f99cf1e9e24fadc6ded2736d7933f32f1151b0f2abb38f9a25"}, + {file = "rapidfuzz-3.9.4-cp38-cp38-win32.whl", hash = "sha256:4968c8bd1df84b42f382549e6226710ad3476f976389839168db3e68fd373298"}, + {file = "rapidfuzz-3.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:3fe4545f89f8d6c27b6bbbabfe40839624873c08bd6700f63ac36970a179f8f5"}, + {file = "rapidfuzz-3.9.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9f256c8fb8f3125574c8c0c919ab0a1f75d7cba4d053dda2e762dcc36357969d"}, + {file = "rapidfuzz-3.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f5fdc09cf6e9d8eac3ce48a4615b3a3ee332ea84ac9657dbbefef913b13e632f"}, + {file = "rapidfuzz-3.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d395d46b80063d3b5d13c0af43d2c2cedf3ab48c6a0c2aeec715aa5455b0c632"}, + {file = "rapidfuzz-3.9.4-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7fa714fb96ce9e70c37e64c83b62fe8307030081a0bfae74a76fac7ba0f91715"}, + {file = "rapidfuzz-3.9.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bc1a0f29f9119be7a8d3c720f1d2068317ae532e39e4f7f948607c3a6de8396"}, + {file = "rapidfuzz-3.9.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6022674aa1747d6300f699cd7c54d7dae89bfe1f84556de699c4ac5df0838082"}, + {file = "rapidfuzz-3.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcb72e5f9762fd469701a7e12e94b924af9004954f8c739f925cb19c00862e38"}, + {file = "rapidfuzz-3.9.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ad04ae301129f0eb5b350a333accd375ce155a0c1cec85ab0ec01f770214e2e4"}, + {file = "rapidfuzz-3.9.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f46a22506f17c0433e349f2d1dc11907c393d9b3601b91d4e334fa9a439a6a4d"}, + {file = "rapidfuzz-3.9.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:01b42a8728c36011718da409aa86b84984396bf0ca3bfb6e62624f2014f6022c"}, + {file = "rapidfuzz-3.9.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:e590d5d5443cf56f83a51d3c4867bd1f6be8ef8cfcc44279522bcef3845b2a51"}, + {file = "rapidfuzz-3.9.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4c72078b5fdce34ba5753f9299ae304e282420e6455e043ad08e4488ca13a2b0"}, + {file = "rapidfuzz-3.9.4-cp39-cp39-win32.whl", hash = "sha256:f75639277304e9b75e6a7b3c07042d2264e16740a11e449645689ed28e9c2124"}, + {file = "rapidfuzz-3.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:e81e27e8c32a1e1278a4bb1ce31401bfaa8c2cc697a053b985a6f8d013df83ec"}, + {file = "rapidfuzz-3.9.4-cp39-cp39-win_arm64.whl", hash = "sha256:15bc397ee9a3ed1210b629b9f5f1da809244adc51ce620c504138c6e7095b7bd"}, + {file = "rapidfuzz-3.9.4-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:20488ade4e1ddba3cfad04f400da7a9c1b91eff5b7bd3d1c50b385d78b587f4f"}, + {file = "rapidfuzz-3.9.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:e61b03509b1a6eb31bc5582694f6df837d340535da7eba7bedb8ae42a2fcd0b9"}, + {file = "rapidfuzz-3.9.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:098d231d4e51644d421a641f4a5f2f151f856f53c252b03516e01389b2bfef99"}, + {file = "rapidfuzz-3.9.4-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:17ab8b7d10fde8dd763ad428aa961c0f30a1b44426e675186af8903b5d134fb0"}, + {file = "rapidfuzz-3.9.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e272df61bee0a056a3daf99f9b1bd82cf73ace7d668894788139c868fdf37d6f"}, + {file = "rapidfuzz-3.9.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d6481e099ff8c4edda85b8b9b5174c200540fd23c8f38120016c765a86fa01f5"}, + {file = "rapidfuzz-3.9.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ad61676e9bdae677d577fe80ec1c2cea1d150c86be647e652551dcfe505b1113"}, + {file = "rapidfuzz-3.9.4-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:af65020c0dd48d0d8ae405e7e69b9d8ae306eb9b6249ca8bf511a13f465fad85"}, + {file = "rapidfuzz-3.9.4-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d38b4e026fcd580e0bda6c0ae941e0e9a52c6bc66cdce0b8b0da61e1959f5f8"}, + {file = "rapidfuzz-3.9.4-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f74ed072c2b9dc6743fb19994319d443a4330b0e64aeba0aa9105406c7c5b9c2"}, + {file = "rapidfuzz-3.9.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aee5f6b8321f90615c184bd8a4c676e9becda69b8e4e451a90923db719d6857c"}, + {file = "rapidfuzz-3.9.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:3a555e3c841d6efa350f862204bb0a3fea0c006b8acc9b152b374fa36518a1c6"}, + {file = "rapidfuzz-3.9.4-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0772150d37bf018110351c01d032bf9ab25127b966a29830faa8ad69b7e2f651"}, + {file = "rapidfuzz-3.9.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:addcdd3c3deef1bd54075bd7aba0a6ea9f1d01764a08620074b7a7b1e5447cb9"}, + {file = "rapidfuzz-3.9.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3fe86b82b776554add8f900b6af202b74eb5efe8f25acdb8680a5c977608727f"}, + {file = "rapidfuzz-3.9.4-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0fc91ac59f4414d8542454dfd6287a154b8e6f1256718c898f695bdbb993467"}, + {file = "rapidfuzz-3.9.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a944e546a296a5fdcaabb537b01459f1b14d66f74e584cb2a91448bffadc3c1"}, + {file = "rapidfuzz-3.9.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4fb96ba96d58c668a17a06b5b5e8340fedc26188e87b0d229d38104556f30cd8"}, + {file = "rapidfuzz-3.9.4.tar.gz", hash = "sha256:366bf8947b84e37f2f4cf31aaf5f37c39f620d8c0eddb8b633e6ba0129ca4a0a"}, ] [[package]] @@ -2167,7 +2172,7 @@ files = [ [[package]] name = "sentry-sdk" -version = "2.7.0" +version = "2.7.1" requires_python = ">=3.6" summary = "Python client for Sentry (https://sentry.io)" groups = ["default"] @@ -2176,19 +2181,19 @@ dependencies = [ "urllib3>=1.26.11", ] files = [ - {file = "sentry_sdk-2.7.0-py2.py3-none-any.whl", hash = "sha256:db9594c27a4d21c1ebad09908b1f0dc808ef65c2b89c1c8e7e455143262e37c1"}, - {file = "sentry_sdk-2.7.0.tar.gz", hash = "sha256:d846a211d4a0378b289ced3c434480945f110d0ede00450ba631fc2852e7a0d4"}, + {file = "sentry_sdk-2.7.1-py2.py3-none-any.whl", hash = "sha256:ef1b3d54eb715825657cd4bb3cb42bb4dc85087bac14c56b0fd8c21abd968c9a"}, + {file = "sentry_sdk-2.7.1.tar.gz", hash = "sha256:25006c7e68b75aaa5e6b9c6a420ece22e8d7daec4b7a906ffd3a8607b67c037b"}, ] [[package]] name = "setuptools" -version = "70.1.1" +version = "70.2.0" requires_python = ">=3.8" summary = "Easily download, build, install, upgrade, and uninstall Python packages" groups = ["default"] files = [ - {file = "setuptools-70.1.1-py3-none-any.whl", hash = "sha256:a58a8fde0541dab0419750bcc521fbdf8585f6e5cb41909df3a472ef7b81ca95"}, - {file = "setuptools-70.1.1.tar.gz", hash = "sha256:937a48c7cdb7a21eb53cd7f9b59e525503aa8abaf3584c730dc5f7a5bec3a650"}, + {file = "setuptools-70.2.0-py3-none-any.whl", hash = "sha256:b8b8060bb426838fbe942479c90296ce976249451118ef566a5a0b7d8b78fb05"}, + {file = "setuptools-70.2.0.tar.gz", hash = "sha256:bd63e505105011b25c3c11f753f7e3b8465ea739efddaccef8f0efac2137bac1"}, ] [[package]] @@ -2207,7 +2212,7 @@ name = "simnet" version = "0.1.22" requires_python = "<4.0,>=3.8" git = "https://github.com/PaiGramTeam/SIMNet" -revision = "277a33321a20909541b46bf4ecf794fd47e19fb1" +revision = "05fcb568d6c1fe44a4f917c996198bfe62a00053" summary = "Modern API wrapper for Genshin Impact & Honkai: Star Rail built on asyncio and pydantic." groups = ["default"] dependencies = [ diff --git a/plugins/account/account.py b/plugins/account/account.py index 248157c..62d052d 100644 --- a/plugins/account/account.py +++ b/plugins/account/account.py @@ -1,7 +1,7 @@ from datetime import datetime from typing import Optional, TYPE_CHECKING -from simnet import GenshinClient, Region +from simnet import ZZZClient, Region from simnet.errors import ( InvalidCookies, BadRequest as SimnetBadRequest, @@ -149,15 +149,15 @@ class BindAccountPlugin(Plugin.Conversation): ) return ConversationHandler.END if region == RegionEnum.HYPERION: - client = GenshinClient(cookies=cookies.data, region=Region.CHINESE) + client = ZZZClient(cookies=cookies.data, region=Region.CHINESE) elif region == RegionEnum.HOYOLAB: - client = GenshinClient(cookies=cookies.data, region=Region.OVERSEAS, lang="zh-cn") + client = ZZZClient(cookies=cookies.data, region=Region.OVERSEAS, lang="zh-cn") else: return ConversationHandler.END try: record_card = await client.get_record_card(account_id) if record_card is None: - await message.reply_text("请在设置展示主界面添加原神", reply_markup=ReplyKeyboardRemove()) + await message.reply_text("请在设置展示主界面添加绝区零", reply_markup=ReplyKeyboardRemove()) return ConversationHandler.END except DataNotPublic: await message.reply_text("角色未公开", reply_markup=ReplyKeyboardRemove()) diff --git a/plugins/account/cookies.py b/plugins/account/cookies.py index 1ffa708..08c3519 100644 --- a/plugins/account/cookies.py +++ b/plugins/account/cookies.py @@ -2,7 +2,7 @@ from datetime import datetime from typing import Dict, Optional from arkowrapper import ArkoWrapper -from simnet import GenshinClient, Region +from simnet import ZZZClient, Region from simnet.errors import DataNotPublic, InvalidCookies, BadRequest as SimnetBadRequest from simnet.models.lab.record import Account from telegram import ReplyKeyboardMarkup, ReplyKeyboardRemove, TelegramObject, Update @@ -226,7 +226,7 @@ class AccountCookiesPlugin(Plugin.Conversation): logger.error("用户 %s[%s] region 异常", user.full_name, user.id) await message.reply_text("数据错误", reply_markup=ReplyKeyboardRemove()) return ConversationHandler.END - async with GenshinClient(cookies=cookies.to_dict(), region=region) as client: + async with ZZZClient(cookies=cookies.to_dict(), region=region) as client: check_cookie = cookies.check() if cookies.login_ticket is not None: try: @@ -301,7 +301,7 @@ class AccountCookiesPlugin(Plugin.Conversation): logger.success("获取用户 %s[%s] account_id[%s] 成功", user.full_name, user.id, account_id) else: account_cookies_plugin_data.account_id = client.account_id - genshin_accounts = await client.get_genshin_accounts() + genshin_accounts = await client.get_zzz_accounts() except DataNotPublic: logger.info("用户 %s[%s] 账号疑似被注销", user.full_name, user.id) await message.reply_text("账号疑似被注销,请检查账号状态", reply_markup=ReplyKeyboardRemove()) @@ -342,7 +342,7 @@ class AccountCookiesPlugin(Plugin.Conversation): level = temp.level genshin_account = temp if genshin_account is None: - await message.reply_text("未找到原神账号,请确认账号信息无误。") + await message.reply_text("未找到绝区零账号,请确认账号信息无误。") return ConversationHandler.END account_cookies_plugin_data.genshin_account = genshin_account player_info = await self.players_service.get( @@ -408,12 +408,13 @@ class AccountCookiesPlugin(Plugin.Conversation): region=region, is_chosen=True, # todo 多账号 ) - await self.update_player_info(player_model, genshin_account.nickname) await self.players_service.add(player_model) + player = player_model + await self.update_player_info(player, genshin_account.nickname) async def update_player_info(self, player: Player, nickname: str): player_info = await self.player_info_service.get(player) - if player_info is None: + if player_info is None or player_info.create_time is None: player_info = PlayerInfoSQLModel( user_id=player.user_id, player_id=player.player_id, diff --git a/plugins/account/players.py b/plugins/account/players.py index 7cf5561..f1cb7ad 100644 --- a/plugins/account/players.py +++ b/plugins/account/players.py @@ -2,7 +2,7 @@ import html from http.cookies import SimpleCookie from typing import Tuple, TYPE_CHECKING -from simnet import Region, GenshinClient +from simnet import Region, ZZZClient from telegram import InlineKeyboardMarkup, InlineKeyboardButton from telegram.ext import filters from core.config import config @@ -223,7 +223,7 @@ class PlayersManagesPlugin(Plugin): if cookies.stoken is not None: try: region = Region.CHINESE if player.region.value == 1 else Region.OVERSEAS - async with GenshinClient(cookies=cookies.to_dict(), region=region) as client: + async with ZZZClient(cookies=cookies.to_dict(), region=region) as client: cookies.cookie_token = await client.get_cookie_token_by_stoken() logger.success("用户 %s[%s] 刷新 cookie_token 成功", user.full_name, user.id) cookies.ltoken = await client.get_ltoken_by_stoken() diff --git a/plugins/admin/get_chat.py b/plugins/admin/get_chat.py index 404c54b..b781071 100644 --- a/plugins/admin/get_chat.py +++ b/plugins/admin/get_chat.py @@ -71,7 +71,7 @@ class GetChat(Plugin): if player_info.region == RegionEnum.HYPERION: text += "米游社绑定:" else: - text += "原神绑定:" + text += "绝区零绑定:" cookies_info = await self.cookies_service.get(chat.id, player_info.account_id, player_info.region) if cookies_info is None: temp = "UID 绑定" diff --git a/plugins/admin/post.py b/plugins/admin/post.py index a007bde..107b2f9 100644 --- a/plugins/admin/post.py +++ b/plugins/admin/post.py @@ -68,8 +68,8 @@ class Post(Plugin.Conversation): MENU_KEYBOARD = ReplyKeyboardMarkup([["推送频道", "添加TAG"], ["编辑文字", "删除图片"], ["退出"]], True, True) def __init__(self): - self.gids = 2 - self.short_name = "ys" + self.gids = 8 + self.short_name = "zzz" self.last_post_id_list: Dict[PostTypeEnum, List[int]] = {PostTypeEnum.CN: [], PostTypeEnum.OS: []} self.ffmpeg_enable = False self.cache_dir = os.path.join(os.getcwd(), "cache") @@ -358,6 +358,8 @@ class Post(Plugin.Conversation): if too_long or len(post_text) >= MessageLimit.CAPTION_LENGTH: post_text = post_text[: MessageLimit.CAPTION_LENGTH] await message.reply_text(f"警告!图片字符描述已经超过 {MessageLimit.CAPTION_LENGTH} 个字,已经切割") + if post_info.video_urls: + await message.reply_text("检测到视频,需要单独下载,视频链接:" + "\n".join(post_info.video_urls)) try: if len(post_images) > 1: media = [self.input_media(img_info) for img_info in post_images if not img_info.is_error] diff --git a/plugins/admin/quiz.py b/plugins/admin/quiz.py deleted file mode 100644 index 38edb76..0000000 --- a/plugins/admin/quiz.py +++ /dev/null @@ -1,230 +0,0 @@ -import re -from typing import List - -from redis import DataError, ResponseError -from telegram import InlineKeyboardButton, InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove, Update -from telegram.ext import CallbackContext, ConversationHandler, filters -from telegram.helpers import escape_markdown - -from core.plugin import Plugin, conversation, handler -from core.services.quiz.models import Answer, Question -from core.services.quiz.services import QuizService -from utils.log import logger - -( - CHECK_COMMAND, - VIEW_COMMAND, - CHECK_QUESTION, - GET_NEW_QUESTION, - GET_NEW_CORRECT_ANSWER, - GET_NEW_WRONG_ANSWER, - QUESTION_EDIT, - SAVE_QUESTION, -) = range(10300, 10308) - - -class QuizCommandData: - question_id: int = -1 - new_question: str = "" - new_correct_answer: str = "" - new_wrong_answer: List[str] = [] - status: int = 0 - - -class SetQuizPlugin(Plugin.Conversation): - """派蒙的十万个为什么问题修改/添加/删除""" - - def __init__(self, quiz_service: QuizService = None): - self.quiz_service = quiz_service - self.time_out = 120 - - @conversation.entry_point - @handler.command(command="set_quiz", filters=filters.ChatType.PRIVATE, block=False, admin=True) - async def command_start(self, update: Update, context: CallbackContext) -> int: - user = update.effective_user - message = update.effective_message - logger.info("用户 %s[%s] set_quiz命令请求", user.full_name, user.id) - quiz_command_data: QuizCommandData = context.chat_data.get("quiz_command_data") - if quiz_command_data is None: - quiz_command_data = QuizCommandData() - context.chat_data["quiz_command_data"] = quiz_command_data - text = f'你好 {user.mention_markdown_v2()} {escape_markdown("!请选择你的操作!")}' - reply_keyboard = [["查看问题", "添加问题"], ["重载问题"], ["退出"]] - await message.reply_markdown_v2(text, reply_markup=ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True)) - return CHECK_COMMAND - - async def view_command(self, update: Update, _: CallbackContext) -> int: - _ = self - keyboard = [[InlineKeyboardButton(text="选择问题", switch_inline_query_current_chat="查看问题 ")]] - await update.message.reply_text("请回复你要查看的问题", reply_markup=InlineKeyboardMarkup(keyboard)) - return CHECK_COMMAND - - @conversation.state(state=CHECK_QUESTION) - @handler.message(filters=filters.TEXT & ~filters.COMMAND, block=False) - async def check_question(self, update: Update, _: CallbackContext) -> int: - reply_keyboard = [["删除问题"], ["退出"]] - await update.message.reply_text("请选择你的操作", reply_markup=ReplyKeyboardMarkup(reply_keyboard)) - return CHECK_COMMAND - - @conversation.state(state=CHECK_COMMAND) - @handler.message(filters=filters.TEXT & ~filters.COMMAND, block=False) - async def check_command(self, update: Update, context: CallbackContext) -> int: - quiz_command_data: QuizCommandData = context.chat_data.get("quiz_command_data") - if update.message.text == "退出": - await update.message.reply_text("退出任务", reply_markup=ReplyKeyboardRemove()) - return ConversationHandler.END - if update.message.text == "查看问题": - return await self.view_command(update, context) - if update.message.text == "添加问题": - return await self.add_question(update, context) - if update.message.text == "删除问题": - return await self.delete_question(update, context) - # elif update.message.text == "修改问题": - # return await self.edit_question(update, context) - if update.message.text == "重载问题": - return await self.refresh_question(update, context) - result = re.findall(r"问题ID (\d+)", update.message.text) - if len(result) == 1: - try: - question_id = int(result[0]) - except ValueError: - await update.message.reply_text("获取问题ID失败") - return ConversationHandler.END - quiz_command_data.question_id = question_id - await update.message.reply_text("获取问题ID成功") - return await self.check_question(update, context) - await update.message.reply_text("命令错误", reply_markup=ReplyKeyboardRemove()) - return ConversationHandler.END - - async def refresh_question(self, update: Update, _: CallbackContext) -> int: - try: - await self.quiz_service.refresh_quiz() - except DataError: - await update.message.reply_text("Redis数据错误,重载失败", reply_markup=ReplyKeyboardRemove()) - return ConversationHandler.END - except ResponseError as exc: - logger.error("重载问题失败", exc_info=exc) - await update.message.reply_text( - "重载问题失败,异常抛出Redis请求错误异常,详情错误请看日记", reply_markup=ReplyKeyboardRemove() - ) - return ConversationHandler.END - await update.message.reply_text("重载成功", reply_markup=ReplyKeyboardRemove()) - return ConversationHandler.END - - async def add_question(self, update: Update, context: CallbackContext) -> int: - _ = self - quiz_command_data: QuizCommandData = context.chat_data.get("quiz_command_data") - quiz_command_data.new_wrong_answer = [] - quiz_command_data.new_question = "" - quiz_command_data.new_correct_answer = "" - quiz_command_data.status = 1 - await update.message.reply_text( - "请回复你要添加的问题,或发送 /cancel 取消操作", reply_markup=ReplyKeyboardRemove() - ) - return GET_NEW_QUESTION - - @conversation.state(state=GET_NEW_QUESTION) - @handler.message(filters=filters.TEXT & ~filters.COMMAND, block=False) - async def get_new_question(self, update: Update, context: CallbackContext) -> int: - message = update.effective_message - quiz_command_data: QuizCommandData = context.chat_data.get("quiz_command_data") - reply_text = f"问题:`{escape_markdown(update.message.text, version=2)}`\n" f"请填写正确答案:" - quiz_command_data.new_question = message.text - await update.message.reply_markdown_v2(reply_text) - return GET_NEW_CORRECT_ANSWER - - @conversation.state(state=GET_NEW_CORRECT_ANSWER) - @handler.message(filters=filters.TEXT & ~filters.COMMAND, block=False) - async def get_new_correct_answer(self, update: Update, context: CallbackContext) -> int: - quiz_command_data: QuizCommandData = context.chat_data.get("quiz_command_data") - reply_text = f"正确答案:`{escape_markdown(update.message.text, version=2)}`\n" f"请填写错误答案:" - await update.message.reply_markdown_v2(reply_text) - quiz_command_data.new_correct_answer = update.message.text - return GET_NEW_WRONG_ANSWER - - @conversation.state(state=GET_NEW_WRONG_ANSWER) - @handler.message(filters=filters.TEXT & ~filters.COMMAND, block=False) - @handler.command(command="finish_edit", block=False) - async def get_new_wrong_answer(self, update: Update, context: CallbackContext) -> int: - quiz_command_data: QuizCommandData = context.chat_data.get("quiz_command_data") - reply_text = ( - f"错误答案:`{escape_markdown(update.message.text, version=2)}`\n" - f"可继续填写,并使用 {escape_markdown('/finish', version=2)} 结束。" - ) - await update.message.reply_markdown_v2(reply_text) - quiz_command_data.new_wrong_answer.append(update.message.text) - return GET_NEW_WRONG_ANSWER - - async def finish_edit(self, update: Update, context: CallbackContext): - _ = self - quiz_command_data: QuizCommandData = context.chat_data.get("quiz_command_data") - reply_text = ( - f"问题:`{escape_markdown(quiz_command_data.new_question, version=2)}`\n" - f"正确答案:`{escape_markdown(quiz_command_data.new_correct_answer, version=2)}`\n" - f"错误答案:`{escape_markdown(' '.join(quiz_command_data.new_wrong_answer), version=2)}`" - ) - await update.message.reply_markdown_v2(reply_text) - reply_keyboard = [["保存并重载配置", "抛弃修改并退出"]] - await update.message.reply_text( - "请核对问题,并选择下一步操作。", reply_markup=ReplyKeyboardMarkup(reply_keyboard) - ) - return SAVE_QUESTION - - @conversation.state(state=SAVE_QUESTION) - @handler.message(filters=filters.TEXT & ~filters.COMMAND, block=False) - async def save_question(self, update: Update, context: CallbackContext): - quiz_command_data: QuizCommandData = context.chat_data.get("quiz_command_data") - if update.message.text == "抛弃修改并退出": - await update.message.reply_text("退出任务", reply_markup=ReplyKeyboardRemove()) - return ConversationHandler.END - if update.message.text == "保存并重载配置": - if quiz_command_data.status == 1: - answer = [ - Answer(text=wrong_answer, is_correct=False) for wrong_answer in quiz_command_data.new_wrong_answer - ] - answer.append(Answer(text=quiz_command_data.new_correct_answer, is_correct=True)) - await self.quiz_service.save_quiz(Question(text=quiz_command_data.new_question)) - await update.message.reply_text("保存成功", reply_markup=ReplyKeyboardRemove()) - try: - await self.quiz_service.refresh_quiz() - except ResponseError as exc: - logger.error("重载问题失败", exc_info=exc) - await update.message.reply_text( - "重载问题失败,异常抛出Redis请求错误异常,详情错误请看日记", reply_markup=ReplyKeyboardRemove() - ) - return ConversationHandler.END - await update.message.reply_text("重载配置成功", reply_markup=ReplyKeyboardRemove()) - return ConversationHandler.END - await update.message.reply_text("回复错误,请重新选择") - return SAVE_QUESTION - - async def edit_question(self, update: Update, context: CallbackContext) -> int: - _ = self - quiz_command_data: QuizCommandData = context.chat_data.get("quiz_command_data") - quiz_command_data.new_wrong_answer = [] - quiz_command_data.new_question = "" - quiz_command_data.new_correct_answer = "" - quiz_command_data.status = 2 - await update.message.reply_text("请回复你要修改的问题", reply_markup=ReplyKeyboardRemove()) - return GET_NEW_QUESTION - - async def delete_question(self, update: Update, context: CallbackContext) -> int: - quiz_command_data: QuizCommandData = context.chat_data.get("quiz_command_data") - # 再问题重载Redis 以免redis数据为空时出现奔溃 - try: - await self.quiz_service.refresh_quiz() - question = await self.quiz_service.get_question(quiz_command_data.question_id) - # 因为外键的存在,先删除答案 - for answer in question.answers: - await self.quiz_service.delete_question_by_id(answer.answer_id) - await self.quiz_service.delete_question_by_id(question.question_id) - await update.message.reply_text("删除问题成功", reply_markup=ReplyKeyboardRemove()) - await self.quiz_service.refresh_quiz() - except ResponseError as exc: - logger.error("重载问题失败", exc_info=exc) - await update.message.reply_text( - "重载问题失败,异常抛出Redis请求错误异常,详情错误请看日记", reply_markup=ReplyKeyboardRemove() - ) - return ConversationHandler.END - await update.message.reply_text("重载配置成功", reply_markup=ReplyKeyboardRemove()) - return ConversationHandler.END diff --git a/plugins/admin/refresh_metadata.py b/plugins/admin/refresh_metadata.py deleted file mode 100644 index 557d28a..0000000 --- a/plugins/admin/refresh_metadata.py +++ /dev/null @@ -1,28 +0,0 @@ -from telegram import Update -from telegram.ext import CallbackContext - -from core.plugin import Plugin, handler -from metadata.scripts.honey import update_honey_metadata -from metadata.scripts.metadatas import update_metadata_from_ambr, update_metadata_from_github -from metadata.scripts.paimon_moe import update_paimon_moe_zh -from utils.log import logger - -__all__ = ("MetadataPlugin",) - - -class MetadataPlugin(Plugin): - @handler.command("refresh_metadata", admin=True, block=False) - async def refresh(self, update: Update, _: CallbackContext) -> None: - message = update.effective_message - user = update.effective_user - logger.info("用户 %s[%s] 刷新[bold]metadata[/]缓存命令", user.full_name, user.id, extra={"markup": True}) - - msg = await message.reply_text("正在刷新元数据,请耐心等待...") - logger.info("正在从 github 上获取元数据") - await update_metadata_from_github() - await update_paimon_moe_zh() - logger.info("正在从 ambr 上获取元数据") - await update_metadata_from_ambr() - logger.info("正在从 honey 上获取元数据") - await update_honey_metadata() - await msg.edit_text("正在刷新元数据,请耐心等待...\n完成!") diff --git a/plugins/admin/set_command.py b/plugins/admin/set_command.py index 5ea1c5e..bc80101 100644 --- a/plugins/admin/set_command.py +++ b/plugins/admin/set_command.py @@ -21,67 +21,31 @@ class SetCommandPlugin(Plugin): user_command = [ BotCommand("cancel", "取消操作(解决一切玄学问题)"), BotCommand("help_raw", "查看文本帮助"), - # gacha_log 相关 - BotCommand("wish_log", "查看抽卡记录"), - BotCommand("wish_log_import", "导入抽卡记录"), - BotCommand("wish_log_export", "导出抽卡记录"), - BotCommand("wish_log_delete", "删除抽卡记录"), - BotCommand("pay_log", "查看充值记录"), - BotCommand("pay_log_import", "导入充值记录"), - BotCommand("pay_log_export", "导出充值记录"), - BotCommand("pay_log_delete", "删除充值记录"), # Cookie 查询类 - BotCommand("sign", "米游社原神每日签到"), + BotCommand("sign", "米游社绝区零每日签到"), BotCommand("dailynote_tasks", "自动便笺提醒"), # 其他 - BotCommand("hilichurls", "丘丘语字典"), - BotCommand("birthday", "查询角色生日"), BotCommand("setuid", "添加/重设UID"), BotCommand("setcookie", "添加/重设Cookie"), - BotCommand("setdevice", "添加/重设设备"), BotCommand("player", "管理用户绑定玩家"), BotCommand("verify", "手动验证"), - BotCommand("redeem", "(国际服)兑换 Key"), ] group_command = [ # 通用 BotCommand("help", "帮助"), - BotCommand("quiz", f"{config.notice.bot_name}的十万个为什么"), - BotCommand("wish", " 非洲人模拟器(抽卡模拟器)"), - BotCommand("set_wish", "抽卡模拟器定轨"), - BotCommand("calendar", "活动日历"), - # Wiki 类 - BotCommand("weapon", "查询武器"), - BotCommand("strategy", "查询角色攻略"), - BotCommand("material", "角色培养素材查询"), # UID 查询类 BotCommand("stats", "玩家统计查询"), - BotCommand("player_card", "查询角色卡片"), - BotCommand("avatar_board", "角色排名"), - BotCommand("gcsim", "组队伤害计算"), # Cookie 查询类 BotCommand("dailynote", "查询实时便笺"), - BotCommand("ledger", "查询当月旅行札记"), - BotCommand("ledger_history", "查询旅行札记历史记录"), - BotCommand("abyss", "查询深渊战绩"), - BotCommand("abyss_team", "查询深渊推荐配队"), - BotCommand("abyss_history", "查询深渊历史战绩"), - BotCommand("avatars", "查询角色练度"), - BotCommand("reg_time", "账号注册时间"), - BotCommand("daily_material", "今日素材表"), BotCommand("cookies_import", "从其他 BOT 导入账号信息"), BotCommand("cookies_export", "导出账号信息给其他 BOT"), ] admin_command = [ BotCommand("add_admin", "添加管理员"), BotCommand("del_admin", "删除管理员"), - BotCommand("refresh_metadata", "刷新元数据"), - BotCommand("refresh_wiki", "刷新Wiki缓存"), - BotCommand("refresh_map", "刷新地图数据"), BotCommand("save_entry", "保存条目数据"), BotCommand("remove_all_entry", "删除全部条目数据"), BotCommand("sign_all", "全部账号重新签到"), - BotCommand("refresh_all_history", "全部账号刷新历史记录"), BotCommand("send_log", "发送日志"), BotCommand("update", "更新"), BotCommand("set_command", "重设命令"), diff --git a/plugins/admin/wiki.py b/plugins/admin/wiki.py deleted file mode 100644 index 2480ad4..0000000 --- a/plugins/admin/wiki.py +++ /dev/null @@ -1,19 +0,0 @@ -from telegram import Update -from telegram.ext import CallbackContext - -from core.plugin import Plugin, handler -from core.services.wiki.services import WikiService - - -class WikiPlugin(Plugin): - """有关WIKI操作""" - - def __init__(self, wiki_service: WikiService): - self.wiki_service = wiki_service - - @handler.command("refresh_wiki", block=False, admin=True) - async def refresh_wiki(self, update: Update, _: CallbackContext): - message = update.effective_message - await message.reply_text("正在刷新Wiki缓存,请稍等") - await self.wiki_service.refresh_wiki() - await message.reply_text("刷新Wiki缓存成功") diff --git a/plugins/app/inline.py b/plugins/app/inline.py index 5251a6f..1c7c1f6 100644 --- a/plugins/app/inline.py +++ b/plugins/app/inline.py @@ -1,4 +1,3 @@ -import asyncio from typing import Awaitable, Dict, List, cast, Tuple from uuid import uuid4 @@ -17,12 +16,10 @@ from telegram.constants import ParseMode from telegram.error import BadRequest from telegram.ext import CallbackContext, ContextTypes -from core.dependence.assets import AssetsCouldNotFound, AssetsService from core.plugin import Plugin, handler from core.services.cookies import CookiesService from core.services.players import PlayersService from core.services.search.services import SearchServices -from core.services.wiki.services import WikiService from gram_core.config import config from gram_core.plugin.methods.inline_use_data import IInlineUseData from utils.log import logger @@ -33,14 +30,10 @@ class Inline(Plugin): def __init__( self, - wiki_service: WikiService, - assets_service: AssetsService, search_service: SearchServices, cookies_service: CookiesService, players_service: PlayersService, ): - self.assets_service = assets_service - self.wiki_service = wiki_service self.weapons_list: List[Dict[str, str]] = [] self.characters_list: List[Dict[str, str]] = [] self.refresh_task: List[Awaitable] = [] @@ -51,41 +44,6 @@ class Inline(Plugin): self.inline_use_data_map: Dict[str, IInlineUseData] = {} self.img_url = "https://i.dawnlab.me/b1bdf9cc3061d254f038e557557694bc.jpg" - async def initialize(self): - # todo: 整合进 wiki 或者单独模块 从Redis中读取 - async def task_weapons(): - logger.info("Inline 模块正在获取武器列表") - weapons_list = await self.wiki_service.get_weapons_name_list() - for weapons_name in weapons_list: - try: - icon = await self.assets_service.weapon(weapons_name).get_link("icon") - except AssetsCouldNotFound: - continue - except Exception as exc: - logger.error("获取武器信息失败 %s", str(exc)) - continue - data = {"name": weapons_name, "icon": icon} - self.weapons_list.append(data) - logger.success("Inline 模块获取武器列表成功") - - async def task_characters(): - logger.info("Inline 模块正在获取角色列表") - characters_list = await self.wiki_service.get_characters_name_list() - for character_name in characters_list: - try: - icon = await self.assets_service.avatar(character_name).get_link("icon") - except AssetsCouldNotFound: - continue - except Exception as exc: - logger.error("获取角色信息失败 %s", str(exc)) - continue - data = {"name": character_name, "icon": icon} - self.characters_list.append(data) - logger.success("Inline 模块获取角色列表成功") - - self.refresh_task.append(asyncio.create_task(task_weapons())) - self.refresh_task.append(asyncio.create_task(task_characters())) - async def init_inline_use_data(self): if self.inline_use_data: return @@ -260,19 +218,6 @@ class Inline(Plugin): ), ) ) - elif args[0] == "查看角色培养素材列表并查询": - characters_list = await self.wiki_service.get_characters_name_list() - for role_name in characters_list: - results_list.append( - InlineQueryResultArticle( - id=str(uuid4()), - title=role_name, - description=f"查看角色培养素材列表并查询 {role_name}", - input_message_content=InputTextMessageContent( - f"角色培养素材查询{role_name}", parse_mode=ParseMode.MARKDOWN_V2 - ), - ) - ) else: simple_search_results = await self.search_service.search(args[0]) if simple_search_results: diff --git a/plugins/genshin/abyss.py b/plugins/genshin/abyss.py deleted file mode 100644 index 2325eb4..0000000 --- a/plugins/genshin/abyss.py +++ /dev/null @@ -1,627 +0,0 @@ -"""深渊数据查询""" - -import asyncio -import math -import re -from datetime import datetime -from functools import lru_cache, partial -from typing import Any, Coroutine, List, Optional, Tuple, Union, Dict - -from arkowrapper import ArkoWrapper -from pytz import timezone -from simnet import GenshinClient -from simnet.models.genshin.chronicle.abyss import SpiralAbyss -from telegram import Message, Update, InlineKeyboardButton, InlineKeyboardMarkup -from telegram.constants import ChatAction, ParseMode -from telegram.ext import CallbackContext, filters, ContextTypes - -from core.dependence.assets import AssetsService -from core.plugin import Plugin, handler -from core.services.cookies.error import TooManyRequestPublicCookies -from core.services.history_data.models import HistoryDataAbyss -from core.services.history_data.services import HistoryDataAbyssServices -from core.services.template.models import RenderGroupResult, RenderResult -from core.services.template.services import TemplateService -from gram_core.config import config -from gram_core.dependence.redisdb import RedisDB -from gram_core.plugin.methods.inline_use_data import IInlineUseData -from plugins.tools.genshin import GenshinHelper -from utils.enkanetwork import RedisCache -from utils.log import logger -from utils.uid import mask_number - -try: - import ujson as jsonlib - -except ImportError: - import json as jsonlib - -TZ = timezone("Asia/Shanghai") - -get_args_pattern = re.compile(r"\d+") - - -@lru_cache -def get_args(text: str) -> Tuple[int, bool, bool]: - total = "all" in text or "总览" in text - prev = "pre" in text or "上期" in text - floor = 0 - - if not total: - m = get_args_pattern.search(text) - if m is not None: - floor = int(m.group(0)) - - return floor, total, prev - - -class AbyssUnlocked(Exception): - """根本没动""" - - -class NoMostKills(Exception): - """挑战了但是数据没刷新""" - - -class FloorNotFoundError(Exception): - """只有数据统计,层数统计未出""" - - -class AbyssNotFoundError(Exception): - """如果查询别人,是无法找到队伍详细,只有数据统计""" - - -class AbyssPlugin(Plugin): - """深渊数据查询""" - - def __init__( - self, - template: TemplateService, - helper: GenshinHelper, - assets_service: AssetsService, - history_data_abyss: HistoryDataAbyssServices, - redis: RedisDB, - ): - self.template_service = template - self.helper = helper - self.assets_service = assets_service - self.history_data_abyss = history_data_abyss - self.cache = RedisCache(redis.client, key="plugin:abyss:history") - - @handler.command("abyss", block=False) - @handler.message(filters.Regex(r"^深渊数据"), block=False) - async def command_start(self, update: Update, context: CallbackContext) -> None: # skipcq: PY-R1000 # - user_id = await self.get_real_user_id(update) - uid, offset = self.get_real_uid_or_offset(update) - args = self.get_args(context) - message = update.effective_message - - # 若查询帮助 - if (message.text.startswith("/") and "help" in message.text) or "帮助" in message.text: - await message.reply_text( - "深渊挑战数据功能使用帮助(中括号表示可选参数)\n\n" - "指令格式:\n/abyss + [层数/all] + [pre]\n(pre表示上期)\n\n" - "文本格式:\n深渊数据 + 查询/总览 + [上期] + [层数] \n\n" - "例如以下指令都正确:\n" - "/abyss\n/abyss 12 pre\n/abyss all pre\n" - "深渊数据查询\n深渊数据查询上期第12层\n深渊数据总览上期", - parse_mode=ParseMode.HTML, - ) - self.log_user(update, logger.info, "查询[bold]深渊挑战数据[/bold]帮助", extra={"markup": True}) - return - - # 解析参数 - floor, total, previous = get_args(" ".join([i for i in args if not i.startswith("@")])) - - if floor > 12 or floor < 0: - reply_msg = await message.reply_text("深渊层数输入错误,请重新输入。支持的参数为: 1-12 或 all") - if filters.ChatType.GROUPS.filter(message): - self.add_delete_message_job(reply_msg) - self.add_delete_message_job(message) - return - if 0 < floor < 9: - previous = False - - self.log_user( - update, - logger.info, - "[bold]深渊挑战数据[/bold]请求: floor=%s total=%s previous=%s", - floor, - total, - previous, - extra={"markup": True}, - ) - - await message.reply_chat_action(ChatAction.TYPING) - - reply_text: Optional[Message] = None - - if total: - reply_text = await message.reply_text(f"{config.notice.bot_name}需要时间整理深渊数据,还请耐心等待哦~") - try: - async with self.helper.genshin_or_public(user_id, uid=uid, offset=offset) as client: - if not client.public: - await client.get_record_cards() - abyss_data, avatar_data = await self.get_rendered_pic_data(client, client.player_id, previous) - images = await self.get_rendered_pic(abyss_data, avatar_data, client.player_id, floor, total) - except AbyssUnlocked: # 若深渊未解锁 - await message.reply_text("还未解锁深渊哦~") - return - except NoMostKills: # 若深渊还未挑战 - await message.reply_text("还没有挑战本次深渊呢,咕咕咕~") - return - except FloorNotFoundError: - await message.reply_text("深渊详细数据未找到,咕咕咕~") - return - except AbyssNotFoundError: - await message.reply_text("无法查询玩家挑战队伍详情,只能查询统计详情哦~") - return - except TooManyRequestPublicCookies: - reply_message = await message.reply_text("查询次数太多,请您稍后重试") - if filters.ChatType.GROUPS.filter(message): - self.add_delete_message_job(reply_message) - self.add_delete_message_job(message) - return - finally: - if reply_text is not None: - await reply_text.delete() - - if images is None: - await message.reply_text(f"还没有第 {floor} 层的挑战数据") - return - - await message.reply_chat_action(ChatAction.UPLOAD_PHOTO) - - for group in ArkoWrapper(images).group(10): # 每 10 张图片分一个组 - await RenderGroupResult(results=group).reply_media_group(message, write_timeout=60) - - self.log_user(update, logger.info, "[bold]深渊挑战数据[/bold]: 成功发送图片", extra={"markup": True}) - - async def get_rendered_pic_data( - self, client: GenshinClient, uid: int, previous: bool - ) -> Tuple["SpiralAbyss", Dict[int, int]]: - abyss_data = await client.get_genshin_spiral_abyss(uid, previous=previous, lang="zh-cn") - avatar_data = {} - if not client.public: # noqa - avatars = await client.get_genshin_characters(uid, lang="zh-cn") - avatar_data = {i.id: i.constellation for i in avatars} - if abyss_data.unlocked and abyss_data.ranks.most_kills: - await self.save_abyss_data(self.history_data_abyss, uid, abyss_data, avatar_data) - return abyss_data, avatar_data - - async def get_rendered_pic( # skipcq: PY-R1000 # - self, abyss_data: "SpiralAbyss", avatar_data: Dict[int, int], uid: int, floor: int, total: bool - ) -> Union[Tuple[Any], List[RenderResult], None]: - """ - 获取渲染后的图片 - - Args: - abyss_data (SpiralAbyss): 深渊数据 - avatar_data (Dict[int, int]): 角色数据 - uid (int): 需要查询的 uid - floor (int): 层数 - total (bool): 是否为总览 - - Returns: - bytes格式的图片 - """ - - def json_encoder(value): - if isinstance(value, datetime): - return value.astimezone(TZ).strftime("%Y-%m-%d %H:%M:%S") - return value - - if not abyss_data.unlocked: - raise AbyssUnlocked - if not abyss_data.ranks.most_kills: - raise NoMostKills - if (total or (floor > 0)) and len(abyss_data.floors) == 0: - raise FloorNotFoundError - if (total or (floor > 0)) and len(abyss_data.floors[0].chambers[0].battles) == 0: - raise AbyssNotFoundError - - start_time = abyss_data.start_time.astimezone(TZ) - time = start_time.strftime("%Y年%m月") + ("上" if start_time.day <= 15 else "下") - stars = [i.stars for i in filter(lambda x: x.floor > 8, abyss_data.floors)] - total_stars = f"{sum(stars)} ({'-'.join(map(str, stars))})" - - render_data = {} - result = abyss_data.json(encoder=json_encoder) - - render_data["time"] = time - render_data["stars"] = total_stars - render_data["uid"] = mask_number(uid) - render_data["floor_colors"] = { - 1: "#374952", - 2: "#374952", - 3: "#55464B", - 4: "#55464B", - 5: "#55464B", - 6: "#1D2A5D", - 7: "#1D2A5D", - 8: "#1D2A5D", - 9: "#292B58", - 10: "#382024", - 11: "#252550", - 12: "#1D2A4A", - } - - if total: - render_data["avatar_data"] = avatar_data - data = jsonlib.loads(result) - render_data["data"] = data - - render_inputs: List[Tuple[int, Coroutine[Any, Any, RenderResult]]] = [] - - def overview_task(): - return -1, self.template_service.render( - "genshin/abyss/overview.jinja2", render_data, viewport={"width": 750, "height": 580} - ) - - def floor_task(floor_index: int): - floor_d = data["floors"][floor_index] - return ( - floor_d["floor"], - self.template_service.render( - "genshin/abyss/floor.jinja2", - { - **render_data, - "floor": floor_d, - "total_stars": f"{floor_d['stars']}/{floor_d['max_stars']}", - }, - viewport={"width": 690, "height": 500}, - full_page=True, - ttl=15 * 24 * 60 * 60, - ), - ) - - render_inputs.append(overview_task()) - - for i, f in enumerate(data["floors"]): - if f["floor"] >= 9: - render_inputs.append(floor_task(i)) - - render_group_inputs = list(map(lambda x: x[1], sorted(render_inputs, key=lambda x: x[0]))) - - return await asyncio.gather(*render_group_inputs) - - if floor < 1: - render_data["data"] = jsonlib.loads(result) - return [ - await self.template_service.render( - "genshin/abyss/overview.jinja2", render_data, viewport={"width": 750, "height": 580} - ) - ] - num_dic = { - "0": "", - "1": "一", - "2": "二", - "3": "三", - "4": "四", - "5": "五", - "6": "六", - "7": "七", - "8": "八", - "9": "九", - } - if num := num_dic.get(str(floor)): - render_data["floor-num"] = num - else: - render_data["floor-num"] = f"十{num_dic.get(str(floor % 10))}" - floors = jsonlib.loads(result)["floors"] - if not (floor_data := list(filter(lambda x: x["floor"] == floor, floors))): - return None - render_data["avatar_data"] = avatar_data - render_data["floor"] = floor_data[0] - render_data["total_stars"] = f"{floor_data[0]['stars']}/{floor_data[0]['max_stars']}" - return [ - await self.template_service.render( - "genshin/abyss/floor.jinja2", render_data, viewport={"width": 690, "height": 500} - ) - ] - - @staticmethod - async def save_abyss_data( - history_data_abyss: "HistoryDataAbyssServices", - uid: int, - abyss_data: "SpiralAbyss", - character_data: Dict[int, int], - ) -> bool: - model = history_data_abyss.create(uid, abyss_data, character_data) - old_data = await history_data_abyss.get_by_user_id_data_id(uid, model.data_id) - exists = history_data_abyss.exists_data(model, old_data) - if not exists: - await history_data_abyss.add(model) - return True - return False - - async def get_abyss_data(self, uid: int): - return await self.history_data_abyss.get_by_user_id(uid) - - @staticmethod - def get_season_data_name(data: "HistoryDataAbyss"): - start_time = data.abyss_data.start_time.astimezone(TZ) - time = start_time.strftime("%Y.%m ")[2:] + ("上" if start_time.day <= 15 else "下") - honor = "" - if data.abyss_data.total_stars == 36: - if data.abyss_data.total_battles == 12: - honor = "👑" - last_battles = data.abyss_data.floors[-1].chambers[-1].battles - num_of_characters = max( - len(last_battles[0].characters), - len(last_battles[1].characters), - ) - if num_of_characters == 2: - honor = "双通" - elif num_of_characters == 1: - honor = "单通" - - return f"{time} {data.abyss_data.total_stars} ★ {honor}" - - async def get_session_button_data(self, user_id: int, uid: int, force: bool = False): - redis = await self.cache.get(str(uid)) - if redis and not force: - return redis["buttons"] - data = await self.get_abyss_data(uid) - data.sort(key=lambda x: x.id, reverse=True) - abyss_data = [HistoryDataAbyss.from_data(i) for i in data] - buttons = [ - { - "name": AbyssPlugin.get_season_data_name(abyss_data[idx]), - "value": f"get_abyss_history|{user_id}|{uid}|{value.id}", - } - for idx, value in enumerate(data) - ] - await self.cache.set(str(uid), {"buttons": buttons}) - return buttons - - async def gen_season_button( - self, - user_id: int, - uid: int, - page: int = 1, - ) -> List[List[InlineKeyboardButton]]: - """生成按钮""" - data = await self.get_session_button_data(user_id, uid) - if not data: - return [] - buttons = [ - InlineKeyboardButton( - value["name"], - callback_data=value["value"], - ) - for value in data - ] - all_buttons = [buttons[i : i + 3] for i in range(0, len(buttons), 3)] - send_buttons = all_buttons[(page - 1) * 5 : page * 5] - last_page = page - 1 if page > 1 else 0 - all_page = math.ceil(len(all_buttons) / 5) - next_page = page + 1 if page < all_page and all_page > 1 else 0 - last_button = [] - if last_page: - last_button.append( - InlineKeyboardButton( - "<< 上一页", - callback_data=f"get_abyss_history|{user_id}|{uid}|p_{last_page}", - ) - ) - if last_page or next_page: - last_button.append( - InlineKeyboardButton( - f"{page}/{all_page}", - callback_data=f"get_abyss_history|{user_id}|{uid}|empty_data", - ) - ) - if next_page: - last_button.append( - InlineKeyboardButton( - "下一页 >>", - callback_data=f"get_abyss_history|{user_id}|{uid}|p_{next_page}", - ) - ) - if last_button: - send_buttons.append(last_button) - return send_buttons - - @staticmethod - async def gen_floor_button( - data_id: int, - abyss_data: "HistoryDataAbyss", - user_id: int, - uid: int, - ) -> List[List[InlineKeyboardButton]]: - floors = [i.floor for i in abyss_data.abyss_data.floors if i.floor] - floors.sort() - buttons = [ - InlineKeyboardButton( - f"第 {i} 层", - callback_data=f"get_abyss_history|{user_id}|{uid}|{data_id}|{i}", - ) - for i in floors - ] - send_buttons = [buttons[i : i + 4] for i in range(0, len(buttons), 4)] - all_buttons = [ - InlineKeyboardButton( - "<< 返回", - callback_data=f"get_abyss_history|{user_id}|{uid}|p_1", - ), - InlineKeyboardButton( - "总览", - callback_data=f"get_abyss_history|{user_id}|{uid}|{data_id}|total", - ), - InlineKeyboardButton( - "所有", - callback_data=f"get_abyss_history|{user_id}|{uid}|{data_id}|all", - ), - ] - send_buttons.append(all_buttons) - return send_buttons - - @handler.command("abyss_history", block=False) - @handler.message(filters.Regex(r"^深渊历史数据"), block=False) - async def abyss_history_command_start(self, update: Update, _: CallbackContext) -> None: - user_id = await self.get_real_user_id(update) - uid, offset = self.get_real_uid_or_offset(update) - message = update.effective_message - self.log_user(update, logger.info, "查询深渊历史数据") - - async with self.helper.genshin_or_public(user_id, uid=uid, offset=offset) as client: - await self.get_session_button_data(user_id, client.player_id, force=True) - buttons = await self.gen_season_button(user_id, client.player_id) - if not buttons: - await message.reply_text("还没有深渊历史数据哦~") - return - await message.reply_text("请选择要查询的深渊历史数据", reply_markup=InlineKeyboardMarkup(buttons)) - - async def get_abyss_history_page(self, update: "Update", user_id: int, uid: int, result: str): - """翻页处理""" - callback_query = update.callback_query - - self.log_user(update, logger.info, "切换深渊历史数据页 page[%s]", result) - page = int(result.split("_")[1]) - async with self.helper.genshin_or_public(user_id, uid=uid) as client: - buttons = await self.gen_season_button(user_id, client.player_id, page) - if not buttons: - await callback_query.answer("还没有深渊历史数据哦~", show_alert=True) - await callback_query.edit_message_text("还没有深渊历史数据哦~") - return - await callback_query.edit_message_reply_markup(reply_markup=InlineKeyboardMarkup(buttons)) - await callback_query.answer(f"已切换到第 {page} 页", show_alert=False) - - async def get_abyss_history_season(self, update: "Update", data_id: int): - """进入选择层数""" - callback_query = update.callback_query - user = callback_query.from_user - - self.log_user(update, logger.info, "切换深渊历史数据到层数页 data_id[%s]", data_id) - data = await self.history_data_abyss.get_by_id(data_id) - if not data: - await callback_query.answer("数据不存在,请尝试重新发送命令~", show_alert=True) - await callback_query.edit_message_text("数据不存在,请尝试重新发送命令~") - return - abyss_data = HistoryDataAbyss.from_data(data) - buttons = await self.gen_floor_button(data_id, abyss_data, user.id, data.user_id) - await callback_query.edit_message_reply_markup(reply_markup=InlineKeyboardMarkup(buttons)) - await callback_query.answer("已切换到层数页", show_alert=False) - - async def get_abyss_history_floor(self, update: "Update", data_id: int, detail: str): - """渲染层数数据""" - callback_query = update.callback_query - message = callback_query.message - reply = None - if message.reply_to_message: - reply = message.reply_to_message - - floor = 0 - total = False - if detail == "total": - floor = 0 - elif detail == "all": - total = True - else: - floor = int(detail) - data = await self.history_data_abyss.get_by_id(data_id) - if not data: - await callback_query.answer("数据不存在,请尝试重新发送命令", show_alert=True) - await callback_query.edit_message_text("数据不存在,请尝试重新发送命令~") - return - abyss_data = HistoryDataAbyss.from_data(data) - - images = await self.get_rendered_pic( - abyss_data.abyss_data, abyss_data.character_data, data.user_id, floor, total - ) - if images is None: - await callback_query.answer(f"还没有第 {floor} 层的挑战数据", show_alert=True) - return - await callback_query.answer("正在渲染图片中 请稍等 请不要重复点击按钮", show_alert=False) - - await message.reply_chat_action(ChatAction.UPLOAD_PHOTO) - - for group in ArkoWrapper(images).group(10): # 每 10 张图片分一个组 - await RenderGroupResult(results=group).reply_media_group(reply or message, write_timeout=60) - self.log_user(update, logger.info, "[bold]深渊挑战数据[/bold]: 成功发送图片", extra={"markup": True}) - self.add_delete_message_job(message, delay=1) - - @handler.callback_query(pattern=r"^get_abyss_history\|", block=False) - async def get_abyss_history(self, update: "Update", _: "ContextTypes.DEFAULT_TYPE") -> None: - callback_query = update.callback_query - user = callback_query.from_user - - async def get_abyss_history_callback( - callback_query_data: str, - ) -> Tuple[str, str, int, int]: - _data = callback_query_data.split("|") - _user_id = int(_data[1]) - _uid = int(_data[2]) - _result = _data[3] - _detail = _data[4] if len(_data) > 4 else None - logger.debug( - "callback_query_data函数返回 detail[%s] result[%s] user_id[%s] uid[%s]", - _detail, - _result, - _user_id, - _uid, - ) - return _detail, _result, _user_id, _uid - - detail, result, user_id, uid = await get_abyss_history_callback(callback_query.data) - if user.id != user_id: - await callback_query.answer(text="这不是你的按钮!\n" + config.notice.user_mismatch, show_alert=True) - return - if result == "empty_data": - await callback_query.answer(text="此按钮不可用", show_alert=True) - return - if result.startswith("p_"): - await self.get_abyss_history_page(update, user_id, uid, result) - return - data_id = int(result) - if detail: - await self.get_abyss_history_floor(update, data_id, detail) - return - await self.get_abyss_history_season(update, data_id) - - async def abyss_use_by_inline(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE", previous: bool): - 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, "查询深渊挑战总览数据 previous[%s]", previous) - notice = None - try: - async with self.helper.genshin_or_public(user_id, uid=uid) as client: - if not client.public: - await client.get_record_cards() - abyss_data, avatar_data = await self.get_rendered_pic_data(client, client.player_id, previous) - images = await self.get_rendered_pic(abyss_data, avatar_data, client.player_id, 0, False) - image = images[0] - except AbyssUnlocked: # 若深渊未解锁 - notice = "还未解锁深渊哦~" - except NoMostKills: # 若深渊还未挑战 - notice = "还没有挑战本次深渊呢,咕咕咕~" - except AbyssNotFoundError: - notice = "无法查询玩家挑战队伍详情,只能查询统计详情哦~" - except TooManyRequestPublicCookies: - notice = "查询次数太多,请您稍后重试" - - if notice: - await callback_query.answer(notice, show_alert=True) - return - - await image.edit_inline_media(callback_query) - - async def get_inline_use_data(self) -> List[Optional[IInlineUseData]]: - return [ - IInlineUseData( - text="本期深渊挑战总览", - hash="abyss_current", - callback=partial(self.abyss_use_by_inline, previous=False), - player=True, - ), - IInlineUseData( - text="上期深渊挑战总览", - hash="abyss_previous", - callback=partial(self.abyss_use_by_inline, previous=True), - player=True, - ), - ] diff --git a/plugins/genshin/abyss_team.py b/plugins/genshin/abyss_team.py deleted file mode 100644 index c8787a6..0000000 --- a/plugins/genshin/abyss_team.py +++ /dev/null @@ -1,132 +0,0 @@ -"""Recommend teams for Spiral Abyss""" - -import re - -from telegram import Update -from telegram.constants import ChatAction, ParseMode -from telegram.ext import CallbackContext, filters - -from core.dependence.assets import AssetsService -from core.plugin import Plugin, handler -from core.services.template.services import TemplateService -from metadata.genshin import AVATAR_DATA -from metadata.shortname import idToName -from modules.apihelper.client.components.abyss import AbyssTeam as AbyssTeamClient -from plugins.tools.genshin import GenshinHelper -from utils.log import logger - - -class AbyssTeamPlugin(Plugin): - """Recommend teams for Spiral Abyss""" - - def __init__( - self, - template: TemplateService, - helper: GenshinHelper, - assets_service: AssetsService, - ): - self.template_service = template - self.helper = helper - self.team_data = AbyssTeamClient() - self.assets_service = assets_service - - @handler.command("abyss_team", block=False) - @handler.message(filters.Regex(r"^深渊配队"), block=False) - async def command_start(self, update: Update, _: CallbackContext) -> None: # skipcq: PY-R1000 # - user_id = await self.get_real_user_id(update) - uid, offset = self.get_real_uid_or_offset(update) - message = update.effective_message - - if "help" in message.text or "帮助" in message.text: - await message.reply_text( - "深渊配队推荐功能使用帮助(中括号表示可选参数)\n\n" - "指令格式:\n/abyss_team [n=配队数]\n(pre表示上期)\n\n" - "文本格式:\n深渊配队 [n=配队数] \n\n" - "如:\n" - "/abyss_team\n/abyss_team n=5\n" - "深渊配队\n", - parse_mode=ParseMode.HTML, - ) - self.log_user(update, logger.info, "查询[bold]深渊配队推荐[/bold]帮助", extra={"markup": True}) - return - - self.log_user(update, logger.info, "[bold]深渊配队推荐[/bold]请求", extra={"markup": True}) - - client = await self.helper.get_genshin_client(user_id, player_id=uid, offset=offset) - - await message.reply_chat_action(ChatAction.TYPING) - team_data = await self.team_data.get_data() - - # Set of uids - characters = {c.id for c in await client.get_genshin_characters(client.player_id)} - - teams = { - "Up": [], - "Down": [], - } - - # All of the effective and available teams - for lane in ["Up", "Down"]: - for a_team in team_data[12 - 9][lane]: - t_characters = [int(s) for s in re.findall(r"\d+", a_team["Item"])] - t_rate = a_team["Rate"] - - # Check availability - if not all(c in characters for c in t_characters): - continue - - teams[lane].append( - { - "Characters": t_characters, - "Rate": t_rate, - } - ) - - # If a number is specified, use it as the number of expected teams. - match = re.search(r"(?<=n=)\d+", message.text) - n_team = int(match.group()) if match is not None else 4 - - if "fast" in message.text: - # TODO: Give it a faster method? - # Maybe we can allow characters exist on both side. - return - - # Otherwise, we'd find a team in a complexity - # O(len(teams[up]) * len(teams[down])) - - abyss_teams_data = {"uid": client.player_id, "teams": []} - - async def _get_render_data(id_list): - return [ - { - "icon": (await self.assets_service.avatar(cid).icon()).as_uri(), - "name": idToName(cid), - "star": AVATAR_DATA[str(cid)]["rank"] if cid not in {10000005, 10000007} else 5, - "hava": True, - } - for cid in id_list - ] - - for u in teams["Up"]: - for d in teams["Down"]: - if not all(c not in d["Characters"] for c in u["Characters"]): - continue - team = { - "Up": await _get_render_data(u["Characters"]), - "UpRate": u["Rate"], - "Down": await _get_render_data(d["Characters"]), - "DownRate": d["Rate"], - } - abyss_teams_data["teams"].append(team) - abyss_teams_data["teams"].sort(key=lambda t: t["UpRate"] * t["DownRate"], reverse=True) - abyss_teams_data["teams"] = abyss_teams_data["teams"][0 : min(n_team, len(abyss_teams_data["teams"]))] - - await message.reply_chat_action(ChatAction.UPLOAD_PHOTO) - render_result = await self.template_service.render( - "genshin/abyss_team/abyss_team.jinja2", - abyss_teams_data, - {"width": 785, "height": 800}, - full_page=True, - query_selector=".bg-contain", - ) - await render_result.reply_photo(message, filename=f"abyss_team_{user_id}.png") diff --git a/plugins/genshin/akasha.py b/plugins/genshin/akasha.py deleted file mode 100644 index 7301833..0000000 --- a/plugins/genshin/akasha.py +++ /dev/null @@ -1,120 +0,0 @@ -from typing import TYPE_CHECKING, Optional - -from telegram.constants import ChatAction -from telegram.ext import filters - -from core.dependence.assets import AssetsService -from core.plugin import Plugin, handler -from core.services.template.models import FileType -from core.services.template.services import TemplateService -from gram_core.services.players import PlayersService -from metadata.genshin import AVATAR_DATA -from metadata.shortname import roleToName, roleToId -from modules.apihelper.client.components.akasha import Akasha -from utils.log import logger - -if TYPE_CHECKING: - from telegram import Update - from telegram.ext import ContextTypes - - -class AkashaPlugin(Plugin): - """Akasha 数据排行""" - - def __init__( - self, - assets_service: AssetsService = None, - template_service: TemplateService = None, - player_service: PlayersService = None, - ) -> None: - self.assets_service = assets_service - self.template_service = template_service - self.player_service = player_service - - async def get_user_uid(self, user_id: int, uid: int, offset: int) -> Optional[int]: - player = await self.player_service.get(user_id, player_id=uid, offset=offset) - if player is None: - return None - return player.player_id - - @staticmethod - async def get_leaderboard_data(character_id: int, uid: int = None): - akasha = Akasha() - categories = await akasha.get_leaderboard_categories(character_id) - if len(categories) == 0 or len(categories[0].weapons) == 0: - raise NotImplementedError - calculation_id = categories[0].weapons[0].calculationId - count = categories[0].count - data = await akasha.get_leaderboard(calculation_id) - if len(data) == 0: - raise NotImplementedError - user_data = [] - if uid: - user_data = await akasha.get_leaderboard(calculation_id, uid) - if len(user_data) == 0: - data = [data] - else: - data = [user_data, data] - return data, count - - async def get_avatar_board_render_data(self, character: str, uid: int): - character_id = roleToId(character) - if not character_id: - raise NotImplementedError - try: - name_card = (await self.assets_service.namecard(character_id).navbar()).as_uri() - avatar = (await self.assets_service.avatar(character_id).icon()).as_uri() - except KeyError: - logger.warning("未找到角色 %s 的角色名片/头像", character_id) - name_card = None - avatar = None - rarity = 5 - try: - rarity = {k: v["rank"] for k, v in AVATAR_DATA.items()}[str(character_id)] - except KeyError: - logger.warning("未找到角色 %s 的星级", character_id) - akasha_data, count = await self.get_leaderboard_data(character_id, uid) - return { - "character": character, # 角色名 - "avatar": avatar, # 角色头像 - "namecard": name_card, # 角色名片 - "rarity": rarity, # 角色稀有度 - "count": count, - "all_data": akasha_data, - } - - @handler.command("avatar_board", block=False) - @handler.message(filters.Regex(r"^角色排名(.*)$"), block=False) - async def avatar_board(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE"): - user_id = await self.get_real_user_id(update) - message = update.effective_message - args = self.get_args(context) - if len(args) == 0: - reply_message = await message.reply_text("请指定要查询的角色") - if filters.ChatType.GROUPS.filter(reply_message): - self.add_delete_message_job(message) - self.add_delete_message_job(reply_message) - return - avatar_name = roleToName(args[0]) - uid, offset = self.get_real_uid_or_offset(update) - uid = await self.get_user_uid(user_id, uid, offset) - try: - render_data = await self.get_avatar_board_render_data(avatar_name, uid) - except NotImplementedError: - reply_message = await message.reply_text("暂不支持该角色,或者角色名称错误") - if filters.ChatType.GROUPS.filter(reply_message): - self.add_delete_message_job(message) - self.add_delete_message_job(reply_message) - return - await message.reply_chat_action(ChatAction.UPLOAD_PHOTO) - - image = await self.template_service.render( - "genshin/akasha/char_rank.jinja2", - render_data, - viewport={"width": 1040, "height": 500}, - full_page=True, - query_selector=".container", - file_type=FileType.PHOTO, - ttl=24 * 60 * 60, - ) - await image.reply_photo(message) diff --git a/plugins/genshin/avatar_list.py b/plugins/genshin/avatar_list.py deleted file mode 100644 index de98e8a..0000000 --- a/plugins/genshin/avatar_list.py +++ /dev/null @@ -1,270 +0,0 @@ -import asyncio -import math -from typing import List, Optional, Sequence, TYPE_CHECKING, Union, Tuple, Any, Dict - -from arkowrapper import ArkoWrapper -from simnet import GenshinClient -from simnet.errors import BadRequest as SimnetBadRequest -from simnet.models.genshin.calculator import CalculatorTalent, CalculatorCharacterDetails -from simnet.models.genshin.chronicle.characters import Character -from telegram.constants import ChatAction -from telegram.ext import filters - -from core.config import config -from core.dependence.assets import AssetsService -from core.plugin import Plugin, handler -from core.services.cookies import CookiesService -from core.services.players import PlayersService -from core.services.players.services import PlayerInfoService -from core.services.template.models import FileType -from core.services.template.services import TemplateService -from gram_core.plugin.methods.inline_use_data import IInlineUseData -from gram_core.services.template.models import RenderGroupResult -from modules.wiki.base import Model -from plugins.tools.genshin import CharacterDetails, GenshinHelper -from plugins.tools.player_info import PlayerInfoSystem -from utils.log import logger -from utils.uid import mask_number - -if TYPE_CHECKING: - from telegram import Update - from telegram.ext import ContextTypes - from gram_core.services.template.models import RenderResult - -MAX_AVATAR_COUNT = 40 - - -class TooManyRequests(Exception): - """请求过多""" - - -class SkillData(Model): - """天赋数据""" - - skill: CalculatorTalent - buffed: bool = False - """是否得到了命座加成""" - - -class AvatarData(Model): - avatar: Character - detail: CalculatorCharacterDetails - icon: str - weapon: Optional[str] - skills: List[SkillData] - - def sum_of_skills(self) -> int: - total_level = 0 - for skill_data in self.skills: - total_level += skill_data.skill.level - return total_level - - -class AvatarListPlugin(Plugin): - """练度统计""" - - def __init__( - self, - player_service: PlayersService = None, - cookies_service: CookiesService = None, - assets_service: AssetsService = None, - template_service: TemplateService = None, - helper: GenshinHelper = None, - character_details: CharacterDetails = None, - player_info_service: PlayerInfoService = None, - player_info_system: PlayerInfoSystem = None, - ) -> None: - self.cookies_service = cookies_service - self.assets_service = assets_service - self.template_service = template_service - self.helper = helper - self.character_details = character_details - self.player_service = player_service - self.player_info_service = player_info_service - self.player_info_system = player_info_system - - async def get_avatar_data(self, character: Character, client: "GenshinClient") -> Optional["AvatarData"]: - detail = await self.character_details.get_character_details(client, character) - if detail is None: - return None - if character.id == 10000005: # 针对男草主 - talents = [] - for talent in detail.talents: - if "普通攻击" in talent.name: - talent.Config.allow_mutation = True - # noinspection Pydantic - talent.group_id = 1131 - if talent.type in ["attack", "skill", "burst"]: - talents.append(talent) - else: - talents = [t for t in detail.talents if t.type in ["attack", "skill", "burst"]] - buffed_talents = [] - for constellation in filter(lambda x: x.pos in [3, 5], character.constellations[: character.constellation]): - if result := list( - filter(lambda x: all([x.name in constellation.effect]), talents) # pylint: disable=W0640 - ): - buffed_talents.append(result[0].type) - return AvatarData( - avatar=character, - detail=detail, - icon=(await self.assets_service.avatar(character.id).side()).as_uri(), - weapon=( - await self.assets_service.weapon(character.weapon.id).__getattr__( - "icon" if character.weapon.ascension < 2 else "awaken" - )() - ).as_uri(), - skills=[ - SkillData(skill=s, buffed=s.type in buffed_talents) - for s in sorted(talents, key=lambda x: ["attack", "skill", "burst"].index(x.type)) - ], - ) - - async def get_avatars_data( - self, characters: Sequence[Character], client: "GenshinClient", max_length: int = None - ) -> List["AvatarData"]: - async def _task(c): - return await self.get_avatar_data(c, client) - - task_results = await asyncio.gather(*[_task(character) for character in characters]) - - return sorted( - list(filter(lambda x: x, task_results)), - key=lambda x: ( - x.avatar.level, - x.avatar.rarity, - x.sum_of_skills(), - x.avatar.constellation, - # TODO 如果加入武器排序条件,需要把武器转化为图片url的处理后置 - # x.weapon.level, - # x.weapon.rarity, - # x.weapon.refinement, - x.avatar.friendship, - ), - reverse=True, - )[:max_length] - - 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( - "genshin/avatar_list/main.jinja2", - _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)] - 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)] - return await asyncio.gather(*tasks) - - async def render( - self, client: "GenshinClient", user_id: int, user_name: str, all_avatars: bool = False - ) -> List["RenderResult"]: - characters = await client.get_genshin_characters(client.player_id) - avatar_datas: List[AvatarData] = await self.get_avatars_data( - characters, client, None if all_avatars else MAX_AVATAR_COUNT - ) - if not avatar_datas: - raise TooManyRequests() - name_card, avatar, nickname, rarity = await self.player_info_system.get_player_info( - client.player_id, user_id, user_name - ) - base_render_data = { - "uid": mask_number(client.player_id), # 玩家uid - "nickname": nickname, # 玩家昵称 - "avatar": avatar, # 玩家头像 - "rarity": rarity, # 玩家头像对应的角色星级 - "namecard": name_card, # 玩家名片 - "has_more": len(characters) != len(avatar_datas), # 是否显示了全部角色 - } - - return await self.avatar_list_render(base_render_data, avatar_datas, not all_avatars) - - @handler.command("avatars", cookie=True, block=False) - @handler.message(filters.Regex(r"^(全部)?练度统计$"), cookie=True, block=False) - async def avatar_list(self, update: "Update", _: "ContextTypes.DEFAULT_TYPE"): - user_id = await self.get_real_user_id(update) - user_name = self.get_real_user_name(update) - uid, offset = self.get_real_uid_or_offset(update) - message = update.effective_message - all_avatars = "全部" in message.text or "all" in message.text # 是否发送全部角色 - - self.log_user(update, logger.info, "[bold]练度统计[/bold]: all=%s", all_avatars, extra={"markup": True}) - try: - async with self.helper.genshin(user_id, player_id=uid, offset=offset) as client: - notice = await message.reply_text(f"{config.notice.bot_name}需要收集整理数据,还请耐心等待哦~") - self.add_delete_message_job(notice, delay=60) - await message.reply_chat_action(ChatAction.TYPING) - images = await self.render(client, user_id, user_name, all_avatars) - except TooManyRequests: - reply_message = await message.reply_html("服务器熟啦 ~ 请稍后再试") - self.add_delete_message_job(reply_message, delay=20) - return - except SimnetBadRequest as e: - if e.ret_code == -502002: - reply_message = await message.reply_html("请先在米游社中使用一次养成计算器后再使用此功能~") - self.add_delete_message_job(reply_message, delay=20) - return - raise e - - await message.reply_chat_action(ChatAction.UPLOAD_PHOTO) - - for group in ArkoWrapper(images).group(10): # 每 10 张图片分一个组 - await RenderGroupResult(results=group).reply_media_group(message, write_timeout=60) - - self.log_user( - update, - logger.info, - "[bold]练度统计[/bold]发送图片成功", - extra={"markup": True}, - ) - - async def avatar_list_use_by_inline(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> None: - callback_query = update.callback_query - user_id = await self.get_real_user_id(update) - user_name = self.get_real_user_name(update) - uid = IInlineUseData.get_uid_from_context(context) - self.log_user(update, logger.info, "查询练度统计") - - try: - async with self.helper.genshin(user_id, player_id=uid) as client: - images = await self.render(client, user_id, user_name) - render = images[0] - except TooManyRequests: - await callback_query.answer("服务器熟啦 ~ 请稍后再试", show_alert=True) - return - except SimnetBadRequest as e: - if e.ret_code == -502002: - await callback_query.answer("请先在米游社中使用一次养成计算器后再使用此功能~", show_alert=True) - return - raise e - 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, - ) - ] diff --git a/plugins/genshin/birthday.py b/plugins/genshin/birthday.py deleted file mode 100644 index 3457180..0000000 --- a/plugins/genshin/birthday.py +++ /dev/null @@ -1,85 +0,0 @@ -import re -from typing import TYPE_CHECKING - -from telegram.ext import filters - -from core.plugin import Plugin, handler -from core.services.cookies import CookiesService -from core.services.task.services import TaskCardServices -from core.services.users.services import UserService, UserAdminService -from metadata.genshin import AVATAR_DATA -from metadata.shortname import roleToId, roleToName -from plugins.tools.birthday_card import ( - BirthdayCardSystem, - rm_starting_str, -) -from plugins.tools.genshin import GenshinHelper -from utils.log import logger - -if TYPE_CHECKING: - from telegram import Update - from telegram.ext import ContextTypes - - -class BirthdayPlugin(Plugin): - """Birthday.""" - - def __init__( - self, - user_service: UserService, - helper: GenshinHelper, - cookie_service: CookiesService, - card_system: BirthdayCardSystem, - user_admin_service: UserAdminService, - card_service: TaskCardServices, - ): - """Load Data.""" - self.user_service = user_service - self.cookie_service = cookie_service - self.helper = helper - self.card_system = card_system - self.user_admin_service = user_admin_service - self.card_service = card_service - - @handler.command(command="birthday", block=False) - async def command_start(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> None: - message = update.effective_message - args = self.get_args(context) - if len(args) >= 1: - msg = args[0] - self.log_user(update, logger.info, "查询角色生日命令请求 || 参数 %s", msg) - if re.match(r"\d{1,2}.\d{1,2}", msg): - try: - month = rm_starting_str(re.findall(r"\d+", msg)[0], "0") - day = rm_starting_str(re.findall(r"\d+", msg)[1], "0") - key = f"{month}_{day}" - day_list = self.card_system.birthday_list.get(key, []) - date = f"{month}月{day}日" - text = f"{date} 是 {'、'.join(day_list)} 的生日哦~" if day_list else f"{date} 没有角色过生日哦~" - except IndexError: - text = "请输入正确的日期格式,如1-1,或输入正确的角色名称。" - reply_message = await message.reply_text(text) - - else: - try: - if msg == "派蒙": - text = "派蒙的生日是6月1日哦~" - elif roleToName(msg) == "旅行者": - text = "喂,旅行者!你该不会忘掉自己的生日了吧?" - else: - name = roleToName(msg) - aid = str(roleToId(msg)) - birthday = AVATAR_DATA[aid]["birthday"] - text = f"{name} 的生日是 {birthday[0]}月{birthday[1]}日 哦~" - reply_message = await message.reply_text(text) - except KeyError: - reply_message = await message.reply_text("请输入正确的日期格式,如1-1,或输入正确的角色名称。") - else: - self.log_user(update, logger.info, "查询今日角色生日列表") - today_list = self.card_system.get_today_birthday() - text = f"今天是 {'、'.join(today_list)} 的生日哦~" if today_list else "今天没有角色过生日哦~" - reply_message = await message.reply_text(text) - - if filters.ChatType.GROUPS.filter(reply_message): - self.add_delete_message_job(message) - self.add_delete_message_job(reply_message) diff --git a/plugins/genshin/calendar.py b/plugins/genshin/calendar.py deleted file mode 100644 index 888f93b..0000000 --- a/plugins/genshin/calendar.py +++ /dev/null @@ -1,90 +0,0 @@ -from datetime import datetime, timedelta -from functools import partial -from typing import Dict, List, Optional, TYPE_CHECKING - -from telegram import Update -from telegram.constants import ChatAction -from telegram.ext import CallbackContext, MessageHandler, filters - -from core.dependence.assets import AssetsService -from core.dependence.redisdb import RedisDB -from core.plugin import Plugin, handler -from core.services.template.services import TemplateService -from gram_core.plugin.methods.inline_use_data import IInlineUseData -from modules.apihelper.client.components.calendar import Calendar -from utils.log import logger - -try: - import ujson as jsonlib -except ImportError: - import json as jsonlib - -if TYPE_CHECKING: - from telegram.ext import ContextTypes - from gram_core.services.template.models import RenderResult - - -class CalendarPlugin(Plugin): - """活动日历查询""" - - def __init__( - self, - template_service: TemplateService, - assets_service: AssetsService, - redis: RedisDB, - ): - self.template_service = template_service - self.assets_service = assets_service - self.calendar = Calendar() - self.cache = redis.client - - async def _fetch_data(self) -> Dict: - if data := await self.cache.get("plugin:calendar"): - return jsonlib.loads(data.decode("utf-8")) - data = await self.calendar.get_photo_data(self.assets_service) - now = datetime.now() - next_hour = (now + timedelta(hours=1)).replace(minute=0, second=0, microsecond=0) - await self.cache.set("plugin:calendar", jsonlib.dumps(data, default=lambda x: x.dict()), ex=next_hour - now) - return data - - async def render(self, list_mode: bool) -> "RenderResult": - data = await self._fetch_data() - data["display_mode"] = "list" if list_mode else "calendar" - return await self.template_service.render( - "genshin/calendar/calendar.jinja2", - data, - query_selector=".container", - ) - - @handler.command("calendar", block=False) - @handler(MessageHandler, filters=filters.Regex(r"^(活动)+(日历|日历列表)$"), block=False) - async def command_start(self, update: Update, _: CallbackContext) -> None: - message = update.effective_message - mode = "list" if "列表" in message.text else "calendar" - self.log_user(update, logger.info, "查询日历 | 模式 %s", mode) - await message.reply_chat_action(ChatAction.TYPING) - image = await self.render(mode == "list") - await message.reply_chat_action(ChatAction.UPLOAD_PHOTO) - await image.reply_photo(message) - - async def calendar_use_by_inline(self, update: "Update", _: "ContextTypes.DEFAULT_TYPE", list_mode: bool): - callback_query = update.callback_query - - self.log_user(update, logger.info, "查询日历 | 列表模式 %s", list_mode) - await callback_query.answer("正在查询日历,请耐心等待") - image = await self.render(list_mode) - await image.edit_inline_media(callback_query) - - async def get_inline_use_data(self) -> List[Optional[IInlineUseData]]: - return [ - IInlineUseData( - text="活动日历", - hash="calendar", - callback=partial(self.calendar_use_by_inline, list_mode=False), - ), - IInlineUseData( - text="活动日历列表", - hash="calendar_list", - callback=partial(self.calendar_use_by_inline, list_mode=True), - ), - ] diff --git a/plugins/genshin/daily/material.py b/plugins/genshin/daily/material.py deleted file mode 100644 index cba0efd..0000000 --- a/plugins/genshin/daily/material.py +++ /dev/null @@ -1,790 +0,0 @@ -import asyncio -import typing -from asyncio import Lock -from ctypes import c_double -from datetime import datetime -from functools import partial -from multiprocessing import Value -from os import path -from ssl import SSLZeroReturnError -from time import time as time_ -from typing import TYPE_CHECKING, Dict, Iterable, Iterator, List, Optional, Tuple - -import aiofiles -import aiofiles.os -import bs4 -import pydantic -from arkowrapper import ArkoWrapper -from httpx import AsyncClient, HTTPError, TimeoutException -from pydantic import BaseModel -from simnet.errors import BadRequest as SimnetBadRequest -from simnet.errors import InvalidCookies -from simnet.models.genshin.chronicle.characters import Character -from telegram.constants import ChatAction, ParseMode -from telegram.error import RetryAfter, TimedOut - -from core.config import config -from core.dependence.assets import AssetsCouldNotFound, AssetsService, AssetsServiceType -from core.plugin import Plugin, handler -from core.services.template.models import FileType, RenderGroupResult -from core.services.template.services import TemplateService -from metadata.genshin import AVATAR_DATA, HONEY_DATA -from plugins.tools.genshin import CharacterDetails, CookiesNotFoundError, GenshinHelper, PlayerNotFoundError -from utils.const import DATA_DIR -from utils.log import logger -from utils.uid import mask_number - -if TYPE_CHECKING: - from simnet import GenshinClient - from telegram import Message, Update - from telegram.ext import ContextTypes - -INTERVAL = 1 - -DATA_FILE_PATH = DATA_DIR.joinpath("daily_material.json").resolve() -# fmt: off -# 章节顺序、国家(区域)名是从《足迹》 PV 中取的 -DOMAINS = [ - "忘却之峡", # 蒙德精通秘境 - "太山府", # 璃月精通秘境 - "菫色之庭", # 稻妻精通秘境 - "昏识塔", # 须弥精通秘境 - "苍白的遗荣", # 枫丹精通秘境 - "", # 纳塔精通秘境 - "", # 至东精通秘境 - "", # 坎瑞亚精通秘境 - "塞西莉亚苗圃", # 蒙德炼武秘境 - "震雷连山密宫", # 璃月炼武秘境 - "砂流之庭", # 稻妻炼武秘境 - "有顶塔", # 须弥炼武秘境 - "深潮的余响", # 枫丹炼武秘境 - "", # 纳塔炼武秘境 - "", # 至东炼武秘境 - "", # 坎瑞亚炼武秘境 -] -# fmt: on -DOMAIN_AREA_MAP = dict(zip(DOMAINS, ["蒙德", "璃月", "稻妻", "须弥", "枫丹", "纳塔", "至冬", "坎瑞亚"] * 2)) -# 此处 avatar 和 weapon 需要分别对应 AreaDailyMaterialsData 中的两个 *_materials 字段,具体逻辑见 _parse_honey_impact_source -DOMAIN_TYPE_MAP = dict(zip(DOMAINS, len(DOMAINS) // 2 * ["avatar"] + len(DOMAINS) // 2 * ["weapon"])) - -WEEK_MAP = ["一", "二", "三", "四", "五", "六", "日"] - - -def sort_item(items: List["ItemData"]) -> Iterable["ItemData"]: - """对武器和角色进行排序 - - 排序规则:持有(星级 > 等级 > 命座/精炼) > 未持有(星级 > 等级 > 命座/精炼) - """ - - def key(item: "ItemData"): - # 有个小小的意外逻辑,不影响排序输出,如果要修改可能需要注意下: - # constellation 可以为 None 或 0,此时都会被判断为 False - # 此时 refinment or constellation or -1 都会返回 -1 - # 但不影响排序结果 - return ( - item.level is not None, # 根据持有与未持有进行分组并排序 - item.rarity, # 根据星级分组并排序 - item.refinement or item.constellation or -1, # 根据命座/精炼进行分组并排序 - item.id, # 默认按照物品 ID 进行排序 - ) - - return sorted(items, key=key, reverse=True) - - -def get_material_serial_name(names: Iterable[str]) -> str: - """ - 获取材料的系列名,本质上是求字符串列表的最长子串 - 如:「自由」的教导、「自由」的指引、「自由」的哲学,三者系列名为「『自由』」 - 如:高塔孤王的破瓦、高塔孤王的残垣、高塔孤王的断片、高塔孤王的碎梦,四者系列名为「高塔孤王」 - TODO(xr1s): 感觉可以优化 - """ - - def all_substrings(string: str) -> Iterator[str]: - """获取字符串的所有连续字串""" - length = len(string) - for i in range(length): - for j in range(i + 1, length + 1): - yield string[i:j] - - result = [] - for name_a, name_b in ArkoWrapper(names).repeat(1).group(2).unique(list): - for sub_string in all_substrings(name_a): - if sub_string in ArkoWrapper(all_substrings(name_b)): - result.append(sub_string) - result = ArkoWrapper(result).sort(len, reverse=True)[0] - chars = {"的": 0, "之": 0} - for char, k in chars.items(): - result = result.split(char)[k] - return result - - -class MaterialsData(BaseModel): - __root__: Optional[List[Dict[str, "AreaDailyMaterialsData"]]] = None - - def weekday(self, weekday: int) -> Dict[str, "AreaDailyMaterialsData"]: - if self.__root__ is None: - return {} - return self.__root__[weekday] - - def is_empty(self) -> bool: - return self.__root__ is None - - -class DailyMaterial(Plugin): - """每日素材表""" - - everyday_materials: "MaterialsData" = MaterialsData() - """ - everyday_materials 储存的是一周中每天能刷的素材 ID - 按照如下形式组织 - ```python - everyday_materials[周几][国家] = AreaDailyMaterialsData( - avatar=[角色, 角色, ...], - avatar_materials=[精通素材, 精通素材, 精通素材], - weapon=[武器, 武器, ...] - weapon_materials=[炼武素材, 炼武素材, 炼武素材, 炼武素材], - ) - ``` - """ - - locks: Tuple[Lock, Lock] = (Lock(), Lock()) - """ - Tuple[每日素材缓存锁, 角色武器材料图标锁] - """ - - def __init__( - self, - assets: AssetsService, - template_service: TemplateService, - helper: GenshinHelper, - character_details: CharacterDetails, - ): - self.assets_service = assets - self.template_service = template_service - self.helper = helper - self.character_details = character_details - self.client = AsyncClient() - - async def initialize(self): - """插件在初始化时,会检查一下本地是否缓存了每日素材的数据""" - - async def task_daily(): - async with self.locks[0]: - logger.info("正在开始获取每日素材缓存") - await self._refresh_everyday_materials() - - # 当缓存不存在或已过期(大于 3 天)则重新下载 - # TODO(xr1s): 是不是可以改成 21 天? - if not await aiofiles.os.path.exists(DATA_FILE_PATH): - asyncio.create_task(task_daily()) - else: - mtime = await aiofiles.os.path.getmtime(DATA_FILE_PATH) - mtime = datetime.fromtimestamp(mtime) - elapsed = datetime.now() - mtime - if elapsed.days > 3: - asyncio.create_task(task_daily()) - - # 若存在则直接使用缓存 - if await aiofiles.os.path.exists(DATA_FILE_PATH): - async with aiofiles.open(DATA_FILE_PATH, "rb") as cache: - try: - self.everyday_materials = self.everyday_materials.parse_raw(await cache.read()) - except pydantic.ValidationError: - await aiofiles.os.remove(DATA_FILE_PATH) - asyncio.create_task(task_daily()) - - async def _get_skills_data(self, client: "FragileGenshinClient", character: Character) -> Optional[List[int]]: - if client.damaged: - return None - detail = None - try: - real_client = typing.cast("GenshinClient", client.client) - detail = await self.character_details.get_character_details(real_client, character) - except InvalidCookies: - client.damaged = True - except SimnetBadRequest as e: - if e.ret_code == -502002: - client.damaged = True - raise - if detail is None: - return None - talents = [t for t in detail.talents if t.type in ["attack", "skill", "burst"]] - return [t.level for t in talents] - - async def _get_items_from_user( - self, user_id: int, uid: int, offset: int - ) -> Tuple[Optional["GenshinClient"], "UserOwned"]: - """获取已经绑定的账号的角色、武器信息""" - user_data = UserOwned() - try: - logger.debug("尝试获取已绑定的原神账号") - client = await self.helper.get_genshin_client(user_id, player_id=uid, offset=offset) - logger.debug("获取账号数据成功: UID=%s", client.player_id) - characters = await client.get_genshin_characters(client.player_id) - for character in characters: - if character.name == "旅行者": - continue - character_id = str(AVATAR_DATA[str(character.id)]["id"]) - character_assets = self.assets_service.avatar(character_id) - character_icon = await character_assets.icon(False) - character_side = await character_assets.side(False) - user_data.avatar[character_id] = ItemData( - id=character_id, - name=typing.cast(str, character.name), - rarity=int(typing.cast(str, character.rarity)), - level=character.level, - constellation=character.constellation, - gid=character.id, - icon=character_icon.as_uri(), - origin=character, - ) - # 判定武器的突破次数是否大于 2, 若是, 则将图标替换为 awakened (觉醒) 的图标 - weapon = character.weapon - weapon_id = str(weapon.id) - weapon_awaken = "icon" if weapon.ascension < 2 else "awaken" - weapon_icon = await getattr(self.assets_service.weapon(weapon_id), weapon_awaken)() - if weapon_id not in user_data.weapon: - # 由于用户可能持有多把同一种武器 - # 这里需要使用 List 来储存所有不同角色持有的同名武器 - user_data.weapon[weapon_id] = [] - user_data.weapon[weapon_id].append( - ItemData( - id=weapon_id, - name=weapon.name, - level=weapon.level, - rarity=weapon.rarity, - refinement=weapon.refinement, - icon=weapon_icon.as_uri(), - c_path=character_side.as_uri(), - ) - ) - except (PlayerNotFoundError, CookiesNotFoundError): - self.log_user(user_id, logger.info, "未查询到绑定的账号信息") - except InvalidCookies: - self.log_user(user_id, logger.info, "所绑定的账号信息已失效") - else: - # 没有异常返回数据 - return client, user_data - # 有上述异常的, client 会返回 None - return None, user_data - - async def area_user_weapon( - self, - area_name: str, - user_owned: "UserOwned", - area_daily: "AreaDailyMaterialsData", - loading_prompt: "Message", - ) -> Optional["AreaData"]: - """ - area_user_weapon 通过从选定区域当日可突破的武器中查找用户持有的武器 - 计算 /daily_material 返回的页面中该国下会出现的武器列表 - """ - weapon_items: List["ItemData"] = [] - for weapon_id in area_daily.weapon: - weapons = user_owned.weapon.get(weapon_id) - if weapons is None or len(weapons) == 0: - weapon = await self._assemble_item_from_honey_data("weapon", weapon_id) - if weapon is None: - continue - weapons = [weapon] - if weapons[0].rarity < 4: - continue - weapon_items.extend(weapons) - if len(weapon_items) == 0: - return None - weapon_materials = await self.user_materials(area_daily.weapon_materials, loading_prompt) - return AreaData( - name=area_name, - materials=weapon_materials, - items=list(sort_item(weapon_items)), - material_name=get_material_serial_name(map(lambda x: x.name, weapon_materials)), - ) - - async def area_user_avatar( - self, - area_name: str, - user_owned: "UserOwned", - area_daily: "AreaDailyMaterialsData", - client: "FragileGenshinClient", - loading_prompt: "Message", - ) -> Optional["AreaData"]: - """ - area_user_avatar 通过从选定区域当日可升级的角色技能中查找用户拥有的角色 - 计算 /daily_material 返回的页面中该国下会出现的角色列表 - """ - avatar_items: List[ItemData] = [] - for avatar_id in area_daily.avatar: - avatar = user_owned.avatar.get(avatar_id) - avatar = avatar or await self._assemble_item_from_honey_data("avatar", avatar_id) - if avatar is None: - continue - if avatar.origin is None: - avatar_items.append(avatar) - continue - # 最大努力获取用户角色等级 - try: - avatar.skills = await self._get_skills_data(client, avatar.origin) - except SimnetBadRequest as e: - if e.ret_code != -502002: - raise e - self.add_delete_message_job(loading_prompt, delay=5) - await loading_prompt.edit_text( - "获取角色天赋信息失败,如果想要显示角色天赋信息,请先在米游社/HoYoLab中使用一次养成计算器后再使用此功能~", - parse_mode=ParseMode.HTML, - ) - avatar_items.append(avatar) - if len(avatar_items) == 0: - return None - avatar_materials = await self.user_materials(area_daily.avatar_materials, loading_prompt) - return AreaData( - name=area_name, - materials=avatar_materials, - items=list(sort_item(avatar_items)), - material_name=get_material_serial_name(map(lambda x: x.name, avatar_materials)), - ) - - async def user_materials(self, material_ids: List[str], loading_prompt: "Message") -> List["ItemData"]: - """ - user_materials 返回 /daily_material 每个国家角色或武器列表右上角标的素材列表 - """ - area_materials: List[ItemData] = [] - for material_id in material_ids: # 添加这个区域当天(weekday)的培养素材 - material = None - try: - material = self.assets_service.material(material_id) - except AssetsCouldNotFound as exc: - logger.warning("AssetsCouldNotFound message[%s] target[%s]", exc.message, exc.target) - await loading_prompt.edit_text(f"出错了呜呜呜 ~ {config.notice.bot_name}找不到一些素材") - raise - [_, material_name, material_rarity] = HONEY_DATA["material"][material_id] - material_icon = await material.icon(False) - material_uri = material_icon.as_uri() - area_materials.append( - ItemData( - id=material_id, - icon=material_uri, - name=typing.cast(str, material_name), - rarity=typing.cast(int, material_rarity), - ) - ) - return area_materials - - @handler.command("daily_material", block=False) - async def daily_material(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE"): - user_id = await self.get_real_user_id(update) - uid, offset = self.get_real_uid_or_offset(update) - message = typing.cast("Message", update.effective_message) - args = self.get_args(context) - now = datetime.now() - - try: - weekday = (_ := int(args[0])) - (_ > 0) - weekday = (weekday % 7 + 7) % 7 - time = title = f"星期{WEEK_MAP[weekday]}" - except (ValueError, IndexError): - title = "今日" - weekday = now.weekday() - (1 if now.hour < 4 else 0) - weekday = 6 if weekday < 0 else weekday - time = f"星期{WEEK_MAP[weekday]}" - full = bool(args and args[-1] == "full") # 判定最后一个参数是不是 full - - self.log_user(update, logger.info, "每日素材命令请求 || 参数 weekday=%s full=%s", WEEK_MAP[weekday], full) - - if weekday == 6: - the_day = "今天" if title == "今日" else "这天" - await message.reply_text(f"{the_day}是星期天, 全部素材都可以刷哦~", parse_mode=ParseMode.HTML) - return - - if self.locks[0].locked(): # 若检测到了第一个锁:正在下载每日素材表的数据 - loading_prompt = await message.reply_text(f"{config.notice.bot_name}正在摘抄每日素材表,以后再来探索吧~") - self.add_delete_message_job(loading_prompt, delay=5) - return - - if self.locks[1].locked(): # 若检测到了第二个锁:正在下载角色、武器、材料的图标 - await message.reply_text(f"{config.notice.bot_name}正在搬运每日素材的图标,以后再来探索吧~") - return - - loading_prompt = await message.reply_text(f"{config.notice.bot_name}可能需要找找图标素材,还请耐心等待哦~") - await message.reply_chat_action(ChatAction.TYPING) - - # 获取已经缓存的秘境素材信息 - if self.everyday_materials.is_empty(): # 若没有缓存每日素材表的数据 - logger.info("正在获取每日素材缓存") - await self._refresh_everyday_materials() - - # 尝试获取用户已绑定的原神账号信息 - client, user_owned = await self._get_items_from_user(user_id, uid, offset) - today_materials = self.everyday_materials.weekday(weekday) - fragile_client = FragileGenshinClient(client) - area_avatars: List["AreaData"] = [] - area_weapons: List["AreaData"] = [] - for country_name, area_daily in today_materials.items(): - area_avatar = await self.area_user_avatar( - country_name, user_owned, area_daily, fragile_client, loading_prompt - ) - if area_avatar is not None: - area_avatars.append(area_avatar) - area_weapon = await self.area_user_weapon(country_name, user_owned, area_daily, loading_prompt) - if area_weapon is not None: - area_weapons.append(area_weapon) - render_data = RenderData( - title=title, - time=time, - uid=mask_number(client.player_id) if client else client, - character=area_avatars, - weapon=area_weapons, - ) - - await message.reply_chat_action(ChatAction.TYPING) - - # 是否发送原图 - file_type = FileType.DOCUMENT if full else FileType.PHOTO - - character_img_data, weapon_img_data = await asyncio.gather( - self.template_service.render( # 渲染角色素材页 - "genshin/daily_material/character.jinja2", - {"data": render_data}, - {"width": 1338, "height": 500}, - file_type=file_type, - ttl=30 * 24 * 60 * 60, - ), - self.template_service.render( # 渲染武器素材页 - "genshin/daily_material/weapon.jinja2", - {"data": render_data}, - {"width": 1338, "height": 500}, - file_type=file_type, - ttl=30 * 24 * 60 * 60, - ), - ) - - self.add_delete_message_job(loading_prompt, delay=5) - await message.reply_chat_action(ChatAction.UPLOAD_PHOTO) - - character_img_data.filename = f"{title}可培养角色.png" - weapon_img_data.filename = f"{title}可培养武器.png" - - await RenderGroupResult([character_img_data, weapon_img_data]).reply_media_group(message) - - logger.debug("角色、武器培养素材图发送成功") - - @handler.command("refresh_daily_material", admin=True, block=False) - async def refresh(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE"): - user = update.effective_user - message = update.effective_message - - logger.info("用户 {%s}[%s] 刷新[bold]每日素材[/]缓存命令", user.full_name, user.id, extra={"markup": True}) - if self.locks[0].locked(): - notice = await message.reply_text(f"{config.notice.bot_name}还在抄每日素材表呢,我有在好好工作哦~") - self.add_delete_message_job(notice, delay=10) - return - if self.locks[1].locked(): - notice = await message.reply_text(f"{config.notice.bot_name}正在搬运每日素材图标,在努力工作呢!") - self.add_delete_message_job(notice, delay=10) - return - async with self.locks[1]: # 锁住第二把锁 - notice = await message.reply_text( - f"{config.notice.bot_name}正在重新摘抄每日素材表,请稍等~", parse_mode=ParseMode.HTML - ) - async with self.locks[0]: # 锁住第一把锁 - await self._refresh_everyday_materials() - notice = await notice.edit_text( - "每日素材表" - + ("摘抄完成!" if self.everyday_materials else "坏掉了!等会它再长好了之后我再抄。。。") - + "\n正搬运每日素材的图标中。。。", - parse_mode=ParseMode.HTML, - ) - time = await self._download_icon(notice) - - async def job(_, n): - await n.edit_text(n.text_html.split("\n")[0] + "\n每日素材图标搬运完成!", parse_mode=ParseMode.HTML) - await asyncio.sleep(INTERVAL) - await notice.delete() - - context.application.job_queue.run_once( - partial(job, n=notice), when=time + INTERVAL, name="notice_msg_final_job" - ) - - async def _refresh_everyday_materials(self, retry: int = 5): - """刷新来自 honey impact 的每日素材表""" - for attempts in range(1, retry + 1): - try: - response = await self.client.get("https://gensh.honeyhunterworld.com/?lang=CHS") - response.raise_for_status() - except (HTTPError, SSLZeroReturnError): - await asyncio.sleep(1) - if attempts == retry: - logger.error("每日素材刷新失败, 请稍后重试") - return - else: - logger.warning("每日素材刷新失败, 正在重试第 %d 次", attempts) - continue - self.everyday_materials = _parse_honey_impact_source(response.content) - # 当场缓存到文件 - content = self.everyday_materials.json(ensure_ascii=False, separators=(",", ":")) - async with aiofiles.open(DATA_FILE_PATH, "w", encoding="utf-8") as file: - await file.write(content) - logger.success("每日素材刷新成功") - return - - async def _assemble_item_from_honey_data(self, item_type: str, item_id: str) -> Optional["ItemData"]: - """用户拥有的角色和武器中找不到数据时,使用 HoneyImpact 的数据组装出基本信息置灰展示""" - honey_item = HONEY_DATA[item_type].get(item_id) - if honey_item is None: - return None - try: - icon = await getattr(self.assets_service, item_type)(item_id).icon() - except KeyError: - return None - return ItemData( - id=item_id, - name=typing.cast(str, honey_item[1]), - rarity=typing.cast(int, honey_item[2]), - icon=icon.as_uri(), - ) - - async def _download_icon(self, message: "Message") -> float: - """下载素材图标""" - asset_list = [] - lock = asyncio.Lock() - the_time = Value(c_double, time_() - INTERVAL) - - async def edit_message(text): - """修改提示消息""" - async with lock: - if message is not None and time_() >= (the_time.value + INTERVAL): - try: - await message.edit_text( - "\n".join(message.text_html.split("\n")[:2] + [text]), parse_mode=ParseMode.HTML - ) - the_time.value = time_() - except (TimedOut, RetryAfter): - pass - - async def task(item_id, name, item_type): - try: - logger.debug("正在开始下载 %s 的图标素材", name) - await edit_message(f"正在搬运 {name} 的图标素材。。。") - asset: AssetsServiceType = getattr(self.assets_service, item_type)(item_id) - asset_list.append(asset.honey_id) - # 找到该素材对象的所有图标类型 - # 并根据图标类型找到下载对应图标的函数 - for icon_type in asset.icon_types: - await getattr(asset, icon_type)(True) # 执行下载函数 - logger.debug("%s 的图标素材下载成功", name) - await edit_message(f"正在搬运 {name} 的图标素材。。。成功!") - except TimeoutException as exc: - logger.warning("Httpx [%s]\n%s[%s]", exc.__class__.__name__, exc.request.method, exc.request.url) - return exc - except Exception as exc: - logger.error("图标素材下载出现异常!", exc_info=exc) - return exc - - notice_text = "图标素材下载完成" - for TYPE, ITEMS in HONEY_DATA.items(): # 遍历每个对象 - task_list = [] - new_items = [] - for ID, DATA in ITEMS.items(): - if (ITEM := [ID, DATA[1], TYPE]) not in new_items: - new_items.append(ITEM) - task_list.append(task(*ITEM)) - results = await asyncio.gather(*task_list, return_exceptions=True) # 等待所有任务执行完成 - for result in results: - if isinstance(result, TimeoutException): - notice_text = "图标素材下载过程中请求超时\n有关详细信息,请查看日志" - elif isinstance(result, Exception): - notice_text = "图标素材下载过程中发生异常\n有关详细信息,请查看日志" - break - try: - await message.edit_text(notice_text) - except RetryAfter as e: - await asyncio.sleep(e.retry_after + 0.1) - await message.edit_text(notice_text) - except Exception as e: - logger.debug(e) - - logger.info("图标素材下载完成") - return the_time.value - - -def _parse_honey_impact_source(source: bytes) -> MaterialsData: - """ - ## honeyimpact 的源码格式: - ```html -
- - - 秘境名 -
- - - -
-
-
-
-
-
-
- - - - - -
- - 角色名 -
- -
- -
- ``` - """ - honey_item_url_map: Dict[str, str] = { # 这个变量可以静态化,不过考虑到这个函数三天调用一次,懒得改了 - typing.cast(str, honey_url): typing.cast(str, honey_id) - for honey_id, [honey_url, _, _] in HONEY_DATA["material"].items() - } - calendar = bs4.BeautifulSoup(source, "lxml").select_one(".calendar_day_wrap") - if calendar is None: - return MaterialsData() # 多半是格式错误或者网页数据有误 - everyday_materials: List[Dict[str, "AreaDailyMaterialsData"]] = [{} for _ in range(7)] - current_country: str = "" - for element in calendar.find_all(recursive=False): - element: bs4.Tag - if element.name == "span": # 找到代表秘境的 span - domain_name = next(iter(element)).text # 第一个孩子节点的 text - current_country = DOMAIN_AREA_MAP[domain_name] # 后续处理 a 列表也会用到这个 current_country - materials_type = f"{DOMAIN_TYPE_MAP[domain_name]}_materials" - for div in element.find_all("div", recursive=False): # 7 个 div 对应的是一周中的每一天 - div: bs4.Tag - weekday = int(div.attrs["data-days"]) # data-days 是一周中的第几天(周一 0,周日 6) - if current_country not in everyday_materials[weekday]: - everyday_materials[weekday][current_country] = AreaDailyMaterialsData() - materials: List[str] = getattr(everyday_materials[weekday][current_country], materials_type) - for a in div.find_all("a", recursive=False): # 当天能刷的所有素材在 a 列表中 - a: bs4.Tag - href = a.attrs["href"] # 素材 ID 在 href 中 - honey_url = path.dirname(href).removeprefix("/") - materials.append(honey_item_url_map[honey_url]) - if element.name == "a": - # country_name 是从上面的 span 继承下来的,下面的 item 对应的是角色或者武器 - # element 的第一个 child,也就是 div.calendar_pic_wrap - calendar_pic_wrap = typing.cast(bs4.Tag, next(iter(element))) # element 的第一个孩子 - item_name_span = calendar_pic_wrap.select_one("span") - if item_name_span is None or item_name_span.text.strip() == "旅行者": - continue # 因为旅行者的天赋计算比较复杂,不做旅行者的天赋计算 - href = element.attrs["href"] # Item ID 在 href 中 - item_is_weapon = href.startswith("/i_n") - # 角色 ID 前缀固定 10000,但是 honey impact 替换成了角色名,剩余部分的数字是真正的 Item ID 组成部分 - item_id = f"{'' if item_is_weapon else '10000'}{''.join(filter(str.isdigit, href))}" - for weekday in map(int, calendar_pic_wrap.attrs["data-days"]): # data-days 中存的是星期几可以刷素材 - ascendable_items = everyday_materials[weekday][current_country] - ascendable_items = ascendable_items.weapon if item_is_weapon else ascendable_items.avatar - ascendable_items.append(item_id) - return MaterialsData(__root__=everyday_materials) - - -class FragileGenshinClient: - def __init__(self, client: Optional["GenshinClient"]): - self.client = client - self._damaged = False - - @property - def damaged(self): - return self._damaged or self.client is None - - @damaged.setter - def damaged(self, damaged: bool): - self._damaged = damaged - - -class ItemData(BaseModel): - id: str # ID - name: str # 名称 - rarity: int # 星级 - icon: str # 图标 - level: Optional[int] = None # 等级 - constellation: Optional[int] = None # 命座 - skills: Optional[List[int]] = None # 天赋等级 - gid: Optional[int] = None # 角色在 genshin.py 里的 ID - refinement: Optional[int] = None # 精炼度 - c_path: Optional[str] = None # 武器使用者图标 - origin: Optional[Character] = None # 原始数据 - - -class AreaData(BaseModel): - name: str # 区域名 - material_name: str # 区域的材料系列名 - materials: List[ItemData] = [] # 区域材料 - items: Iterable[ItemData] = [] # 可培养的角色或武器 - - -class RenderData(BaseModel): - title: str # 页面标题,主要用于显示星期几 - time: str # 页面时间 - uid: Optional[str] = None # 用户UID - character: List[AreaData] = [] # 角色数据 - weapon: List[AreaData] = [] # 武器数据 - - def __getitem__(self, item): - return self.__getattribute__(item) - - -class UserOwned(BaseModel): - avatar: Dict[str, ItemData] = {} - """角色 ID 到角色对象的映射""" - weapon: Dict[str, List[ItemData]] = {} - """用户同时可以拥有多把同名武器,因此是 ID 到 List 的映射""" - - -class AreaDailyMaterialsData(BaseModel): - """ - AreaDailyMaterialsData 储存某一天某个国家所有可以刷的突破素材以及可以突破的角色和武器 - 对应 /daily_material 命令返回的图中一个国家横向这一整条的信息 - """ - - avatar_materials: List[str] = [] - """ - avatar_materials 是当日该国所有可以刷的精通和炼武素材的 ID 列表 - 举个例子:稻妻周三可以刷「天光」系列材料 - (不用蒙德璃月举例是因为它们每天的角色武器太多了,等稻妻多了再换) - 那么 avatar_materials 将会包括 - - 104326 「天光」的教导 - - 104327 「天光」的指引 - - 104328 「天光」的哲学 - """ - avatar: List[str] = [] - """ - avatar 是排除旅行者后该国当日可以突破天赋的角色 ID 列表 - 举个例子:稻妻周三可以刷「天光」系列精通素材 - 需要用到「天光」系列的角色有 - - 10000052 雷电将军 - - 10000053 早柚 - - 10000055 五郎 - - 10000058 八重神子 - """ - weapon_materials: List[str] = [] - """ - weapon_materials 是当日该国所有可以刷的炼武素材的 ID 列表 - 举个例子:稻妻周三可以刷今昔剧画系列材料 - 那么 weapon_materials 将会包括 - - 114033 今昔剧画之恶尉 - - 114034 今昔剧画之虎啮 - - 114035 今昔剧画之一角 - - 114036 今昔剧画之鬼人 - """ - weapon: List[str] = [] - """ - weapon 是该国当日可以突破天赋的武器 ID 列表 - 举个例子:稻妻周三可以刷今昔剧画系列炼武素材 - 需要用到今昔剧画系列的武器有 - - 11416 笼钓瓶一心 - - 13414 喜多院十文字 - - 13415 「渔获」 - - 13416 断浪长鳍 - - 13509 薙草之稻光 - - 14509 神乐之真意 - """ - - -MaterialsData.update_forward_refs() diff --git a/plugins/genshin/gcsim/plugin.py b/plugins/genshin/gcsim/plugin.py deleted file mode 100644 index fd73150..0000000 --- a/plugins/genshin/gcsim/plugin.py +++ /dev/null @@ -1,331 +0,0 @@ -import copy -from typing import Optional, TYPE_CHECKING, List, Union, Dict, Tuple - -from enkanetwork import EnkaNetworkResponse -from telegram import InlineKeyboardButton, InlineKeyboardMarkup -from telegram.ext import filters - -from core.config import config -from core.dependence.assets import AssetsService -from core.dependence.redisdb import RedisDB -from core.plugin import Plugin, handler -from core.services.players import PlayersService -from gram_core.services.template.services import TemplateService -from gram_core.services.users.services import UserAdminService -from metadata.shortname import roleToName, roleToId -from modules.gcsim.file import PlayerGCSimScripts -from modules.playercards.file import PlayerCardsFile -from plugins.genshin.gcsim.renderer import GCSimResultRenderer -from plugins.genshin.gcsim.runner import GCSimRunner, GCSimFit, GCSimQueueFull, GCSimResult -from plugins.genshin.model.base import CharacterInfo -from plugins.genshin.model.converters.enka import EnkaConverter -from plugins.tools.genshin import PlayerNotFoundError -from utils.log import logger - -if TYPE_CHECKING: - from telegram import Update, Message - from telegram.ext import ContextTypes - -__all__ = ("GCSimPlugin",) - - -async def _no_character_return(user_id: int, uid: int, message: "Message"): - photo = open("resources/img/kitsune.png", "rb") - buttons = [ - [ - InlineKeyboardButton( - "更新面板", - callback_data=f"update_player_card|{user_id}|{uid}", - ) - ] - ] - await message.reply_photo( - photo=photo, - caption="角色列表未找到,请尝试点击下方按钮从 Enka.Network 更新角色列表", - reply_markup=InlineKeyboardMarkup(buttons), - ) - - -class GCSimPlugin(Plugin): - def __init__( - self, - assets_service: AssetsService, - player_service: PlayersService, - template_service: TemplateService, - redis: RedisDB = None, - user_admin_service: UserAdminService = None, - ): - self.player_service = player_service - self.player_cards_file = PlayerCardsFile() - self.player_gcsim_scripts = PlayerGCSimScripts() - self.gcsim_runner = GCSimRunner(redis) - self.gcsim_renderer = GCSimResultRenderer(assets_service, template_service) - self.scripts_per_page = 8 - self.user_admin_service = user_admin_service - - async def initialize(self): - await self.gcsim_runner.initialize() - - def _gen_buttons( - self, user_id: int, uid: int, fits: List[GCSimFit], page: int = 1 - ) -> List[List[InlineKeyboardButton]]: - buttons = [] - for fit in fits[(page - 1) * self.scripts_per_page : page * self.scripts_per_page]: - button = InlineKeyboardButton( - f"{fit.script_key} ({','.join(map(str, fit.characters))})", - callback_data=f"enqueue_gcsim|{user_id}|{uid}|{fit.script_key}", - ) - if not buttons or len(buttons[-1]) >= 1: - buttons.append([]) - buttons[-1].append(button) - buttons.append( - [ - ( - InlineKeyboardButton("上一页", callback_data=f"gcsim_page|{user_id}|{uid}|{page - 1}") - if page > 1 - else InlineKeyboardButton("更新配队", callback_data=f"gcsim_refresh|{user_id}|{uid}") - ), - InlineKeyboardButton( - f"{page}/{int(len(fits) / self.scripts_per_page) + 1}", - callback_data=f"gcsim_unclickable|{user_id}|{uid}|unclickable", - ), - ( - InlineKeyboardButton("下一页", callback_data=f"gcsim_page|{user_id}|{uid}|{page + 1}") - if page < int(len(fits) / self.scripts_per_page) + 1 - else InlineKeyboardButton( - "更新配队", - callback_data=f"gcsim_refresh|{user_id}|{uid}", - ) - ), - ] - ) - return buttons - - @staticmethod - def _filter_fits_by_names(names: List[str], fits: List[GCSimFit]) -> List[GCSimFit]: - if not names: - return fits - return [fit for fit in fits if all(name in [str(i) for i in fit.characters] for name in names)] - - async def _get_uid_names( - self, - user_id: int, - args: List[str], - reply: Optional["Message"], - player_id: int, - offset: int, - ) -> Tuple[Optional[int], List[str]]: - """通过消息获取 uid,优先级:args > reply > self""" - uid, user_id_, names = player_id, user_id, [] - if args: - for i in args: - if i is not None and roleToId(i) is not None: - names.append(roleToName(i)) - if reply: - try: - user_id_ = reply.from_user.id - except AttributeError: - pass - if not uid: - player_info = await self.player_service.get_player(user_id_, offset=offset) - if player_info is not None: - uid = player_info.player_id - if (not uid) and (user_id_ != user_id): - player_info = await self.player_service.get_player(user_id, offset=offset) - if player_info is not None: - uid = player_info.player_id - return uid, names - - @staticmethod - def _fix_skill_level(data: Dict) -> Dict: - for i in data["avatarInfoList"]: - if "proudSkillExtraLevelMap" in i: - del i["proudSkillExtraLevelMap"] - return data - - async def _load_characters(self, uid: Union[int, str]) -> List[CharacterInfo]: - original_data = await self.player_cards_file.load_history_info(uid) - if original_data is None: - return [] - if len(original_data["avatarInfoList"]) == 0: - return [] - enka_response: EnkaNetworkResponse = EnkaNetworkResponse.parse_obj( - self._fix_skill_level(copy.deepcopy(original_data)) - ) - character_infos = [] - for avatar_info in enka_response.characters: - try: - character_infos.append(EnkaConverter.to_character_info(avatar_info)) - except ValueError as e: - logger.error("无法解析 Enka.Network 角色信息: %s\n%s", e, avatar_info.json()) - return character_infos - - @handler.command(command="gcsim", block=False) - async def gcsim(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE"): - user_id = await self.get_real_user_id(update) - message = update.effective_message - args = self.get_args(context) - if not self.gcsim_runner.initialized: - await message.reply_text(f"GCSim 未初始化,请稍候再试或重启{config.notice.bot_name}") - return - if context.user_data.get("overlapping", False): - reply = await message.reply_text(f"旅行者已经有脚本正在运行,请让{config.notice.bot_name}稍微休息一下") - if filters.ChatType.GROUPS.filter(message): - self.add_delete_message_job(reply) - self.add_delete_message_job(message) - return - - uid, offset = self.get_real_uid_or_offset(update) - uid, names = await self._get_uid_names(user_id, args, message.reply_to_message, uid, offset) - self.log_user(update, logger.info, "发出 gcsim 命令 UID[%s] NAMES[%s]", uid, " ".join(names)) - if uid is None: - raise PlayerNotFoundError(user_id) - - character_infos = await self._load_characters(uid) - if not character_infos: - return await _no_character_return(user_id, uid, message) - - fits = await self.gcsim_runner.get_fits(uid) - if not fits: - fits = await self.gcsim_runner.calculate_fits(uid, character_infos) - fits = self._filter_fits_by_names(names, fits) - if not fits: - await message.reply_text("好像没有找到适合旅行者的配队呢,要不更新下面板吧") - return - buttons = self._gen_buttons(user_id, uid, fits) - await message.reply_text( - "请选择 GCSim 脚本", - reply_markup=InlineKeyboardMarkup(buttons), - ) - - @handler.callback_query(pattern=r"^gcsim_refresh\|", block=False) - async def gcsim_refresh(self, update: "Update", _: "ContextTypes.DEFAULT_TYPE") -> None: - callback_query = update.callback_query - user = callback_query.from_user - message = callback_query.message - - user_id, uid = map(int, callback_query.data.split("|")[1:]) - if user.id != user_id: - await callback_query.answer(text="这不是你的按钮!\n" + config.notice.user_mismatch, show_alert=True) - return - - character_infos = await self._load_characters(uid) - if not character_infos: - return await _no_character_return(user.id, uid, message) - - await self.gcsim_runner.remove_fits(uid) - fits = await self.gcsim_runner.calculate_fits(uid, character_infos) - if not fits: - await callback_query.edit_message_text("好像没有找到适合旅行者的配队呢,要不更新下面板吧") - return - buttons = self._gen_buttons(user.id, uid, fits) - await callback_query.edit_message_reply_markup(reply_markup=InlineKeyboardMarkup(buttons)) - - @handler.callback_query(pattern=r"^gcsim_page\|", block=False) - async def gcsim_page(self, update: "Update", _: "ContextTypes.DEFAULT_TYPE") -> None: - callback_query = update.callback_query - user = callback_query.from_user - message = callback_query.message - - user_id, uid, page = map(int, callback_query.data.split("|")[1:]) - if user.id != user_id: - await callback_query.answer(text="这不是你的按钮!\n" + config.notice.user_mismatch, show_alert=True) - return - - fits = await self.gcsim_runner.get_fits(uid) - if not fits: - await callback_query.answer( - text=f"其他数据好像被{config.notice.bot_name}吃掉了,要不重新试试吧", show_alert=True - ) - await message.delete() - return - buttons = self._gen_buttons(user_id, uid, fits, page) - await callback_query.edit_message_reply_markup(reply_markup=InlineKeyboardMarkup(buttons)) - - @handler.callback_query(pattern=r"^gcsim_unclickable\|", block=False) - async def gcsim_unclickable(self, update: "Update", _: "ContextTypes.DEFAULT_TYPE") -> None: - callback_query = update.callback_query - - _, _, _, reason = callback_query.data.split("|") - await callback_query.answer( - text=( - "已经是第一页了!\n" - if reason == "first_page" - else ( - "已经是最后一页了!\n" - if reason == "last_page" - else "这个按钮不可用\n" + config.notice.user_mismatch - ) - ), - show_alert=True, - ) - - @handler.callback_query(pattern=r"^enqueue_gcsim\|", block=False) - async def enqueue_gcsim(self, update: "Update", _: "ContextTypes.DEFAULT_TYPE") -> None: - callback_query = update.callback_query - user = callback_query.from_user - message = callback_query.message - user_id, uid, script_key = callback_query.data.split("|")[1:] - logger.info("用户 %s[%s] GCSim运行请求 || %s", user.full_name, user.id, callback_query.data) - if str(user.id) != user_id: - await callback_query.answer(text="这不是你的按钮!\n" + config.notice.user_mismatch, show_alert=True) - return - - logger.info("用户 %s[%s] enqueue_gcsim 运行请求 || %s", user.full_name, user.id, callback_query.data) - character_infos = await self._load_characters(uid) - if not character_infos: - return await _no_character_return(user.id, uid, message) - - await callback_query.edit_message_text(f"GCSim {script_key} 运行中...", reply_markup=InlineKeyboardMarkup([])) - results = [] - callback_task = self._callback(update, results, character_infos) - priority = 1 if await self.user_admin_service.is_admin(user.id) else 2 - try: - await self.gcsim_runner.run(user_id, uid, script_key, character_infos, results, callback_task, priority) - except GCSimQueueFull: - await callback_query.edit_message_text(f"{config.notice.bot_name}任务过多忙碌中,请稍后再试") - return - - async def _callback( - self, update: "Update", results: List[GCSimResult], character_infos: List[CharacterInfo] - ) -> None: - result = results[0] - callback_query = update.callback_query - message = callback_query.message - _, uid, script_key = callback_query.data.split("|")[1:] - msg_to_reply = message - if message.reply_to_message: - msg_to_reply = message.reply_to_message - if result.error: - await callback_query.edit_message_text(result.error) - else: - await callback_query.edit_message_text(f"GCSim {result.script_key} 运行完成") - if result.file_id: - await msg_to_reply.reply_photo(result.file_id, caption=f"GCSim {script_key} 运行结果") - self.add_delete_message_job(message, delay=1) - return - - result_path = self.player_gcsim_scripts.get_result_path(uid, script_key) - if not result_path.exists(): - await callback_query.answer( - text=f"运行结果似乎在提瓦特之外,{config.notice.bot_name}找不到了", show_alert=True - ) - return - if result.script is None: - await callback_query.answer(text=f"脚本似乎在提瓦特之外,{config.notice.bot_name}找不到了", show_alert=True) - return - - result_ = await self.gcsim_renderer.prepare_result(result_path, result.script, character_infos) - if not result_: - await callback_query.answer(text=f"在准备运行结果时{config.notice.bot_name}出问题了", show_alert=True) - return - - render_result = await self.gcsim_renderer.render(script_key, result_) - reply = await render_result.reply_photo( - msg_to_reply, - filename=f"gcsim_{uid}_{script_key}.png", - caption=f"GCSim {script_key} 运行结果", - ) - self.add_delete_message_job(message, delay=1) - if reply and reply.photo: - await self.gcsim_runner.cache.set_cache(uid, hash(str(result.script)), reply.photo[0].file_id) diff --git a/plugins/genshin/gcsim/renderer.py b/plugins/genshin/gcsim/renderer.py deleted file mode 100644 index 75bc54f..0000000 --- a/plugins/genshin/gcsim/renderer.py +++ /dev/null @@ -1,141 +0,0 @@ -import json -from pathlib import Path -from typing import Optional, List, TYPE_CHECKING - -from core.dependence.assets import AssetsService -from gram_core.services.template.models import RenderResult -from gram_core.services.template.services import TemplateService -from metadata.shortname import idToName, elementToName, elementsToColor -from plugins.genshin.model import GCSim, GCSimCharacterInfo, CharacterInfo -from plugins.genshin.model.converters.gcsim import GCSimConverter - -if TYPE_CHECKING: - from utils.typedefs import StrOrInt - - -class GCSimResultRenderer: - def __init__(self, assets_service: AssetsService, template_service: TemplateService): - self.assets_service = assets_service - self.template_service = template_service - - @staticmethod - def fix_asset_id(asset_id: "StrOrInt") -> "StrOrInt": - if "-" in str(asset_id): - _asset_id = asset_id.split("-")[0] - if _asset_id.isnumeric(): - return int(_asset_id) - return asset_id - - async def prepare_result( - self, result_path: Path, script: GCSim, character_infos: List[CharacterInfo] - ) -> Optional[dict]: - result = json.loads(result_path.read_text(encoding="utf-8")) - characters = {ch.character for ch in character_infos} - result["extra"] = {} - for idx, character_details in enumerate(result["character_details"]): - asset_id, _ = GCSimConverter.to_character(character_details["name"]) - asset_id = self.fix_asset_id(asset_id) - gcsim_character: GCSimCharacterInfo = next( - filter(lambda gc, cn=character_details["name"]: gc.character == cn, script.characters), None - ) - if not gcsim_character: - return None - if character_details["name"] not in result["extra"]: - result["extra"][character_details["name"]] = {} - if GCSimConverter.to_character(gcsim_character.character)[1] in characters: - result["extra"][character_details["name"]]["owned"] = True - else: - result["extra"][character_details["name"]]["owned"] = False - - result["extra"][character_details["name"]]["icon"] = ( - await self.assets_service.avatar(asset_id).icon() - ).as_uri() - result["extra"][character_details["name"]]["rarity"] = self.assets_service.avatar(asset_id).enka.rarity - result["extra"][character_details["name"]]["constellation"] = gcsim_character.constellation - - if "character_dps" not in result["extra"]: - result["extra"]["character_dps"] = [] - result["extra"]["character_dps"].append( - {"value": result["statistics"]["character_dps"][idx]["mean"], "name": idToName(asset_id)} - ) - result["extra"]["element_dps"] = [ - {"value": data["mean"], "name": elementToName(elem), "itemStyle": {"color": elementsToColor[elem]}} - for elem, data in result["statistics"]["element_dps"].items() - ] - result["extra"]["damage"] = { - "xAxis": [i * 0.5 for i in range(len(result["statistics"]["damage_buckets"]["buckets"]))], - "series": [ - { - "data": [bucket["mean"] for bucket in result["statistics"]["damage_buckets"]["buckets"]], - "type": "line", - "symbol": "none", - "color": "#66ccff", - "name": "平均伤害", - }, - { - "data": [bucket["min"] for bucket in result["statistics"]["damage_buckets"]["buckets"]], - "type": "line", - "lineStyle": { - "opacity": 0, - }, - "stack": "area", - "symbol": "none", - }, - { - "data": [ - max(0, bucket["mean"] - bucket["sd"]) - for bucket in result["statistics"]["damage_buckets"]["buckets"] - ], - "type": "line", - "lineStyle": {"opacity": 0}, - "stack": "cofidence-band", - "symbol": "none", - }, - { - "data": [ - min(bucket["mean"], bucket["sd"]) + bucket["sd"] - for bucket in result["statistics"]["damage_buckets"]["buckets"] - ], - "type": "line", - "lineStyle": { - "opacity": 0, - }, - "areaStyle": { - "opacity": 0.5, - "color": "#4c9bd4", - }, - "stack": "cofidence-band", - "symbol": "none", - "color": "#4c9bd4", - "name": "标准差", - }, - { - "data": [ - bucket["max"] - bucket["min"] for bucket in result["statistics"]["damage_buckets"]["buckets"] - ], - "type": "line", - "lineStyle": { - "opacity": 0, - }, - "areaStyle": { - "opacity": 0.25, - "color": "#a5cde9", - }, - "stack": "area", - "symbol": "none", - "color": "#a5cde9", - "name": "极值", - }, - ], - } - - return result - - async def render(self, script_key: str, data: dict) -> RenderResult: - return await self.template_service.render( - "genshin/gcsim/result.jinja2", - {"script_key": script_key, **data}, - full_page=True, - query_selector="body > div", - ttl=7 * 24 * 60 * 60, - ) diff --git a/plugins/genshin/gcsim/runner.py b/plugins/genshin/gcsim/runner.py deleted file mode 100644 index d503bd6..0000000 --- a/plugins/genshin/gcsim/runner.py +++ /dev/null @@ -1,273 +0,0 @@ -import asyncio -import multiprocessing -import platform -import time -from dataclasses import dataclass -from pathlib import Path -from queue import PriorityQueue -from typing import Optional, Dict, List, Union, TYPE_CHECKING, Tuple, Coroutine, Any - -import gcsim_pypi -from pydantic import BaseModel - -from gram_core.config import config -from metadata.shortname import idToName -from modules.apihelper.client.components.remote import Remote -from modules.gcsim.cache import GCSimCache -from modules.gcsim.file import PlayerGCSimScripts -from plugins.genshin.model.base import CharacterInfo, Character -from plugins.genshin.model.converters.gcsim import GCSimConverter -from plugins.genshin.model.gcsim import GCSim, GCSimCharacter -from utils.const import DATA_DIR -from utils.log import logger - -if TYPE_CHECKING: - from core.dependence.redisdb import RedisDB - -GCSIM_SCRIPTS_PATH = DATA_DIR / "gcsim" / "scripts" -GCSIM_SCRIPTS_PATH.mkdir(parents=True, exist_ok=True) - - -class FitCharacter(BaseModel): - id: int - name: str - gcsim: GCSimCharacter - character: Character - - def __str__(self): - return self.name - - -class GCSimFit(BaseModel): - script_key: str - fit_count: int - characters: List[FitCharacter] - total_levels: int - total_weapon_levels: int - - -@dataclass -class GCSimResult: - error: Optional[str] - user_id: str - uid: str - script_key: str - script: Optional[GCSim] = None - file_id: Optional[str] = None - - -def _get_gcsim_bin_name() -> str: - if platform.system() == "Windows": - return "gcsim.exe" - bin_name = "gcsim" - if platform.system() == "Darwin": - bin_name += ".darwin" - if platform.machine() == "arm64": - bin_name += ".arm64" - return bin_name - - -def _get_limit_command() -> str: - if platform.system() == "Linux": - return "ulimit -m 1000000 && ulimit -v 1000000 && timeout 120 " - return "" - - -class GCSimRunnerTask: - def __init__(self, task: Coroutine[Any, Any, None]): - self.task = task - - def __lt__(self, other: "GCSimRunnerTask") -> bool: - return False - - async def run(self) -> None: - await self.task - - -class GCSimQueueFull(Exception): - pass - - -class GCSimRunner: - def __init__(self, client: "RedisDB"): - self.initialized = False - self.bin_path = None - self.player_gcsim_scripts = PlayerGCSimScripts() - self.gcsim_version: Optional[str] = None - self.scripts: Dict[str, GCSim] = {} - max_concurrent_gcsim = multiprocessing.cpu_count() - self.sema = asyncio.BoundedSemaphore(max_concurrent_gcsim) - self.queue_size = 21 - self.queue: PriorityQueue[List[int, GCSimRunnerTask]] = PriorityQueue(maxsize=self.queue_size) - self.cache = GCSimCache(client) - - @staticmethod - def check_gcsim_script(name: str, script: str) -> Optional[GCSim]: - try: - return GCSimConverter.from_gcsim_script(script) - except ValueError as e: - logger.error("无法解析 GCSim 脚本 %s: %s", name, e) - return None - - async def refresh(self): - self.player_gcsim_scripts.clear_fits() - self.scripts.clear() - new_scripts = await Remote.get_gcsim_scripts() - for name, text in new_scripts.items(): - if script := self.check_gcsim_script(name, text): - self.scripts[name] = script - for path in GCSIM_SCRIPTS_PATH.iterdir(): - if path.is_file(): - with open(path, "r", encoding="utf-8") as f: - try: - if script := self.check_gcsim_script(path.name, f.read()): - self.scripts[path.stem] = script - except UnicodeError as e: - logger.error("无法读取 GCSim 脚本 %s: %s", path.name, e) - - async def initialize(self): - gcsim_pypi_path = Path(gcsim_pypi.__file__).parent - - self.bin_path = gcsim_pypi_path.joinpath("bin").joinpath(_get_gcsim_bin_name()) - - process = await asyncio.create_subprocess_exec( - self.bin_path, "-version", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE - ) - - stdout, stderr = await process.communicate() - - if process.returncode == 0: - self.gcsim_version = stdout.decode().splitlines()[0] - logger.debug("GCSim version: %s", self.gcsim_version) - else: - logger.error("GCSim 运行时出错: %s", stderr.decode()) - - now = time.time() - await self.refresh() - logger.debug("加载 %d GCSim 脚本耗时 %.2f 秒", len(self.scripts), time.time() - now) - self.initialized = True - - @staticmethod - async def _execute_queue( - gcsim_task: Coroutine[Any, Any, GCSimResult], - results: List[GCSimResult], - callback_task: Coroutine[Any, Any, None], - ) -> None: - data = await gcsim_task - results.append(data) - await callback_task - - async def _execute_gcsim( - self, - user_id: str, - uid: str, - script_key: str, - added_time: float, - character_infos: List[CharacterInfo], - ) -> GCSimResult: - script = self.scripts.get(script_key) - if script is None: - return GCSimResult(error="未找到脚本", user_id=user_id, uid=uid, script_key=script_key) - try: - merged_script = GCSimConverter.merge_character_infos(script.copy(), character_infos) - except ValueError: - return GCSimResult(error="无法合并角色信息", user_id=user_id, uid=uid, script_key=script_key) - if not config.debug: - if file_id := await self.cache.get_cache(uid, hash(str(merged_script))): - return GCSimResult(error=None, user_id=user_id, uid=uid, script_key=script_key, file_id=file_id) - await self.player_gcsim_scripts.write_script(uid, script_key, str(merged_script)) - limit = _get_limit_command() - command = [ - self.bin_path, - "-c", - self.player_gcsim_scripts.get_script_path(uid, script_key).absolute().as_posix(), - "-out", - self.player_gcsim_scripts.get_result_path(uid, script_key).absolute().as_posix(), - ] - process = await asyncio.create_subprocess_shell( - limit + " ".join([str(i) for i in command]), - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - stdout, stderr = await process.communicate() - logger.debug("GCSim 脚本 (%s|%s|%s) 用时 %.2fs", user_id, uid, script_key, time.time() - added_time) - error = None - if stderr: - error = stderr.decode()[:500] - if "out of memory" in error: - error = "超出内存限制" - if process.returncode == 124: - error = "超出运行时间限制" - if error: - logger.error("GCSim 脚本 (%s|%s|%s) 错误: %s", user_id, uid, script_key, error) - return GCSimResult(error=error, user_id=user_id, uid=uid, script_key=script_key, script=merged_script) - if stdout: - logger.info("GCSim 脚本 (%s|%s|%s) 运行完成", user_id, uid, script_key) - logger.debug("GCSim 脚本 (%s|%s|%s) 输出: %s", user_id, uid, script_key, stdout.decode()) - return GCSimResult(error=None, user_id=user_id, uid=uid, script_key=script_key, script=merged_script) - return GCSimResult( - error="No output", - user_id=user_id, - uid=uid, - script_key=script_key, - script=merged_script, - ) - - async def run( - self, - user_id: str, - uid: str, - script_key: str, - character_infos: List[CharacterInfo], - results: List[GCSimResult], - callback_task: Coroutine[Any, Any, None], - priority: int = 2, - ) -> None: - start_time = time.time() - gcsim_task = self._execute_gcsim(user_id, uid, script_key, start_time, character_infos) - queue_task = GCSimRunnerTask(self._execute_queue(gcsim_task, results, callback_task)) - if priority == 2 and self.queue.qsize() >= (self.queue_size - 1): - raise GCSimQueueFull() - if self.queue.full(): - raise GCSimQueueFull() - self.queue.put([priority, queue_task]) - async with self.sema: - if not self.queue.empty(): - _, task = self.queue.get() - await task.run() - - async def calculate_fits(self, uid: Union[int, str], character_infos: List[CharacterInfo]) -> List[GCSimFit]: - fits = [] - for key, script in self.scripts.items(): - # 空和莹会被认为是两个角色 - fit_characters: List[Tuple[CharacterInfo, GCSimCharacter]] = [] - for ch in character_infos: - gcsim_character = GCSimConverter.from_character(ch.character) - if gcsim_character in [c.character for c in script.characters]: - fit_characters.append((ch, gcsim_character)) - if fit_characters: - fits.append( - GCSimFit( - script_key=key, - characters=[ - FitCharacter(id=ch[0].id, name=idToName(ch[0].id), gcsim=ch[1], character=ch[0].character) - for ch in fit_characters - ], - fit_count=len(fit_characters), - total_levels=sum(ch.level for ch in script.characters), - total_weapon_levels=sum(ch.weapon_info.level for ch in script.characters), - ) - ) - fits = sorted( - fits, - key=lambda x: (x.fit_count, x.total_levels, x.total_weapon_levels), - reverse=True, - ) - await self.player_gcsim_scripts.write_fits(uid, [fit.dict() for fit in fits]) - return fits - - async def get_fits(self, uid: Union[int, str]) -> List[GCSimFit]: - return [GCSimFit(**fit) for fit in self.player_gcsim_scripts.get_fits(uid)] - - async def remove_fits(self, uid: Union[int, str]) -> None: - self.player_gcsim_scripts.remove_fits(uid) diff --git a/plugins/genshin/hilichurls.py b/plugins/genshin/hilichurls.py deleted file mode 100644 index 87418f4..0000000 --- a/plugins/genshin/hilichurls.py +++ /dev/null @@ -1,51 +0,0 @@ -from typing import Dict - -from aiofiles import open as async_open -from telegram import Update -from telegram.ext import CallbackContext, filters - -from core.plugin import Plugin, handler -from utils.const import RESOURCE_DIR -from utils.log import logger - -try: - import ujson as jsonlib - -except ImportError: - import json as jsonlib - -__all__ = ("HilichurlsPlugin",) - - -class HilichurlsPlugin(Plugin): - """丘丘语字典.""" - - hilichurls_dictionary: Dict[str, str] - - async def initialize(self) -> None: - """加载数据文件.数据整理自 https://wiki.biligame.com/ys By @zhxycn.""" - async with async_open(RESOURCE_DIR / "json/hilichurls_dictionary.json", encoding="utf-8") as file: - self.hilichurls_dictionary = jsonlib.loads(await file.read()) - - @handler.command(command="hilichurls", block=False) - async def command_start(self, update: Update, context: CallbackContext) -> None: - message = update.effective_message - args = self.get_args(context) - if len(args) >= 1: - msg = args[0] - else: - reply_message = await message.reply_text("请输入要查询的丘丘语。") - if filters.ChatType.GROUPS.filter(reply_message): - self.add_delete_message_job(message) - self.add_delete_message_job(reply_message) - return - search = str.casefold(msg) # 忽略大小写以方便查询 - if search not in self.hilichurls_dictionary: - reply_message = await message.reply_text(f"在丘丘语字典中未找到 {msg}。") - if filters.ChatType.GROUPS.filter(reply_message): - self.add_delete_message_job(message) - self.add_delete_message_job(reply_message) - return - self.log_user(update, logger.info, "查询今日角色生日列表 查询丘丘语字典命令请求 || 参数 %s", msg) - result = self.hilichurls_dictionary[f"{search}"] - await message.reply_markdown_v2(f"丘丘语: `{search}`\n\n`{result}`") diff --git a/plugins/genshin/ledger.py b/plugins/genshin/ledger.py deleted file mode 100644 index 54138d2..0000000 --- a/plugins/genshin/ledger.py +++ /dev/null @@ -1,353 +0,0 @@ -import math -import os -import re -from datetime import datetime, timedelta -from typing import TYPE_CHECKING, List, Tuple, Optional - -from simnet.errors import DataNotPublic, BadRequest as SimnetBadRequest -from telegram import InlineKeyboardMarkup, InlineKeyboardButton -from telegram.constants import ChatAction -from telegram.ext import filters - -from core.plugin import Plugin, handler -from core.services.cookies import CookiesService -from core.services.history_data.models import HistoryDataLedger -from core.services.history_data.services import HistoryDataLedgerServices -from core.services.template.models import RenderResult -from core.services.template.services import TemplateService -from gram_core.config import config -from gram_core.dependence.redisdb import RedisDB -from gram_core.plugin.methods.inline_use_data import IInlineUseData -from plugins.tools.genshin import GenshinHelper -from utils.enkanetwork import RedisCache -from utils.log import logger -from utils.uid import mask_number - -if TYPE_CHECKING: - from telegram import Update - from telegram.ext import ContextTypes - from simnet import GenshinClient - from simnet.models.genshin.diary import Diary - -__all__ = ("LedgerPlugin",) - - -class LedgerPlugin(Plugin): - """旅行札记查询""" - - def __init__( - self, - helper: GenshinHelper, - cookies_service: CookiesService, - template_service: TemplateService, - history_data_ledger: HistoryDataLedgerServices, - redis: RedisDB, - ): - self.template_service = template_service - self.cookies_service = cookies_service - self.current_dir = os.getcwd() - self.helper = helper - self.history_data_ledger = history_data_ledger - self.cache = RedisCache(redis.client, key="plugin:ledger:history") - self.kitsune = None - - async def _start_get_ledger(self, client: "GenshinClient", month=None) -> RenderResult: - diary_info = await client.get_genshin_diary(client.player_id, month=month) - if month: - await self.save_ledger_data(self.history_data_ledger, client.player_id, diary_info) - return await self._start_get_ledger_render(client.player_id, diary_info) - - async def _start_get_ledger_render(self, uid: int, diary_info: "Diary") -> RenderResult: - color = ["#73a9c6", "#d56565", "#70b2b4", "#bd9a5a", "#739970", "#7a6da7", "#597ea0"] - categories = [ - { - "id": i.id, - "name": i.name, - "color": color[i.id % len(color)], - "amount": i.amount, - "percentage": i.percentage, - } - for i in diary_info.month_data.categories - ] - color = [i["color"] for i in categories] - - def format_amount(amount: int) -> str: - return f"{round(amount / 10000, 2)}w" if amount >= 10000 else amount - - ledger_data = { - "uid": mask_number(uid), - "day": diary_info.month, - "current_primogems": format_amount(diary_info.month_data.current_primogems), - "gacha": int(diary_info.month_data.current_primogems / 160), - "current_mora": format_amount(diary_info.month_data.current_mora), - "last_primogems": format_amount(diary_info.month_data.last_primogems), - "last_gacha": int(diary_info.month_data.last_primogems / 160), - "last_mora": format_amount(diary_info.month_data.last_mora), - "categories": categories, - "color": color, - } - render_result = await self.template_service.render( - "genshin/ledger/ledger.jinja2", ledger_data, {"width": 580, "height": 610} - ) - return render_result - - @handler.command(command="ledger", cookie=True, block=False) - @handler.message(filters=filters.Regex("^旅行札记查询(.*)"), block=False) - async def command_start(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> None: - user_id = await self.get_real_user_id(update) - uid, offset = self.get_real_uid_or_offset(update) - message = update.effective_message - - now = datetime.now() - now_time = (now - timedelta(days=1)) if now.day == 1 and now.hour <= 4 else now - month = now_time.month - try: - args = self.get_args(context) - if len(args) >= 1: - month = args[0].replace("月", "") - if re_data := re.findall(r"\d+", str(month)): - month = int(re_data[0]) - else: - num_dict = {"一": 1, "二": 2, "三": 3, "四": 4, "五": 5, "六": 6, "七": 7, "八": 8, "九": 9, "十": 10} - month = sum(num_dict.get(i, 0) for i in str(month)) - # check right - allow_month = [now_time.month] - - last_month = now_time.replace(day=1) - timedelta(days=1) - allow_month.append(last_month.month) - - last_month = last_month.replace(day=1) - timedelta(days=1) - allow_month.append(last_month.month) - - if month not in allow_month and isinstance(month, int): - raise IndexError - except IndexError: - reply_message = await message.reply_text("仅可查询最新三月的数据,请重新输入") - if filters.ChatType.GROUPS.filter(message): - self.add_delete_message_job(reply_message, delay=30) - self.add_delete_message_job(message, delay=30) - return - self.log_user(update, logger.info, "查询旅行札记") - await message.reply_chat_action(ChatAction.TYPING) - try: - async with self.helper.genshin(user_id, player_id=uid, offset=offset) as client: - render_result = await self._start_get_ledger(client, month) - except DataNotPublic: - reply_message = await message.reply_text( - "查询失败惹,可能是旅行札记功能被禁用了?请先通过米游社或者 hoyolab 获取一次旅行札记后重试。" - ) - if filters.ChatType.GROUPS.filter(message): - self.add_delete_message_job(reply_message, delay=30) - self.add_delete_message_job(message, delay=30) - return - except SimnetBadRequest as exc: - if exc.ret_code == -120: - await message.reply_text("当前角色冒险等阶不足,暂时无法获取信息") - return - raise exc - await message.reply_chat_action(ChatAction.UPLOAD_PHOTO) - await render_result.reply_photo(message, filename=f"{client.player_id}.png") - - @staticmethod - async def save_ledger_data( - history_data_ledger: "HistoryDataLedgerServices", uid: int, ledger_data: "Diary" - ) -> bool: - month = int((ledger_data.date or datetime.now().strftime("%Y-%m-%d")).split("-")[1]) - if month == ledger_data.month: - return False - model = history_data_ledger.create(uid, ledger_data) - old_data = await history_data_ledger.get_by_user_id_data_id(uid, model.data_id) - if not old_data: - await history_data_ledger.add(model) - return True - return False - - async def get_ledger_data(self, uid: int): - return await self.history_data_ledger.get_by_user_id(uid) - - @staticmethod - def get_season_data_name(data: "HistoryDataLedger") -> str: - return f"{data.diary_data.data_id}" - - async def get_session_button_data(self, user_id: int, uid: int, force: bool = False): - redis = await self.cache.get(str(uid)) - if redis and not force: - return redis["buttons"] - data = await self.get_ledger_data(uid) - data.sort(key=lambda x: x.data_id, reverse=True) - abyss_data = [HistoryDataLedger.from_data(i) for i in data] - buttons = [ - { - "name": LedgerPlugin.get_season_data_name(abyss_data[idx]), - "value": f"get_ledger_history|{user_id}|{uid}|{value.id}", - } - for idx, value in enumerate(data) - ] - await self.cache.set(str(uid), {"buttons": buttons}) - return buttons - - async def gen_season_button( - self, - user_id: int, - uid: int, - page: int = 1, - ) -> List[List[InlineKeyboardButton]]: - """生成按钮""" - data = await self.get_session_button_data(user_id, uid) - if not data: - return [] - buttons = [ - InlineKeyboardButton( - value["name"], - callback_data=value["value"], - ) - for value in data - ] - all_buttons = [buttons[i : i + 3] for i in range(0, len(buttons), 3)] - send_buttons = all_buttons[(page - 1) * 5 : page * 5] - last_page = page - 1 if page > 1 else 0 - all_page = math.ceil(len(all_buttons) / 5) - next_page = page + 1 if page < all_page and all_page > 1 else 0 - last_button = [] - if last_page: - last_button.append( - InlineKeyboardButton( - "<< 上一页", - callback_data=f"get_ledger_history|{user_id}|{uid}|p_{last_page}", - ) - ) - if last_page or next_page: - last_button.append( - InlineKeyboardButton( - f"{page}/{all_page}", - callback_data=f"get_ledger_history|{user_id}|{uid}|empty_data", - ) - ) - if next_page: - last_button.append( - InlineKeyboardButton( - "下一页 >>", - callback_data=f"get_ledger_history|{user_id}|{uid}|p_{next_page}", - ) - ) - if last_button: - send_buttons.append(last_button) - return send_buttons - - @handler.command("ledger_history", block=False) - @handler.message(filters.Regex(r"^旅行札记历史数据"), block=False) - async def ledger_history_command_start(self, update: "Update", _: "ContextTypes.DEFAULT_TYPE") -> None: - user_id = await self.get_real_user_id(update) - uid, offset = self.get_real_uid_or_offset(update) - message = update.effective_message - self.log_user(update, logger.info, "查询旅行札记历史数据") - - async with self.helper.genshin(user_id, player_id=uid, offset=offset) as client: - await self.get_session_button_data(user_id, client.player_id, force=True) - buttons = await self.gen_season_button(user_id, client.player_id) - if not buttons: - await message.reply_text("还没有旅行札记历史数据哦~") - return - if isinstance(self.kitsune, str): - photo = self.kitsune - else: - photo = open("resources/img/kitsune.png", "rb") - reply_message = await message.reply_photo( - photo, "请选择要查询的旅行札记历史数据", reply_markup=InlineKeyboardMarkup(buttons) - ) - if reply_message.photo: - self.kitsune = reply_message.photo[-1].file_id - - async def get_ledger_history_page(self, update: "Update", user_id: int, uid: int, result: str): - """翻页处理""" - callback_query = update.callback_query - - self.log_user(update, logger.info, "切换旅行札记历史数据页 page[%s]", result) - page = int(result.split("_")[1]) - async with self.helper.genshin(user_id, player_id=uid) as client: - buttons = await self.gen_season_button(user_id, client.player_id, page) - if not buttons: - await callback_query.answer("还没有旅行札记历史数据哦~", show_alert=True) - await callback_query.edit_message_text("还没有旅行札记历史数据哦~") - return - await callback_query.edit_message_reply_markup(reply_markup=InlineKeyboardMarkup(buttons)) - await callback_query.answer(f"已切换到第 {page} 页", show_alert=False) - - @handler.callback_query(pattern=r"^get_ledger_history\|", block=False) - async def get_ledger_history(self, update: "Update", _: "ContextTypes.DEFAULT_TYPE") -> None: - callback_query = update.callback_query - message = callback_query.message - user = callback_query.from_user - - async def get_ledger_history_callback( - callback_query_data: str, - ) -> Tuple[str, int, int]: - _data = callback_query_data.split("|") - _user_id = int(_data[1]) - _uid = int(_data[2]) - _result = _data[3] - logger.debug( - "callback_query_data函数返回 result[%s] user_id[%s] uid[%s]", - _result, - _user_id, - _uid, - ) - return _result, _user_id, _uid - - result, user_id, uid = await get_ledger_history_callback(callback_query.data) - if user.id != user_id: - await callback_query.answer(text="这不是你的按钮!\n" + config.notice.user_mismatch, show_alert=True) - return - if result == "empty_data": - await callback_query.answer(text="此按钮不可用", show_alert=True) - return - if result.startswith("p_"): - await self.get_ledger_history_page(update, user_id, uid, result) - return - data_id = int(result) - data = await self.history_data_ledger.get_by_id(data_id) - if not data: - await callback_query.answer("数据不存在,请尝试重新发送命令", show_alert=True) - await callback_query.edit_message_text("数据不存在,请尝试重新发送命令~") - return - await callback_query.answer("正在渲染图片中 请稍等 请不要重复点击按钮") - render = await self._start_get_ledger_render(user_id, HistoryDataLedger.from_data(data).diary_data) - await render.edit_media(message) - - async def ledger_use_by_inline(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE"): - 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, "查询旅行札记") - try: - async with self.helper.genshin(user_id, player_id=uid) as client: - render_result = await self._start_get_ledger(client) - except DataNotPublic: - await callback_query.answer( - "查询失败惹,可能是旅行札记功能被禁用了?请先通过米游社或者 hoyolab 获取一次旅行札记后重试。", - show_alert=True, - ) - return - except SimnetBadRequest as exc: - if exc.ret_code == -120: - await callback_query.answer( - "当前角色冒险等阶不足,暂时无法获取信息", - show_alert=True, - ) - return - raise exc - - await render_result.edit_inline_media(callback_query) - - async def get_inline_use_data(self) -> List[Optional[IInlineUseData]]: - return [ - IInlineUseData( - text="当月旅行札记", - hash="ledger", - callback=self.ledger_use_by_inline, - cookie=True, - player=True, - ) - ] diff --git a/plugins/genshin/map.py b/plugins/genshin/map.py deleted file mode 100644 index ff8a40a..0000000 --- a/plugins/genshin/map.py +++ /dev/null @@ -1,197 +0,0 @@ -from io import BytesIO -from typing import Union, Optional, List, Tuple - -from telegram import Update, Message, InputMediaDocument, InputMediaPhoto, InlineKeyboardButton, InlineKeyboardMarkup -from telegram.constants import ChatAction -from telegram.ext import CommandHandler, MessageHandler, filters, CallbackContext - -from core.config import config -from core.dependence.redisdb import RedisDB -from core.handler.callbackqueryhandler import CallbackQueryHandler -from core.plugin import handler, Plugin -from modules.apihelper.client.components.map import MapHelper, MapException -from utils.log import logger - - -class Map(Plugin): - """资源点查询""" - - def __init__(self, redis: RedisDB): - self.cache = redis.client - self.cache_photo_key = "plugin:map:photo:" - self.cache_doc_key = "plugin:map:doc:" - self.map_helper = MapHelper() - self.temp_photo_path = "resources/img/map.png" - self.temp_photo = None - - async def get_photo_cache(self, map_id: Union[str, int], name: str) -> Optional[str]: - if file_id := await self.cache.get(f"{self.cache_photo_key}{map_id}:{name}"): - return file_id.decode("utf-8") - return None - - async def get_doc_cache(self, map_id: Union[str, int], name: str) -> Optional[str]: - if file_id := await self.cache.get(f"{self.cache_doc_key}{map_id}:{name}"): - return file_id.decode("utf-8") - return None - - async def set_photo_cache(self, map_id: Union[str, int], name: str, file_id: str) -> None: - await self.cache.set(f"{self.cache_photo_key}{map_id}:{name}", file_id) - - async def set_doc_cache(self, map_id: Union[str, int], name: str, file_id: str) -> None: - await self.cache.set(f"{self.cache_doc_key}{map_id}:{name}", file_id) - - async def clear_cache(self) -> None: - for i in await self.cache.keys(f"{self.cache_photo_key}*"): - await self.cache.delete(i) - for i in await self.cache.keys(f"{self.cache_doc_key}*"): - await self.cache.delete(i) - - async def edit_media(self, message: Message, map_id: str, name: str) -> None: - caption = self.gen_caption(map_id, name) - if cache := await self.get_photo_cache(map_id, name): - media = InputMediaPhoto(media=cache, caption=caption) - await message.edit_media(media) - return - if cache := await self.get_doc_cache(map_id, name): - media = InputMediaDocument(media=cache, caption=caption) - await message.edit_media(media) - return - data = await self.map_helper.get_map(map_id, name) - if len(data) > (1024 * 1024): - data = BytesIO(data) - data.name = "map.jpg" - media = InputMediaDocument(media=data, caption=caption) - msg = await message.edit_media(media) - await self.set_doc_cache(map_id, name, msg.document.file_id) - else: - media = InputMediaPhoto(media=data, caption=caption) - msg = await message.edit_media(media) - await self.set_photo_cache(map_id, name, msg.photo[0].file_id) - - def get_show_map(self, name: str) -> List[int]: - return [ - idx - for idx, map_id in enumerate(self.map_helper.MAP_ID_LIST) - if self.map_helper.get_label_count(map_id, name) > 0 - ] - - async def gen_map_button( - self, maps: List[int], user_id: Union[str, int], name: str - ) -> List[List[InlineKeyboardButton]]: - return [ - [ - InlineKeyboardButton( - self.map_helper.MAP_NAME_LIST[idx], - callback_data=f"get_map|{user_id}|{self.map_helper.MAP_ID_LIST[idx]}|{name}", - ) - for idx in maps - ] - ] - - async def send_media(self, message: Message, map_id: Union[str, int], name: str) -> None: - caption = self.gen_caption(map_id, name) - if cache := await self.get_photo_cache(map_id, name): - await message.reply_photo(photo=cache, caption=caption) - return - if cache := await self.get_doc_cache(map_id, name): - await message.reply_document(document=cache, caption=caption) - return - try: - data = await self.map_helper.get_map(map_id, name) - except MapException as e: - await message.reply_text(e.message) - return - if len(data) > (1024 * 1024): - data = BytesIO(data) - data.name = "map.jpg" - msg = await message.reply_document(document=data, caption=caption) - await self.set_doc_cache(map_id, name, msg.document.file_id) - else: - msg = await message.reply_photo(photo=data, caption=caption) - await self.set_photo_cache(map_id, name, msg.photo[0].file_id) - - def gen_caption(self, map_id: Union[int, str], name: str) -> str: - count = self.map_helper.get_label_count(map_id, name) - return f"{config.notice.bot_name}一共找到了 {name} 的 {count} 个位置点\n* 数据来源于米游社wiki" - - @handler(CommandHandler, command="map", block=False) - @handler( - MessageHandler, filters=filters.Regex("^(?P.*)(在哪里|在哪|哪里有|哪儿有|哪有|在哪儿)$"), block=False - ) - @handler(MessageHandler, filters=filters.Regex("^(哪里有|哪儿有|哪有)(?P.*)$"), block=False) - async def command_start(self, update: Update, context: CallbackContext): - user_id = await self.get_real_user_id(update) - message = update.effective_message - args = context.args - group_dict = context.match and context.match.groupdict() - resource_name = None - await message.reply_chat_action(ChatAction.TYPING) - if args and len(args) >= 1: - resource_name = args[0] - elif group_dict: - resource_name = group_dict.get("name", None) - if not resource_name: - if group_dict: - return - await message.reply_text("请指定要查找的资源名称。", parse_mode="Markdown") - return - self.log_user(update, logger.info, "使用 map 命令查询了 %s", resource_name) - if resource_name not in self.map_helper.query_map: - # 消息来源于群组中并且无法找到默认不回复即可 - if filters.ChatType.GROUPS.filter(message) and group_dict is not None: - return - await message.reply_text("没有找到该资源。", parse_mode="Markdown") - return - maps = self.get_show_map(resource_name) - if len(maps) == 0: - if filters.ChatType.GROUPS.filter(message) and group_dict is not None: - return - await message.reply_text("没有找到该资源。", parse_mode="Markdown") - return - if len(maps) == 1: - map_id = self.map_helper.MAP_ID_LIST[maps[0]] - await self.send_media(message, map_id, resource_name) - return - buttons = await self.gen_map_button(maps, user_id, resource_name) - if isinstance(self.temp_photo, str): - photo = self.temp_photo - else: - photo = open(self.temp_photo_path, "rb") - reply_message = await message.reply_photo( - photo=photo, caption="请选择你要查询的地图", reply_markup=InlineKeyboardMarkup(buttons) - ) - if reply_message.photo: - self.temp_photo = reply_message.photo[-1].file_id - - @handler(CallbackQueryHandler, pattern=r"^get_map\|", block=False) - async def get_maps(self, update: Update, _: CallbackContext) -> None: - callback_query = update.callback_query - user = callback_query.from_user - message = callback_query.message - - async def get_map_callback(callback_query_data: str) -> Tuple[int, str, str]: - _data = callback_query_data.split("|") - _user_id = int(_data[1]) - _map_id = _data[2] - _name = _data[3] - logger.debug("callback_query_data 函数返回 user_id[%s] map_id[%s] name[%s]", _user_id, _map_id, _name) - return _user_id, _map_id, _name - - user_id, map_id, name = await get_map_callback(callback_query.data) - if user.id != user_id: - await callback_query.answer(text="这不是你的按钮!\n" + config.notice.user_mismatch, show_alert=True) - return - await callback_query.answer(text="正在渲染图片中 请稍等 请不要重复点击按钮", show_alert=False) - try: - await self.edit_media(message, map_id, name) - except MapException as e: - await message.reply_text(e.message) - - @handler.command("refresh_map", admin=True, block=False) - async def refresh_map(self, update: Update, _: CallbackContext): - message = update.effective_message - msg = await message.reply_text("正在刷新地图数据,请耐心等待...") - await self.map_helper.refresh_query_map() - await self.map_helper.refresh_label_count() - await self.clear_cache() - await msg.edit_text("正在刷新地图数据,请耐心等待...\n刷新成功") diff --git a/plugins/genshin/material.py b/plugins/genshin/material.py deleted file mode 100644 index a75f017..0000000 --- a/plugins/genshin/material.py +++ /dev/null @@ -1,239 +0,0 @@ -import re - -from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update -from telegram.constants import ChatAction -from telegram.ext import CallbackContext, CommandHandler, MessageHandler, filters - -from core.dependence.assets import AssetsService -from core.plugin import Plugin, handler -from core.services.template.services import TemplateService -from metadata.genshin import MATERIAL_DATA -from metadata.shortname import roleToName -from modules.apihelper.client.components.remote import Remote -from modules.material.talent import TalentMaterials -from modules.wiki.character import Character -from utils.log import logger - -__all__ = ("MaterialPlugin",) - - -class MaterialPlugin(Plugin): - """角色培养素材查询""" - - KEYBOARD = [ - [ - InlineKeyboardButton( - text="查看角色培养素材列表并查询", switch_inline_query_current_chat="查看角色培养素材列表并查询" - ) - ] - ] - - def __init__( - self, - template_service: TemplateService, - assets_service: AssetsService, - ): - self.roles_material = {} - self.assets_service = assets_service - self.template_service = template_service - - async def initialize(self): - await self._refresh() - - async def _refresh(self): - self.roles_material = await Remote.get_remote_material() - - async def _parse_material(self, data: dict, character_name: str, talent_level: str) -> dict: - data = data["data"] - if character_name not in data.keys(): - return {} - character = self.assets_service.avatar(character_name) - level_up_material = self.assets_service.material(data[character_name]["level_up_materials"]) - ascension_material = self.assets_service.material(data[character_name]["ascension_materials"]) - local_material = self.assets_service.material(data[character_name]["materials"][0]) - enemy_material = self.assets_service.material(data[character_name]["materials"][1]) - level_up_materials = [ - { - "num": 46, - "rarity": MATERIAL_DATA[str(level_up_material.id)]["rank"], - "icon": (await level_up_material.icon()).as_uri(), - "name": data[character_name]["level_up_materials"], - }, - { - "num": 419, - "rarity": 4, - "icon": (await self.assets_service.material(104003).icon()).as_uri(), - "name": "大英雄的经验", - }, - { - "num": 1, - "rarity": 2, - "icon": (await self.assets_service.material(ascension_material.id - 3).icon()).as_uri(), - "name": MATERIAL_DATA[str(ascension_material.id - 3)]["name"], - }, - { - "num": 9, - "rarity": 3, - "icon": (await self.assets_service.material(ascension_material.id - 2).icon()).as_uri(), - "name": MATERIAL_DATA[str(ascension_material.id - 2)]["name"], - }, - { - "num": 9, - "rarity": 4, - "icon": (await self.assets_service.material(str(ascension_material.id - 1)).icon()).as_uri(), - "name": MATERIAL_DATA[str(ascension_material.id - 1)]["name"], - }, - { - "num": 6, - "rarity": 5, - "icon": (await ascension_material.icon()).as_uri(), - "name": MATERIAL_DATA[str(ascension_material.id)]["name"], - }, - { - "num": 168, - "rarity": MATERIAL_DATA[str(local_material.id)]["rank"], - "icon": (await local_material.icon()).as_uri(), - "name": MATERIAL_DATA[str(local_material.id)]["name"], - }, - { - "num": 18, - "rarity": MATERIAL_DATA[str(enemy_material.id)]["rank"], - "icon": (await self.assets_service.material(enemy_material.id).icon()).as_uri(), - "name": MATERIAL_DATA[str(enemy_material.id)]["name"], - }, - { - "num": 30, - "rarity": MATERIAL_DATA[str(enemy_material.id + 1)]["rank"], - "icon": (await self.assets_service.material(enemy_material.id + 1).icon()).as_uri(), - "name": MATERIAL_DATA[str(enemy_material.id + 1)]["name"], - }, - { - "num": 36, - "rarity": MATERIAL_DATA[str(enemy_material.id + 2)]["rank"], - "icon": (await self.assets_service.material(str(enemy_material.id + 2)).icon()).as_uri(), - "name": MATERIAL_DATA[str(enemy_material.id + 2)]["name"], - }, - ] - talent_book = self.assets_service.material(f"「{data[character_name]['talent'][0]}」的教导") - weekly_talent_material = self.assets_service.material(data[character_name]["talent"][1]) - talent_materials = [ - { - "num": 9, - "rarity": MATERIAL_DATA[str(talent_book.id)]["rank"], - "icon": (await self.assets_service.material(talent_book.id).icon()).as_uri(), - "name": MATERIAL_DATA[str(talent_book.id)]["name"], - }, - { - "num": 63, - "rarity": MATERIAL_DATA[str(talent_book.id + 1)]["rank"], - "icon": (await self.assets_service.material(talent_book.id + 1).icon()).as_uri(), - "name": MATERIAL_DATA[str(talent_book.id + 1)]["name"], - }, - { - "num": 114, - "rarity": MATERIAL_DATA[str(talent_book.id + 2)]["rank"], - "icon": (await self.assets_service.material(str(talent_book.id + 2)).icon()).as_uri(), - "name": MATERIAL_DATA[str(talent_book.id + 2)]["name"], - }, - { - "num": 18, - "rarity": MATERIAL_DATA[str(enemy_material.id)]["rank"], - "icon": (await self.assets_service.material(enemy_material.id).icon()).as_uri(), - "name": MATERIAL_DATA[str(enemy_material.id)]["name"], - }, - { - "num": 66, - "rarity": MATERIAL_DATA[str(enemy_material.id + 1)]["rank"], - "icon": (await self.assets_service.material(enemy_material.id + 1).icon()).as_uri(), - "name": MATERIAL_DATA[str(enemy_material.id + 1)]["name"], - }, - { - "num": 93, - "rarity": MATERIAL_DATA[str(enemy_material.id + 2)]["rank"], - "icon": (await self.assets_service.material(str(enemy_material.id + 2)).icon()).as_uri(), - "name": MATERIAL_DATA[str(enemy_material.id + 2)]["name"], - }, - { - "num": 3, - "rarity": 5, - "icon": (await self.assets_service.material(104319).icon()).as_uri(), - "name": "智识之冕", - }, - { - "num": 18, - "rarity": MATERIAL_DATA[str(weekly_talent_material.id)]["rank"], - "icon": (await self.assets_service.material(weekly_talent_material.id).icon()).as_uri(), - "name": MATERIAL_DATA[str(weekly_talent_material.id)]["name"], - }, - ] - - return { - "character": { - "element": character.enka.element.name, - "image": character.enka.images.banner.url, - "name": character_name, - "association": (await Character.get_by_name(character_name)).association.name, - }, - "level_up_materials": level_up_materials, - "talent_materials": talent_materials, - "talent_level": talent_level, - "talent_amount": TalentMaterials(list(map(int, talent_level.split("/")))).cal_materials(), - } - - async def render(self, character_name: str, talent_amount: str): - if not self.roles_material: - await self._refresh() - data = await self._parse_material(self.roles_material, character_name, talent_amount) - if not data: - return - return await self.template_service.render( - "genshin/material/roles_material.jinja2", - data, - {"width": 960, "height": 1460}, - full_page=True, - ttl=7 * 24 * 60 * 60, - ) - - @staticmethod - def _is_valid(string: str): - """ - 判断字符串是否符合`8/9/10`的格式并保证每个数字都在[1,10] - """ - return bool( - re.match(r"^\d+/\d+/\d+$", string) - and all(1 <= int(num) <= 10 for num in string.split("/")) - and string != "1/1/1" - and string != "10/10/10" - ) - - @handler(CommandHandler, command="material", block=False) - @handler(MessageHandler, filters=filters.Regex("^角色培养素材查询(.*)"), block=False) - async def command_start(self, update: Update, context: CallbackContext) -> None: - message = update.effective_message - args = self.get_args(context) - if len(args) >= 1: - character_name = args[0] - material_count = "8/8/8" - if len(args) >= 2 and self._is_valid(args[1]): - material_count = args[1] - else: - reply_message = await message.reply_text( - "请回复你要查询的培养素材的角色名", reply_markup=InlineKeyboardMarkup(self.KEYBOARD) - ) - if filters.ChatType.GROUPS.filter(reply_message): - self.add_delete_message_job(message) - self.add_delete_message_job(reply_message) - return - character_name = roleToName(character_name) - self.log_user(update, logger.info, "查询角色培养素材命令请求 || 参数 %s", character_name) - await message.reply_chat_action(ChatAction.UPLOAD_PHOTO) - result = await self.render(character_name, material_count) - if not result: - reply_message = await message.reply_text( - f"没有找到 {character_name} 的培养素材", reply_markup=InlineKeyboardMarkup(self.KEYBOARD) - ) - if filters.ChatType.GROUPS.filter(reply_message): - self.add_delete_message_job(message) - self.add_delete_message_job(reply_message) - return - await result.reply_photo(message) diff --git a/plugins/genshin/model/__init__.py b/plugins/genshin/model/__init__.py deleted file mode 100644 index e178a1a..0000000 --- a/plugins/genshin/model/__init__.py +++ /dev/null @@ -1,29 +0,0 @@ -from plugins.genshin.model.base import * - -from plugins.genshin.model.gcsim import * - -__all__ = [ - "Digit", - "DigitType", - "Character", - "CharacterInfo", - "CharacterStats", - "Weapon", - "WeaponInfo", - "WeaponType", - "Set", - "Artifact", - "ArtifactAttribute", - "ArtifactAttributeType", - "ArtifactPosition", - "GCSimCharacter", - "GCSimSet", - "GCSimSetInfo", - "GCSimWeapon", - "GCSimWeaponInfo", - "GCSimCharacterStats", - "GCSimCharacterInfo", - "GCSimTarget", - "GCSimEnergySettings", - "GCSim", -] diff --git a/plugins/genshin/model/base.py b/plugins/genshin/model/base.py deleted file mode 100644 index e1730d9..0000000 --- a/plugins/genshin/model/base.py +++ /dev/null @@ -1,198 +0,0 @@ -from decimal import Decimal -from enum import Enum -from typing import Optional, List, NewType - -from pydantic import BaseModel, Field, validator - -# TODO: 考虑自动生成Enum -Character = NewType("Character", str) -Weapon = NewType("Weapon", str) -Set = NewType("Set", str) - - -class DigitType(Enum): - NUMERIC = "numeric" - PERCENT = "percent" - - -class Digit(BaseModel): - type: DigitType - value: Decimal - - -class WeaponType(Enum): - BOW = "bow" - CLAYMORE = "claymore" - CATALYST = "catalyst" - POLEARM = "polearm" - SWORD = "sword" - - -class ArtifactPosition(Enum): - FLOWER = "flower" - PLUME = "plume" - SANDS = "sands" - GOBLET = "goblet" - CIRCLET = "circlet" - - -class ArtifactAttributeType(Enum): - HP = "hp" - ATK = "atk" - DEF = "def" - HP_PERCENT = "hp_percent" - ATK_PERCENT = "atk_percent" - DEF_PERCENT = "def_percent" - ELEMENTAL_MASTERY = "elemental_mastery" - ENERGY_RECHARGE = "energy_recharge" - CRIT_RATE = "crit_rate" - CRIT_DMG = "crit_dmg" - HEALING_BONUS = "healing_bonus" - PYRO_DMG_BONUS = "pyro_dmg_bonus" - HYDRO_DMG_BONUS = "hydro_dmg_bonus" - DENDRO_DMG_BONUS = "dendro_dmg_bonus" - ELECTRO_DMG_BONUS = "electro_dmg_bonus" - ANEMO_DMG_BONUS = "anemo_dmg_bonus" - CRYO_DMG_BONUS = "cryo_dmg_bonus" - GEO_DMG_BONUS = "geo_dmg_bonus" - PHYSICAL_DMG_BONUS = "physical_dmg_bonus" - - -class ArtifactAttribute(BaseModel): - type: ArtifactAttributeType - digit: Digit - - -class WeaponInfo(BaseModel): - id: int = 0 - weapon: Weapon = "" - type: WeaponType - level: int = 0 - max_level: int = 0 - refinement: int = 0 - ascension: int = 0 - - @validator("max_level") - def validate_max_level(cls, v, values): - if v == 0: - return values["level"] - if v < values["level"]: - raise ValueError("max_level must be greater than or equal to level") - return v - - @validator("refinement") - def validate_refinement(cls, v): - if v < 0 or v > 5: - raise ValueError("refinement must be between 1 and 5") - return v - - -class Artifact(BaseModel): - id: int = 0 - set: Set = "" - position: ArtifactPosition - level: int = 0 - rarity: int = 0 - main_attribute: ArtifactAttribute - sub_attributes: List[ArtifactAttribute] = [] - - @validator("level") - def validate_level(cls, v): - if v < 0 or v > 20: - raise ValueError("level must be between 0 and 20") - return v - - @validator("rarity") - def validate_rarity(cls, v): - if v < 0 or v > 5: - raise ValueError("rarity must be between 0 and 5") - return v - - @validator("sub_attributes") - def validate_sub_attributes(cls, v): - if len(v) > 4: - raise ValueError("sub_attributes must not be greater than 4") - return v - - -class CharacterStats(BaseModel): - BASE_HP: Digit = Digit(type=DigitType.NUMERIC, value=Decimal(0)) - HP: Digit = Field(Digit(type=DigitType.NUMERIC, value=Decimal(0)), alias="FIGHT_PROP_HP") - HP_PERCENT: Digit = Field(Digit(type=DigitType.PERCENT, value=Decimal(0)), alias="FIGHT_PROP_HP_PERCENT") - BASE_ATTACK: Digit = Field(Digit(type=DigitType.NUMERIC, value=Decimal(0)), alias="FIGHT_PROP_BASE_ATTACK") - ATTACK: Digit = Field(Digit(type=DigitType.NUMERIC, value=Decimal(0)), alias="FIGHT_PROP_ATTACK") - ATTACK_PERCENT: Digit = Field(Digit(type=DigitType.PERCENT, value=Decimal(0)), alias="FIGHT_PROP_ATTACK_PERCENT") - BASE_DEFENSE: Digit = Field(Digit(type=DigitType.NUMERIC, value=Decimal(0)), alias="FIGHT_PROP_BASE_DEFENSE") - DEFENSE: Digit = Field(Digit(type=DigitType.NUMERIC, value=Decimal(0)), alias="FIGHT_PROP_DEFENSE") - DEFENSE_PERCENT: Digit = Field(Digit(type=DigitType.PERCENT, value=Decimal(0)), alias="FIGHT_PROP_DEFENSE_PERCENT") - ELEMENTAL_MASTERY: Digit = Field( - Digit(type=DigitType.NUMERIC, value=Decimal(0)), alias="FIGHT_PROP_ELEMENT_MASTERY" - ) - - CRIT_RATE: Digit = Field(Digit(type=DigitType.PERCENT, value=Decimal(0)), alias="FIGHT_PROP_CRITICAL") - CRIT_DMG: Digit = Field(Digit(type=DigitType.PERCENT, value=Decimal(0)), alias="FIGHT_PROP_CRITICAL_HURT") - HEALING_BONUS: Digit = Field(Digit(type=DigitType.PERCENT, value=Decimal(0)), alias="FIGHT_PROP_HEAL_ADD") - INCOMING_HEALING_BONUS: Digit = Field( - Digit(type=DigitType.PERCENT, value=Decimal(0)), alias="FIGHT_PROP_HEALED_ADD" - ) - ENERGY_RECHARGE: Digit = Field( - Digit(type=DigitType.PERCENT, value=Decimal(0)), alias="FIGHT_PROP_CHARGE_EFFICIENCY" - ) - CD_REDUCTION: Digit = Field( - Digit(type=DigitType.NUMERIC, value=Decimal(0)), alias="FIGHT_PROP_SKILL_CD_MINUS_RATIO" - ) - SHIELD_STRENGTH: Digit = Field( - Digit(type=DigitType.NUMERIC, value=Decimal(0)), alias="FIGHT_PROP_SHIELD_COST_MINUS_RATIO" - ) - - PYRO_DMG_BONUS: Digit = Field(Digit(type=DigitType.PERCENT, value=Decimal(0)), alias="FIGHT_PROP_FIRE_ADD_HURT") - PYRO_RES: Digit = Field(Digit(type=DigitType.PERCENT, value=Decimal(0)), alias="FIGHT_PROP_FIRE_SUB_HURT") - HYDRO_DMG_BONUS: Digit = Field(Digit(type=DigitType.PERCENT, value=Decimal(0)), alias="FIGHT_PROP_WATER_ADD_HURT") - HYDRO_RES: Digit = Field(Digit(type=DigitType.PERCENT, value=Decimal(0)), alias="FIGHT_PROP_WATER_SUB_HURT") - DENDRO_DMG_BONUS: Digit = Field(Digit(type=DigitType.PERCENT, value=Decimal(0)), alias="FIGHT_PROP_GRASS_ADD_HURT") - DENDRO_RES: Digit = Field(Digit(type=DigitType.PERCENT, value=Decimal(0)), alias="FIGHT_PROP_GRASS_SUB_HURT") - ELECTRO_DMG_BONUS: Digit = Field(Digit(type=DigitType.PERCENT, value=Decimal(0)), alias="FIGHT_PROP_ELEC_ADD_HURT") - ELECTRO_RES: Digit = Field(Digit(type=DigitType.PERCENT, value=Decimal(0)), alias="FIGHT_PROP_ELEC_SUB_HURT") - ANEMO_DMG_BONUS: Digit = Field(Digit(type=DigitType.PERCENT, value=Decimal(0)), alias="FIGHT_PROP_WIND_ADD_HURT") - ANEMO_RES: Digit = Field(Digit(type=DigitType.PERCENT, value=Decimal(0)), alias="FIGHT_PROP_WIND_SUB_HURT") - CRYO_DMG_BONUS: Digit = Field(Digit(type=DigitType.PERCENT, value=Decimal(0)), alias="FIGHT_PROP_ICE_ADD_HURT") - CRYO_RES: Digit = Field(Digit(type=DigitType.PERCENT, value=Decimal(0)), alias="FIGHT_PROP_ICE_SUB_HURT") - GEO_DMG_BONUS: Digit = Field(Digit(type=DigitType.PERCENT, value=Decimal(0)), alias="FIGHT_PROP_ROCK_ADD_HURT") - GEO_RES: Digit = Field(Digit(type=DigitType.PERCENT, value=Decimal(0)), alias="FIGHT_PROP_ROCK_SUB_HURT") - PHYSICAL_DMG_BONUS: Digit = Field( - Digit(type=DigitType.PERCENT, value=Decimal(0)), alias="FIGHT_PROP_PHYSICAL_SUB_HURT" - ) - PHYSICAL_RES: Digit = Field(Digit(type=DigitType.PERCENT, value=Decimal(0)), alias="FIGHT_PROP_PHYSICAL_ADD_HURT") - - CURRENT_HP: Digit = Field(Digit(type=DigitType.NUMERIC, value=Decimal(0)), alias="FIGHT_PROP_CUR_HP") - MAX_HP: Digit = Field(Digit(type=DigitType.NUMERIC, value=Decimal(0)), alias="FIGHT_PROP_MAX_HP") - CURRENT_ATTACK: Digit = Field(Digit(type=DigitType.NUMERIC, value=Decimal(0)), alias="FIGHT_PROP_CUR_ATTACK") - CURRENT_DEFENSE: Digit = Field(Digit(type=DigitType.NUMERIC, value=Decimal(0)), alias="FIGHT_PROP_CUR_DEFENSE") - - -class CharacterInfo(BaseModel): - id: int = 0 - character: Character = "" - weapon_info: Optional[WeaponInfo] = None - artifacts: List[Artifact] = [] - level: int = 0 - max_level: int = 0 - constellation: int = 0 - ascension: int = 0 - skills: List[int] = [] - rarity: int = 0 - stats: CharacterStats = CharacterStats() - - @validator("max_level") - def validate_max_level(cls, v, values): - if v == 0: - return values["level"] - if v < values["level"]: - raise ValueError("max_level must be greater than or equal to level") - return v - - @validator("skills") - def validate_skills(cls, v): - if len(v) > 3: - raise ValueError("skills must not be greater than 3") - return v diff --git a/plugins/genshin/model/converters/enka.py b/plugins/genshin/model/converters/enka.py deleted file mode 100644 index 1df23a7..0000000 --- a/plugins/genshin/model/converters/enka.py +++ /dev/null @@ -1,197 +0,0 @@ -from decimal import Decimal - -from enkanetwork import ( - CharacterInfo as EnkaCharacterInfo, - CharacterStats as EnkaCharacterStats, - StatsPercentage, - Equipments, - EquipmentsType, - EquipType, - EquipmentsStats, - DigitType as EnkaDigitType, -) - -from plugins.genshin.model import ( - CharacterInfo, - Digit, - DigitType, - CharacterStats, - WeaponInfo, - WeaponType, - Artifact, - ArtifactPosition, - ArtifactAttribute, - ArtifactAttributeType, -) -from plugins.genshin.model.metadata import Metadata - -metadata = Metadata() - - -class EnkaConverter: - @classmethod - def to_weapon_type(cls, type_str: str) -> WeaponType: - if type_str == "WEAPON_BOW": - return WeaponType.BOW - if type_str == "WEAPON_CATALYST": - return WeaponType.CATALYST - if type_str == "WEAPON_CLAYMORE": - return WeaponType.CLAYMORE - if type_str == "WEAPON_POLE": - return WeaponType.POLEARM - if type_str == "WEAPON_SWORD_ONE_HAND": - return WeaponType.SWORD - if type_str == "单手剑": - return WeaponType.SWORD - raise ValueError(f"Unknown weapon type: {type_str}") - - @classmethod - def to_weapon_info(cls, equipment: Equipments) -> WeaponInfo: - if equipment.type != EquipmentsType.WEAPON: - raise ValueError(f"Not weapon equipment type: {equipment.type}") - - weapon_data = metadata.weapon_metadata.get(str(equipment.id)) - if not weapon_data: - raise ValueError(f"Unknown weapon id: {equipment.id}") - - return WeaponInfo( - id=equipment.id, - weapon=weapon_data["route"], - type=cls.to_weapon_type(weapon_data["type"]), - level=equipment.level, - max_level=equipment.max_level, - refinement=equipment.refinement, - ascension=equipment.ascension, - ) - - @classmethod - def to_artifact_attribute_type(cls, prop_id: str) -> ArtifactAttributeType: # skipcq: PY-R1000 - if prop_id == "FIGHT_PROP_HP": - return ArtifactAttributeType.HP - if prop_id == "FIGHT_PROP_ATTACK": - return ArtifactAttributeType.ATK - if prop_id == "FIGHT_PROP_DEFENSE": - return ArtifactAttributeType.DEF - if prop_id == "FIGHT_PROP_HP_PERCENT": - return ArtifactAttributeType.HP_PERCENT - if prop_id == "FIGHT_PROP_ATTACK_PERCENT": - return ArtifactAttributeType.ATK_PERCENT - if prop_id == "FIGHT_PROP_DEFENSE_PERCENT": - return ArtifactAttributeType.DEF_PERCENT - if prop_id == "FIGHT_PROP_ELEMENT_MASTERY": - return ArtifactAttributeType.ELEMENTAL_MASTERY - if prop_id == "FIGHT_PROP_CHARGE_EFFICIENCY": - return ArtifactAttributeType.ENERGY_RECHARGE - if prop_id == "FIGHT_PROP_CRITICAL": - return ArtifactAttributeType.CRIT_RATE - if prop_id == "FIGHT_PROP_CRITICAL_HURT": - return ArtifactAttributeType.CRIT_DMG - if prop_id == "FIGHT_PROP_HEAL_ADD": - return ArtifactAttributeType.HEALING_BONUS - if prop_id == "FIGHT_PROP_FIRE_ADD_HURT": - return ArtifactAttributeType.PYRO_DMG_BONUS - if prop_id == "FIGHT_PROP_WATER_ADD_HURT": - return ArtifactAttributeType.HYDRO_DMG_BONUS - if prop_id == "FIGHT_PROP_ELEC_ADD_HURT": - return ArtifactAttributeType.ELECTRO_DMG_BONUS - if prop_id == "FIGHT_PROP_ICE_ADD_HURT": - return ArtifactAttributeType.CRYO_DMG_BONUS - if prop_id == "FIGHT_PROP_WIND_ADD_HURT": - return ArtifactAttributeType.ANEMO_DMG_BONUS - if prop_id == "FIGHT_PROP_ROCK_ADD_HURT": - return ArtifactAttributeType.GEO_DMG_BONUS - if prop_id == "FIGHT_PROP_GRASS_ADD_HURT": - return ArtifactAttributeType.DENDRO_DMG_BONUS - if prop_id == "FIGHT_PROP_PHYSICAL_ADD_HURT": - return ArtifactAttributeType.PHYSICAL_DMG_BONUS - raise ValueError(f"Unknown artifact attribute type: {prop_id}") - - @classmethod - def to_artifact_attribute(cls, equip_stat: EquipmentsStats) -> ArtifactAttribute: - return ArtifactAttribute( - type=cls.to_artifact_attribute_type(equip_stat.prop_id), - digit=Digit( - value=Decimal(equip_stat.value), - type=DigitType.PERCENT if equip_stat.type == EnkaDigitType.PERCENT else DigitType.NUMERIC, - ), - ) - - @classmethod - def to_artifact_position(cls, equip_type: EquipType) -> ArtifactPosition: - if equip_type == EquipType.Flower: - return ArtifactPosition.FLOWER - if equip_type == EquipType.Feather: - return ArtifactPosition.PLUME - if equip_type == EquipType.Sands: - return ArtifactPosition.SANDS - if equip_type == EquipType.Goblet: - return ArtifactPosition.GOBLET - if equip_type == EquipType.Circlet: - return ArtifactPosition.CIRCLET - raise ValueError(f"Unknown artifact position: {equip_type}") - - @classmethod - def to_artifact(cls, equipment: Equipments) -> Artifact: - if equipment.type != EquipmentsType.ARTIFACT: - raise ValueError(f"Not artifact equipment type: {equipment.type}") - - artifact_data = next( - ( - data - for data in metadata.artifacts_metadata.values() - if data["name"] == equipment.detail.artifact_name_set - ), - None, - ) - if not artifact_data: - raise ValueError(f"Unknown artifact: {equipment}") - - return Artifact( - id=artifact_data["id"], - set=artifact_data["route"], - position=cls.to_artifact_position(equipment.detail.artifact_type), - level=equipment.level, - rarity=equipment.detail.rarity, - main_attribute=cls.to_artifact_attribute(equipment.detail.mainstats), - sub_attributes=[cls.to_artifact_attribute(stat) for stat in equipment.detail.substats], - ) - - @classmethod - def to_character_stats(cls, character_stats: EnkaCharacterStats) -> CharacterStats: - return CharacterStats( - **{ - stat: Digit( - value=Decimal(value.value), - type=DigitType.PERCENT if isinstance(value, StatsPercentage) else DigitType.NUMERIC, - ) - for stat, value in character_stats._iter() # pylint: disable=W0212 - } - ) - - @classmethod - def to_character(cls, character_info: EnkaCharacterInfo) -> str: - character_id = str(character_info.id) - if character_id in ("10000005", "10000007"): - character_id += f"-{character_info.element.name.lower()}" - character_data = metadata.characters_metadata.get(character_id) - if not character_data: - raise ValueError(f"Unknown character: {character_info.name}\n{character_info}") - return character_data["route"] - - @classmethod - def to_character_info(cls, character_info: EnkaCharacterInfo) -> CharacterInfo: - weapon_equip = next((equip for equip in character_info.equipments if equip.type == EquipmentsType.WEAPON), None) - artifacts_equip = [equip for equip in character_info.equipments if equip.type == EquipmentsType.ARTIFACT] - return CharacterInfo( - id=character_info.id, - character=cls.to_character(character_info), - rarity=character_info.rarity, - weapon_info=cls.to_weapon_info(weapon_equip) if weapon_equip else None, - artifacts=[cls.to_artifact(equip) for equip in artifacts_equip], - level=character_info.level, - max_level=character_info.max_level, - ascension=character_info.ascension, - constellation=character_info.constellations_unlocked, - skills=[skill.level for skill in character_info.skills], - stats=cls.to_character_stats(character_info.stats), - ) diff --git a/plugins/genshin/model/converters/gcsim.py b/plugins/genshin/model/converters/gcsim.py deleted file mode 100644 index e1efb97..0000000 --- a/plugins/genshin/model/converters/gcsim.py +++ /dev/null @@ -1,405 +0,0 @@ -import re -from collections import Counter -from decimal import Decimal -from functools import lru_cache -from typing import List, Optional, Tuple, Dict - -from gcsim_pypi.aliases import CHARACTER_ALIASES, WEAPON_ALIASES, ARTIFACT_ALIASES -from pydantic import ValidationError - -from plugins.genshin.model import ( - Set, - Weapon, - DigitType, - WeaponInfo, - Artifact, - ArtifactAttributeType, - Character, - CharacterInfo, - GCSim, - GCSimTarget, - GCSimWeapon, - GCSimWeaponInfo, - GCSimSet, - GCSimSetInfo, - GCSimCharacter, - GCSimEnergySettings, - GCSimCharacterInfo, - GCSimCharacterStats, -) -from plugins.genshin.model.metadata import Metadata -from utils.log import logger - -metadata = Metadata() - - -def remove_non_words(text: str) -> str: - return text.replace("'", "").replace('"', "").replace("-", "").replace(" ", "") - - -def from_character_gcsim_character(character: Character) -> GCSimCharacter: - if character == "Raiden Shogun": - return GCSimCharacter("raiden") - if character == "Yae Miko": - return GCSimCharacter("yaemiko") - if character == "Hu Tao": - return GCSimCharacter("hutao") - if character == "Yun Jin": - return GCSimCharacter("yunjin") - if character == "Kuki Shinobu": - return GCSimCharacter("kuki") - if "Traveler" in character: - s = character.split(" ") - traveler_name = "aether" if s[-1] == "Boy" else "lumine" - return GCSimCharacter(f"{traveler_name}{s[0].lower()}") - return GCSimCharacter(character.split(" ")[-1].lower()) - - -GCSIM_CHARACTER_TO_CHARACTER: Dict[GCSimCharacter, Tuple[int, Character]] = {} -for char in metadata.characters_metadata.values(): - GCSIM_CHARACTER_TO_CHARACTER[from_character_gcsim_character(char["route"])] = (char["id"], char["route"]) -for alias, char in CHARACTER_ALIASES.items(): - if alias not in GCSIM_CHARACTER_TO_CHARACTER: - if char in GCSIM_CHARACTER_TO_CHARACTER: - GCSIM_CHARACTER_TO_CHARACTER[alias] = GCSIM_CHARACTER_TO_CHARACTER[char] - elif alias.startswith("traveler") or alias.startswith("aether") or alias.startswith("lumine"): - continue - else: - logger.warning("Character alias %s not found in GCSIM", alias) - -GCSIM_WEAPON_TO_WEAPON: Dict[GCSimWeapon, Tuple[int, Weapon]] = {} -for _weapon in metadata.weapon_metadata.values(): - GCSIM_WEAPON_TO_WEAPON[remove_non_words(_weapon["route"].lower())] = (_weapon["id"], _weapon["route"]) -for alias, _weapon in WEAPON_ALIASES.items(): - if alias not in GCSIM_WEAPON_TO_WEAPON: - if _weapon in GCSIM_WEAPON_TO_WEAPON: - GCSIM_WEAPON_TO_WEAPON[alias] = GCSIM_WEAPON_TO_WEAPON[_weapon] - else: - logger.warning("Weapon alias %s not found in GCSIM", alias) - -GCSIM_ARTIFACT_TO_ARTIFACT: Dict[GCSimSet, Tuple[int, Set]] = {} -for _artifact in metadata.artifacts_metadata.values(): - GCSIM_ARTIFACT_TO_ARTIFACT[remove_non_words(_artifact["route"].lower())] = (_artifact["id"], _artifact["route"]) -for alias, _artifact in ARTIFACT_ALIASES.items(): - if alias not in GCSIM_ARTIFACT_TO_ARTIFACT: - if _artifact in GCSIM_ARTIFACT_TO_ARTIFACT: - GCSIM_ARTIFACT_TO_ARTIFACT[alias] = GCSIM_ARTIFACT_TO_ARTIFACT[_artifact] - else: - logger.warning("Artifact alias %s not found in GCSIM", alias) - - -class GCSimConverter: - literal_keys_numeric_values_regex = re.compile( - r"([\w_%]+)=(\d+ *, *\d+ *, *\d+|[\d*\.*\d+]+ *, *[\d*\.*\d+]+|\d+/\d+|\d*\.*\d+|\d+)" - ) - - @classmethod - def to_character(cls, character: GCSimCharacter) -> Tuple[int, Character]: - return GCSIM_CHARACTER_TO_CHARACTER[character] - - @classmethod - def from_character(cls, character: Character) -> GCSimCharacter: - return from_character_gcsim_character(character) - - @classmethod - def to_weapon(cls, weapon: GCSimWeapon) -> Tuple[int, Weapon]: - return GCSIM_WEAPON_TO_WEAPON[weapon] - - @classmethod - def from_weapon(cls, weapon: Weapon) -> GCSimWeapon: - return GCSimWeapon(remove_non_words(weapon).lower()) - - @classmethod - def from_weapon_info(cls, weapon_info: Optional[WeaponInfo]) -> GCSimWeaponInfo: - if weapon_info is None: - return GCSimWeaponInfo(weapon=GCSimWeapon("dullblade"), refinement=1, level=1, max_level=20) - return GCSimWeaponInfo( - weapon=cls.from_weapon(weapon_info.weapon), - refinement=weapon_info.refinement, - level=weapon_info.level, - max_level=weapon_info.max_level, - ) - - @classmethod - def to_set(cls, set_name: GCSimSet) -> Tuple[int, Set]: - return GCSIM_ARTIFACT_TO_ARTIFACT[set_name] - - @classmethod - def from_set(cls, set_name: Set) -> GCSimSet: - return GCSimSet(remove_non_words(set_name).lower()) - - @classmethod - def from_artifacts(cls, artifacts: List[Artifact]) -> List[GCSimSetInfo]: - c = Counter() - for art in artifacts: - c[cls.from_set(art.set)] += 1 - return [GCSimSetInfo(set=set_name, count=count) for set_name, count in c.items()] - - @classmethod - @lru_cache - def from_attribute_type(cls, attribute_type: ArtifactAttributeType) -> str: # skipcq: PY-R1000 - if attribute_type == ArtifactAttributeType.HP: - return "HP" - if attribute_type == ArtifactAttributeType.HP_PERCENT: - return "HP_PERCENT" - if attribute_type == ArtifactAttributeType.ATK: - return "ATK" - if attribute_type == ArtifactAttributeType.ATK_PERCENT: - return "ATK_PERCENT" - if attribute_type == ArtifactAttributeType.DEF: - return "DEF" - if attribute_type == ArtifactAttributeType.DEF_PERCENT: - return "DEF_PERCENT" - if attribute_type == ArtifactAttributeType.ELEMENTAL_MASTERY: - return "EM" - if attribute_type == ArtifactAttributeType.ENERGY_RECHARGE: - return "ER" - if attribute_type == ArtifactAttributeType.CRIT_RATE: - return "CR" - if attribute_type == ArtifactAttributeType.CRIT_DMG: - return "CD" - if attribute_type == ArtifactAttributeType.HEALING_BONUS: - return "HEAL" - if attribute_type == ArtifactAttributeType.PYRO_DMG_BONUS: - return "PYRO_PERCENT" - if attribute_type == ArtifactAttributeType.HYDRO_DMG_BONUS: - return "HYDRO_PERCENT" - if attribute_type == ArtifactAttributeType.DENDRO_DMG_BONUS: - return "DENDRO_PERCENT" - if attribute_type == ArtifactAttributeType.ELECTRO_DMG_BONUS: - return "ELECTRO_PERCENT" - if attribute_type == ArtifactAttributeType.ANEMO_DMG_BONUS: - return "ANEMO_PERCENT" - if attribute_type == ArtifactAttributeType.CRYO_DMG_BONUS: - return "CRYO_PERCENT" - if attribute_type == ArtifactAttributeType.GEO_DMG_BONUS: - return "GEO_PERCENT" - if attribute_type == ArtifactAttributeType.PHYSICAL_DMG_BONUS: - return "PHYS_PERCENT" - raise ValueError(f"Unknown attribute type: {attribute_type}") - - @classmethod - def from_artifacts_stats(cls, artifacts: List[Artifact]) -> GCSimCharacterStats: - gcsim_stats = GCSimCharacterStats() - for art in artifacts: - main_attr_name = cls.from_attribute_type(art.main_attribute.type) - setattr( - gcsim_stats, - main_attr_name, - getattr(gcsim_stats, main_attr_name) - + ( - Decimal(art.main_attribute.digit.value) / Decimal(100) - if art.main_attribute.digit.type == DigitType.PERCENT - else Decimal(art.main_attribute.digit.value) - ), - ) - for sub_attr in art.sub_attributes: - attr_name = cls.from_attribute_type(sub_attr.type) - setattr( - gcsim_stats, - attr_name, - getattr(gcsim_stats, attr_name) - + ( - Decimal(sub_attr.digit.value) / Decimal(100) - if sub_attr.digit.type == DigitType.PERCENT - else Decimal(sub_attr.digit.value) - ), - ) - return gcsim_stats - - @classmethod - def from_character_info(cls, character: CharacterInfo) -> GCSimCharacterInfo: - return GCSimCharacterInfo( - character=cls.from_character(character.character), - level=character.level, - max_level=character.max_level, - constellation=character.constellation, - talent=character.skills, - weapon_info=cls.from_weapon_info(character.weapon_info), - set_info=cls.from_artifacts(character.artifacts), - # NOTE: Only stats from arifacts are needed - stats=cls.from_artifacts_stats(character.artifacts), - ) - - @classmethod - def merge_character_infos(cls, gcsim: GCSim, character_infos: List[CharacterInfo]) -> GCSim: - gcsim_characters = {ch.character: ch for ch in gcsim.characters} - for character_info in character_infos: - try: - gcsim_character = cls.from_character_info(character_info) - if gcsim_character.character in gcsim_characters: - gcsim_characters[gcsim_character.character] = gcsim_character - except ValidationError as e: - errors = e.errors() - if errors and errors[0].get("msg").startswith("Not supported"): - # Something is not supported, skip - continue - logger.warning("Failed to convert character info: %s", character_info) - gcsim.characters = list(gcsim_characters.values()) - return gcsim - - @classmethod - def prepend_scripts(cls, gcsim: GCSim, scripts: List[str]) -> GCSim: - gcsim.scripts = scripts + gcsim.scripts - return gcsim - - @classmethod - def append_scripts(cls, gcsim: GCSim, scripts: List[str]) -> GCSim: - gcsim.scripts = gcsim.scripts + scripts - return gcsim - - @classmethod - def from_gcsim_energy(cls, line: str) -> GCSimEnergySettings: - energy_settings = GCSimEnergySettings() - matches = cls.literal_keys_numeric_values_regex.findall(line) - for key, value in matches: - if key == "interval": - energy_settings.intervals = list(map(int, value.split(","))) - elif key == "amount": - energy_settings.amount = int(value) - else: - logger.warning("Unknown energy setting: %s=%s", key, value) - return energy_settings - - @classmethod - def from_gcsim_target(cls, line: str) -> GCSimTarget: - target = GCSimTarget() - matches = cls.literal_keys_numeric_values_regex.findall(line) - for key, value in matches: - if key == "lvl": - target.level = int(value) - elif key == "hp": - target.hp = int(value) - elif key == "amount": - target.amount = int(value) - elif key == "resist": - target.resist = float(value) - elif key == "pos": - target.position = tuple(p for p in value.split(",")) - elif key == "interval": - target.interval = list(map(int, value.split(","))) - elif key == "radius": - target.radius = float(value) - elif key == "particle_threshold": - target.particle_threshold = float(value) - elif key == "particle_drop_count": - target.particle_drop_count = float(value) - elif key in ("pyro", "hydro", "dendro", "electro", "anemo", "cryo", "geo", "physical"): - target.others[key] = float(value) - else: - logger.warning("Unknown target setting: %s=%s", key, value) - return target - - @classmethod - def from_gcsim_char_line(cls, line: str, character: GCSimCharacterInfo) -> GCSimCharacterInfo: - matches = cls.literal_keys_numeric_values_regex.findall(line) - for key, value in matches: - if key == "lvl": - character.level, character.max_level = map(int, value.split("/")) - elif key == "cons": - character.constellation = int(value) - elif key == "talent": - character.talent = list(map(int, value.split(","))) - elif key == "start_hp": - character.start_hp = int(value) - elif key == "breakthrough": - character.params.append(f"{key}={value}") - else: - logger.warning("Unknown character setting: %s=%s", key, value) - return character - - @classmethod - def from_gcsim_weapon_line(cls, line: str, weapon_info: GCSimWeaponInfo) -> GCSimWeaponInfo: - weapon_name = re.search(r"weapon= *\"(.*)\"", line).group(1) - if weapon_name not in WEAPON_ALIASES: - raise ValueError(f"Unknown weapon: {weapon_name}") - weapon_info.weapon = WEAPON_ALIASES[weapon_name] - - for key, value in cls.literal_keys_numeric_values_regex.findall(line): - if key == "refine": - weapon_info.refinement = int(value) - elif key == "lvl": - weapon_info.level, weapon_info.max_level = map(int, value.split("/")) - elif key.startswith("stack"): - weapon_info.params.append(f"stacks={value}") - elif key in ("pickup_delay", "breakthrough"): - weapon_info.params.append(f"{key}={value}") - else: - logger.warning("Unknown weapon setting: %s=%s", key, value) - return weapon_info - - @classmethod - def from_gcsim_set_line(cls, line: str) -> GCSimSetInfo: - gcsim_set = re.search(r"set= *\"(.*)\"", line).group(1) - if gcsim_set not in ARTIFACT_ALIASES: - raise ValueError(f"Unknown set: {gcsim_set}") - gcsim_set = ARTIFACT_ALIASES[gcsim_set] - set_info = GCSimSetInfo(set=gcsim_set) - - for key, value in cls.literal_keys_numeric_values_regex.findall(line): - if key == "count": - set_info.count = int(value) - elif key.startswith("stack"): - set_info.params.append(f"stacks={value}") - else: - logger.warning("Unknown set info: %s=%s", key, value) - return set_info - - @classmethod - def from_gcsim_stats_line(cls, line: str, stats: GCSimCharacterStats) -> GCSimCharacterStats: - matches = re.findall(r"(\w+%?)=(\d*\.*\d+)", line) - for stat, value in matches: - attr = stat.replace("%", "_percent").upper() - setattr(stats, attr, getattr(stats, attr) + Decimal(value)) - return stats - - @classmethod - def from_gcsim_script(cls, script: str) -> GCSim: # skipcq: PY-R1000 - options = "" - characters = {} - character_aliases = {} - active_character = None - targets = [] - energy_settings = GCSimEnergySettings() - script_lines = [] - for line in script.strip().split("\n"): - line = line.split("#")[0].strip() - if not line or line.startswith("#"): - continue - if line.startswith("options"): - options = line.strip(";") - elif line.startswith("target"): - targets.append(cls.from_gcsim_target(line)) - elif line.startswith("energy"): - energy_settings = cls.from_gcsim_energy(line) - elif line.startswith("active"): - active_character = line.strip(";").split(" ")[1] - elif m := re.match(r"(\w+) +(char|add weapon|add set|add stats)\W", line): - if m.group(1) not in CHARACTER_ALIASES: - raise ValueError(f"Unknown character: {m.group(1)}") - c = CHARACTER_ALIASES[m.group(1)] - if c not in characters: - characters[c] = GCSimCharacterInfo(character=c) - if m.group(1) != c: - character_aliases[m.group(1)] = c - if m.group(2) == "char": - characters[c] = cls.from_gcsim_char_line(line, characters[c]) - elif m.group(2) == "add weapon": - characters[c].weapon_info = cls.from_gcsim_weapon_line(line, characters[c].weapon_info) - elif m.group(2) == "add set": - characters[c].set_info.append(cls.from_gcsim_set_line(line)) - elif m.group(2) == "add stats": - characters[c].stats = cls.from_gcsim_stats_line(line, characters[c].stats) - else: - for key, value in character_aliases.items(): - line = line.replace(f"{key} ", f"{value} ") - line = line.replace(f".{key}.", f".{value}.") - script_lines.append(line) - return GCSim( - options=options, - characters=list(characters.values()), - targets=targets, - energy_settings=energy_settings, - active_character=active_character, - script_lines=script_lines, - ) diff --git a/plugins/genshin/model/gcsim.py b/plugins/genshin/model/gcsim.py deleted file mode 100644 index 3fd867f..0000000 --- a/plugins/genshin/model/gcsim.py +++ /dev/null @@ -1,237 +0,0 @@ -from decimal import Decimal -from typing import Any, NewType, List, Optional, Tuple, Dict - -from gcsim_pypi.aliases import ARTIFACT_ALIASES, CHARACTER_ALIASES, WEAPON_ALIASES -from gcsim_pypi.availability import AVAILABLE_ARTIFACTS, AVAILABLE_CHARACTERS, AVAILABLE_WEAPONS -from pydantic import BaseModel, validator - -GCSimCharacter = NewType("GCSimCharacter", str) -GCSimWeapon = NewType("GCSimWeapon", str) -GCSimSet = NewType("GCSimSet", str) - - -class GCSimWeaponInfo(BaseModel): - weapon: GCSimWeapon = "dullblade" - refinement: int = 1 - level: int = 1 - max_level: int = 20 - params: List[str] = [] - - @validator("weapon") - def validate_weapon(cls, v): - if v not in AVAILABLE_WEAPONS or v not in WEAPON_ALIASES: - raise ValueError(f"Not supported weapon: {v}") - return WEAPON_ALIASES[v] - - -class GCSimSetInfo(BaseModel): - set: GCSimSet - count: int = 2 - params: List[str] = [] - - @validator("set") - def validate_set(cls, v): - if v not in AVAILABLE_ARTIFACTS or v not in ARTIFACT_ALIASES: - raise ValueError(f"Not supported set: {v}") - return ARTIFACT_ALIASES[v] - - -class GCSimCharacterStats(BaseModel): - HP: Decimal = Decimal(0) - HP_PERCENT: Decimal = Decimal(0) - ATK: Decimal = Decimal(0) - ATK_PERCENT: Decimal = Decimal(0) - DEF: Decimal = Decimal(0) - DEF_PERCENT: Decimal = Decimal(0) - EM: Decimal = Decimal(0) - ER: Decimal = Decimal(0) - CR: Decimal = Decimal(0) - CD: Decimal = Decimal(0) - HEAL: Decimal = Decimal(0) - PYRO_PERCENT: Decimal = Decimal(0) - HYDRO_PERCENT: Decimal = Decimal(0) - DENDRO_PERCENT: Decimal = Decimal(0) - ELECTRO_PERCENT: Decimal = Decimal(0) - ANEMO_PERCENT: Decimal = Decimal(0) - CRYO_PERCENT: Decimal = Decimal(0) - GEO_PERCENT: Decimal = Decimal(0) - PHYS_PERCENT: Decimal = Decimal(0) - - -class GCSimCharacterInfo(BaseModel): - character: GCSimCharacter - level: int = 1 - max_level: int = 20 - constellation: int = 0 - talent: List[int] = [1, 1, 1] - start_hp: Optional[int] = None - weapon_info: GCSimWeaponInfo = GCSimWeaponInfo() - set_info: List[GCSimSetInfo] = [] - stats: GCSimCharacterStats = GCSimCharacterStats() - params: List[str] = [] - - @validator("character") - def validate_character(cls, v): - if v not in AVAILABLE_CHARACTERS or v not in CHARACTER_ALIASES: - raise ValueError(f"Not supported character: {v}") - return CHARACTER_ALIASES[v] - - @property - def char(self) -> str: - return self.character - - @property - def char_line(self) -> str: - return ( - " ".join( - filter( - lambda w: w, - [ - f"{self.char}", - "char", - f"lvl={self.level}/{self.max_level}", - f"cons={self.constellation}", - f"start_hp={self.start_hp}" if self.start_hp is not None else "", - f"talent={','.join(str(t) for t in self.talent)}", - f"+params=[{','.join(self.params)}] " if self.params else "", - ], - ) - ) - + ";" - ) - - @property - def weapon_line(self) -> str: - return ( - " ".join( - filter( - lambda w: w, - [ - f"{self.char}", - f'add weapon="{self.weapon_info.weapon}"', - f"refine={self.weapon_info.refinement}", - f"lvl={self.weapon_info.level}/{self.weapon_info.max_level}", - f"+params=[{','.join(self.weapon_info.params)}] " if self.weapon_info.params else "", - ], - ) - ) - + ";" - ) - - @property - def set_line(self) -> str: - return "\n".join( - " ".join( - filter( - lambda w: w, - [ - f"{self.char}", - f'add set="{set_info.set}"', - f"count={4 if set_info.count >= 4 else 2}", - f"+params=[{','.join(set_info.params)}] " if set_info.params else "", - ], - ) - ) - + ";" - for set_info in self.set_info - # NOTE: 祭*系列似乎并不支持 - if set_info.count > 1 - ) - - @property - def stats_line(self) -> str: - if all(value == 0 for _, value in self.stats): - return "" - return ( - f"{self.char} add stats " - + " ".join( - [ - ( - f"{stat.replace('_PERCENT', '%').lower()}={value:.4f}" - if stat.endswith("_PERCENT") or stat in {"CR", "CD", "ER"} - else f"{stat.lower()}={value:.2f}" - ) - for stat, value in iter(self.stats) - if value > 0 - ] - ) - + ";" - ) - - def __str__(self) -> str: - return "\n".join([self.char_line, self.weapon_line, self.set_line, self.stats_line]) - - -class GCSimTarget(BaseModel): - level: int = 100 - resist: float = 0.1 - position: Tuple[str, str] = ("0", "0") - interval: List[int] = [] - radius: Optional[float] = None - hp: Optional[int] = None - amount: Optional[int] = None - particle_threshold: Optional[float] = None - particle_drop_count: Optional[float] = None - others: Dict[str, Any] = {} - - def __str__(self) -> str: - return ( - " ".join( - filter( - lambda w: w, - [ - f"target lvl={self.level} resist={self.resist} ", - f"pos={','.join(self.position)}", - f"radius={self.radius}" if self.radius is not None else "", - f"hp={self.hp}" if self.hp is not None else "", - f"amount={self.amount}" if self.amount is not None else "", - f"interval={','.join(str(i) for i in self.interval)}" if self.interval else "", - f"particle_threshold={self.particle_threshold}" if self.particle_threshold is not None else "", - ( - f"particle_drop_count={self.particle_drop_count}" - if self.particle_drop_count is not None - else "" - ), - " ".join([f"{k}={v}" for k, v in self.others.items()]), - ], - ) - ) - + ";" - ) - - -class GCSimEnergySettings(BaseModel): - intervals: List[int] = [480, 720] - amount: int = 1 - - def __str__(self) -> str: - return f"energy every interval={','.join(str(i) for i in self.intervals)} amount={self.amount};" - - -class GCSim(BaseModel): - options: Optional[str] = None - characters: List[GCSimCharacterInfo] = [] - targets: List[GCSimTarget] = [GCSimTarget()] - energy_settings: Optional[GCSimEnergySettings] = None - # TODO: Do we even want this? - hurt_settings: Optional[str] = None - active_character: Optional[GCSimCharacter] = None - script_lines: List[str] = [] - - def __str__(self) -> str: - line = "" - if self.options: - line += f"{self.options};\n" - line += "\n".join([str(c) for c in self.characters]) - line += "\n" - line += "\n".join([str(t) for t in self.targets]) - line += "\n" - if self.energy_settings: - line += f"{self.energy_settings}\n" - if self.active_character: - line += f"active {self.active_character};\n" - else: - line += f"active {self.characters[0].char};\n" - line += "\n".join(self.script_lines) - line += "\n" - return line diff --git a/plugins/genshin/model/metadata.py b/plugins/genshin/model/metadata.py deleted file mode 100644 index cdde2d8..0000000 --- a/plugins/genshin/model/metadata.py +++ /dev/null @@ -1,31 +0,0 @@ -from typing import Dict, Any - -from utils.const import PROJECT_ROOT - -try: - import ujson as jsonlib -except ImportError: - import json as jsonlib - -METADATA_PATH = PROJECT_ROOT.joinpath("metadata").joinpath("data") - - -class Metadata: - _instance: "Metadata" = None - weapon_metadata: Dict[str, Any] = {} - artifacts_metadata: Dict[str, Any] = {} - characters_metadata: Dict[str, Any] = {} - - def __new__(cls): - if cls._instance is None: - cls._instance = super().__new__(cls) - cls._instance.reload_assets() - return cls._instance - - def reload_assets(self) -> None: - self.__load_assets_data() - - def __load_assets_data(self) -> None: - self.weapon_metadata = jsonlib.loads(METADATA_PATH.joinpath("weapon.json").read_text(encoding="utf-8")) - self.artifacts_metadata = jsonlib.loads(METADATA_PATH.joinpath("reliquary.json").read_text(encoding="utf-8")) - self.characters_metadata = jsonlib.loads(METADATA_PATH.joinpath("avatar.json").read_text(encoding="utf-8")) diff --git a/plugins/genshin/pay_log.py b/plugins/genshin/pay_log.py deleted file mode 100644 index 4feadef..0000000 --- a/plugins/genshin/pay_log.py +++ /dev/null @@ -1,244 +0,0 @@ -from typing import TYPE_CHECKING, Optional, List - -from simnet import GenshinClient, Region -from telegram import InlineKeyboardButton, InlineKeyboardMarkup -from telegram.constants import ChatAction -from telegram.ext import CommandHandler, MessageHandler, filters, ConversationHandler -from telegram.helpers import create_deep_linked_url - -from core.basemodel import RegionEnum -from core.config import config -from core.plugin import Plugin, handler, conversation -from core.services.cookies import CookiesService -from core.services.players.services import PlayersService -from core.services.template.services import TemplateService -from modules.gacha_log.helpers import from_url_get_authkey -from modules.pay_log.error import PayLogNotFound, PayLogAccountNotFound, PayLogInvalidAuthkey, PayLogAuthkeyTimeout -from modules.pay_log.log import PayLog -from modules.pay_log.migrate import PayLogMigrate -from plugins.tools.genshin import PlayerNotFoundError, CookiesNotFoundError -from plugins.tools.player_info import PlayerInfoSystem -from utils.log import logger - -if TYPE_CHECKING: - from telegram import Update, User - from telegram.ext import ContextTypes - from gram_core.services.players.models import Player - - -INPUT_URL, CONFIRM_DELETE = range(10100, 10102) -WAITING = f"小{config.notice.bot_name}正在从服务器获取数据,请稍后" -PAYLOG_NOT_FOUND = f"{config.notice.bot_name}没有找到你的充值记录,快来私聊{config.notice.bot_name}导入吧~" - - -class PayLogPlugin(Plugin.Conversation): - """充值记录导入/导出/分析""" - - def __init__( - self, - template_service: TemplateService, - players_service: PlayersService, - cookie_service: CookiesService, - player_info: PlayerInfoSystem, - ): - self.template_service = template_service - self.players_service = players_service - self.cookie_service = cookie_service - self.pay_log = PayLog() - self.player_info = player_info - - async def get_player_id(self, uid: int) -> int: - """获取绑定的游戏ID""" - logger.debug("尝试获取已绑定的原神账号") - player = await self.players_service.get_player(uid) - if player is None: - raise PlayerNotFoundError(uid) - return player.player_id - - async def _refresh_user_data(self, user: "User", authkey: str = None) -> str: - """刷新用户数据 - :param user: 用户 - :param authkey: 认证密钥 - :return: 返回信息 - """ - try: - player_id = await self.get_player_id(user.id) - new_num = await self.pay_log.get_log_data(user.id, player_id, authkey) - return "更新完成,本次没有新增数据" if new_num == 0 else f"更新完成,本次共新增{new_num}条充值记录" - except PayLogNotFound: - return f"{config.notice.bot_name}没有找到你的充值记录,快去充值吧~" - except PayLogAccountNotFound: - return "导入失败,可能文件包含的祈愿记录所属 uid 与你当前绑定的 uid 不同" - except PayLogInvalidAuthkey: - return "更新数据失败,authkey 无效" - except PayLogAuthkeyTimeout: - return "更新数据失败,authkey 已经过期" - except PlayerNotFoundError: - logger.info("未查询到用户 %s[%s] 所绑定的账号信息", user.full_name, user.id) - return config.notice.user_not_found - - @conversation.entry_point - @handler.command(command="pay_log_import", filters=filters.ChatType.PRIVATE, block=False) - @handler.message(filters=filters.Regex("^导入充值记录$") & filters.ChatType.PRIVATE, block=False) - @handler.command(command="start", filters=filters.Regex("pay_log_import$"), block=False) - async def command_start(self, update: "Update", _: "ContextTypes.DEFAULT_TYPE") -> int: - message = update.effective_message - user = update.effective_user - logger.info("用户 %s[%s] 导入充值记录命令请求", user.full_name, user.id) - player_info = await self.players_service.get_player(user.id, region=RegionEnum.HYPERION) - if player_info is not None: - cookies = await self.cookie_service.get(user.id, account_id=player_info.account_id) - if cookies is not None: - if "stoken" in cookies.data: - if stuid := next( - (value for key, value in cookies.data.items() if key in ["ltuid", "login_uid"]), None - ): - cookies.data["stuid"] = stuid - async with GenshinClient( - cookies=cookies.data, region=Region.CHINESE, lang="zh-cn", player_id=player_info.player_id - ) as client: - authkey = await client.get_authkey_by_stoken("csc") - else: - await message.reply_text("该功能需要绑定 stoken 才能使用") - return ConversationHandler.END - else: - raise CookiesNotFoundError(user.id) - else: - raise CookiesNotFoundError(user.id) - reply = await message.reply_text(WAITING) - await message.reply_chat_action(ChatAction.TYPING) - data = await self._refresh_user_data(user, authkey=authkey) - await reply.edit_text(data) - return ConversationHandler.END - - @conversation.state(state=INPUT_URL) - @handler.message(filters=~filters.COMMAND, block=False) - async def import_data_from_message(self, update: "Update", _: "ContextTypes.DEFAULT_TYPE") -> int: - message = update.effective_message - user = update.effective_user - if message.document: - await message.reply_text("呜呜呜~本次导入不支持文件导入,请尝试获取连接") - return INPUT_URL - if not message.text: - await message.reply_text("呜呜呜~输入错误,请尝试重新获取连接") - return INPUT_URL - authkey = from_url_get_authkey(message.text) - reply = await message.reply_text(WAITING) - await message.reply_chat_action(ChatAction.TYPING) - text = await self._refresh_user_data(user, authkey=authkey) - await reply.edit_text(text) - return ConversationHandler.END - - @conversation.entry_point - @handler(CommandHandler, command="pay_log_delete", filters=filters.ChatType.PRIVATE, block=False) - @handler(MessageHandler, filters=filters.Regex("^删除充值记录$") & filters.ChatType.PRIVATE, block=False) - async def command_start_delete(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> int: - message = update.effective_message - user = update.effective_user - logger.info("用户 %s[%s] 删除充值记录命令请求", user.full_name, user.id) - player_info = await self.players_service.get_player(user.id) - if player_info is None: - logger.info("未查询到用户 %s[%s] 所绑定的账号信息", user.full_name, user.id) - await message.reply_text(config.notice.user_not_found) - return ConversationHandler.END - _, status = await self.pay_log.load_history_info(str(user.id), str(player_info.player_id), only_status=True) - if not status: - await message.reply_text("你还没有导入充值记录哦~") - return ConversationHandler.END - context.chat_data["uid"] = player_info.player_id - await message.reply_text( - "你确定要删除充值记录吗?(此项操作无法恢复),如果确定请发送 ”确定“,发送其他内容取消" - ) - return CONFIRM_DELETE - - @conversation.state(state=CONFIRM_DELETE) - @handler.message(filters=filters.TEXT & ~filters.COMMAND, block=False) - async def command_confirm_delete(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> int: - message = update.effective_message - user = update.effective_user - if message.text == "确定": - status = await self.pay_log.remove_history_info(str(user.id), str(context.chat_data["uid"])) - await message.reply_text("充值记录已删除" if status else "充值记录删除失败") - return ConversationHandler.END - await message.reply_text("已取消") - return ConversationHandler.END - - @handler(CommandHandler, command="pay_log_force_delete", block=False, admin=True) - async def command_pay_log_force_delete(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE"): - message = update.effective_message - args = self.get_args(context) - if not args: - await message.reply_text("请指定用户ID") - return - try: - cid = int(args[0]) - if cid < 0: - raise ValueError("Invalid cid") - player_info = await self.players_service.get_player(cid) - if player_info is None: - await message.reply_text("该用户暂未绑定账号") - return - _, status = await self.pay_log.load_history_info(str(cid), str(player_info.player_id), only_status=True) - if not status: - await message.reply_text("该用户还没有导入充值记录") - return - status = await self.pay_log.remove_history_info(str(cid), str(player_info.player_id)) - await message.reply_text("充值记录已强制删除" if status else "充值记录删除失败") - except PayLogNotFound: - await message.reply_text("该用户还没有导入充值记录") - except (ValueError, IndexError): - await message.reply_text("用户ID 不合法") - - @handler(CommandHandler, command="pay_log_export", filters=filters.ChatType.PRIVATE, block=False) - @handler(MessageHandler, filters=filters.Regex("^导出充值记录$") & filters.ChatType.PRIVATE, block=False) - async def command_start_export(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> None: - message = update.effective_message - user = update.effective_user - logger.info("用户 %s[%s] 导出充值记录命令请求", user.full_name, user.id) - try: - await message.reply_chat_action(ChatAction.TYPING) - player_id = await self.get_player_id(user.id) - path = self.pay_log.get_file_path(str(user.id), str(player_id)) - if not path.exists(): - raise PayLogNotFound - await message.reply_chat_action(ChatAction.UPLOAD_DOCUMENT) - await message.reply_document(document=open(path, "rb+"), caption="充值记录导出文件") - except PayLogNotFound: - buttons = [ - [InlineKeyboardButton("点我导入", url=create_deep_linked_url(context.bot.username, "pay_log_import"))] - ] - await message.reply_text(PAYLOG_NOT_FOUND, reply_markup=InlineKeyboardMarkup(buttons)) - except PayLogAccountNotFound: - await message.reply_text("导出失败,可能文件包含的祈愿记录所属 uid 与你当前绑定的 uid 不同") - except PlayerNotFoundError: - logger.info("未查询到用户 %s[%s] 所绑定的账号信息", user.full_name, user.id) - await message.reply_text(config.notice.user_not_found) - - @handler(CommandHandler, command="pay_log", block=False) - @handler(MessageHandler, filters=filters.Regex("^充值记录$"), block=False) - async def command_start_analysis(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> None: - user_id = await self.get_real_user_id(update) - message = update.effective_message - self.log_user(update, logger.info, "充值记录统计命令请求") - try: - await message.reply_chat_action(ChatAction.TYPING) - player_id = await self.get_player_id(user_id) - data = await self.pay_log.get_analysis(user_id, player_id) - await message.reply_chat_action(ChatAction.UPLOAD_PHOTO) - name_card = await self.player_info.get_name_card(player_id, user_id) - data["name_card"] = name_card - png_data = await self.template_service.render( - "genshin/pay_log/pay_log.jinja2", data, full_page=True, query_selector=".container" - ) - await png_data.reply_photo(message) - except PayLogNotFound: - buttons = [ - [InlineKeyboardButton("点我导入", url=create_deep_linked_url(context.bot.username, "pay_log_import"))] - ] - await message.reply_text(PAYLOG_NOT_FOUND, reply_markup=InlineKeyboardMarkup(buttons)) - - @staticmethod - async def get_migrate_data( - old_user_id: int, new_user_id: int, old_players: List["Player"] - ) -> Optional[PayLogMigrate]: - return await PayLogMigrate.create(old_user_id, new_user_id, old_players) diff --git a/plugins/genshin/player_cards.py b/plugins/genshin/player_cards.py deleted file mode 100644 index dae2cd5..0000000 --- a/plugins/genshin/player_cards.py +++ /dev/null @@ -1,748 +0,0 @@ -import copy -import math -from typing import Any, List, Tuple, Union, Optional, TYPE_CHECKING, Dict - -from enkanetwork import ( - DigitType, - EnkaNetworkResponse, - EnkaServerError, - Equipments, - EquipmentsType, - HTTPException, - Stats, - StatsPercentage, - VaildateUIDError, - EnkaServerMaintanance, - EnkaServerUnknown, - EnkaServerRateLimit, - EnkaPlayerNotFound, - TimedOut, -) -from pydantic import BaseModel -from telegram import InlineKeyboardButton, InlineKeyboardMarkup -from telegram.constants import ChatAction -from telegram.ext import filters - -from core.config import config -from core.dependence.assets import DEFAULT_EnkaAssets, AssetsService -from core.dependence.redisdb import RedisDB -from core.plugin import Plugin, handler -from core.services.players import PlayersService -from core.services.template.services import TemplateService -from metadata.shortname import roleToName, idToName -from modules.apihelper.client.components.remote import Remote -from modules.gcsim.file import PlayerGCSimScripts -from modules.playercards.file import PlayerCardsFile -from modules.playercards.helpers import ArtifactStatsTheory -from plugins.tools.genshin import PlayerNotFoundError -from utils.enkanetwork import RedisCache, EnkaNetworkAPI -from utils.helpers import download_resource -from utils.log import logger -from utils.uid import mask_number - -try: - from python_genshin_artifact import get_damage_analysis, get_transformative_damage - from python_genshin_artifact.enka.enka_parser import enka_parser - from python_genshin_artifact.error import JsonParseException, EnkaParseException - from python_genshin_artifact import CalculatorConfig, SkillInterface - - GENSHIN_ARTIFACT_FUNCTION_AVAILABLE = True -except ImportError: - get_damage_analysis = None - get_transformative_damage = None - enka_parser = None - CalculatorConfig = None - SkillInterface = None - - GENSHIN_ARTIFACT_FUNCTION_AVAILABLE = False - -if TYPE_CHECKING: - from enkanetwork import CharacterInfo, EquipmentsStats - from telegram.ext import ContextTypes - from telegram import Update, Message - -try: - import ujson as jsonlib -except ImportError: - import json as jsonlib - - -class PlayerCards(Plugin): - def __init__( - self, - player_service: PlayersService, - template_service: TemplateService, - assets_service: AssetsService, - redis: RedisDB, - ): - self.player_service = player_service - self.client = EnkaNetworkAPI(lang="chs", user_agent=config.enka_network_api_agent, cache=False) - self.cache = RedisCache(redis.client, key="plugin:player_cards:enka_network", ex=60) - self.player_cards_file = PlayerCardsFile() - self.player_gcsim_scripts = PlayerGCSimScripts() - self.assets_service = assets_service - self.template_service = template_service - self.kitsune: Optional[str] = None - self.fight_prop_rule: Dict[str, Dict[str, float]] = {} - self.damage_config: Dict = {} - - async def initialize(self): - await self._refresh() - - async def _refresh(self): - self.fight_prop_rule = await Remote.get_fight_prop_rule_data() - self.damage_config = await Remote.get_damage_data() - - async def _update_enka_data(self, uid) -> Union[EnkaNetworkResponse, str]: - try: - data = await self.cache.get(uid) - if data is not None: - return EnkaNetworkResponse.parse_obj(data) - user = await self.client.http.fetch_user_by_uid(uid) - data = user["content"].decode("utf-8", "surrogatepass") # type: ignore - data = jsonlib.loads(data) - data = await self.player_cards_file.merge_info(uid, data) - await self.cache.set(uid, data) - return EnkaNetworkResponse.parse_obj(data) - except TimedOut: - error = "Enka.Network 服务请求超时,请稍后重试" - except EnkaServerRateLimit: - error = "Enka.Network 已对此API进行速率限制,请稍后重试" - except EnkaServerMaintanance: - error = "Enka.Network 正在维护,请等待5-8小时或1天" - except EnkaServerError: - error = "Enka.Network 服务请求错误,请稍后重试" - except EnkaServerUnknown: - error = "Enka.Network 服务瞬间爆炸,请稍后重试" - except EnkaPlayerNotFound: - error = "UID 未找到,可能为服务器抽风,请稍后重试" - except VaildateUIDError: - error = "未找到玩家,请检查您的UID/用户名" - except HTTPException: - error = "Enka.Network HTTP 服务请求错误,请稍后重试" - return error - - async def _load_data_as_enka_response(self, uid) -> Optional[EnkaNetworkResponse]: - data = await self.player_cards_file.load_history_info(uid) - if data is None: - return None - return EnkaNetworkResponse.parse_obj(data) - - async def _load_history(self, uid) -> Optional[Dict]: - return await self.player_cards_file.load_history_info(uid) - - async def get_uid_and_ch( - self, - user_id: int, - args: List[str], - reply: Optional["Message"], - player_id: int, - offset: int, - ) -> Tuple[Optional[int], Optional[str]]: - """通过消息获取 uid,优先级:args > reply > self""" - uid, ch_name, user_id_ = player_id, None, user_id - if args: - for i in args: - if i is not None and not i.startswith("@"): - ch_name = roleToName(i) - if reply: - try: - user_id_ = reply.from_user.id - except AttributeError: - pass - if not uid: - player_info = await self.player_service.get_player(user_id_, offset=offset) - if player_info is not None: - uid = player_info.player_id - if (not uid) and (user_id_ != user_id): - player_info = await self.player_service.get_player(user_id, offset=offset) - if player_info is not None: - uid = player_info.player_id - return uid, ch_name - - @staticmethod - def get_caption(character: "CharacterInfo") -> str: - tags = [character.name, f"等级{character.level}", f"命座{character.constellations_unlocked}"] - if character.equipments: - for item in character.equipments: - if item.type == EquipmentsType.WEAPON and item.detail: - tags.append(item.detail.name) - tags.append(f"武器等级{item.level}") - tags.append(f"精{item.refinement}") - return "#" + " #".join(tags) - - @handler.command(command="player_card", player=True, block=False) - @handler.command(command="player_cards", player=True, block=False) - @handler.message(filters=filters.Regex("^角色卡片查询(.*)"), player=True, block=False) - async def player_cards(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> None: - user_id = await self.get_real_user_id(update) - message = update.effective_message - args = self.get_args(context) - await message.reply_chat_action(ChatAction.TYPING) - uid, offset = self.get_real_uid_or_offset(update) - uid, character_name = await self.get_uid_and_ch(user_id, args, message.reply_to_message, uid, offset) - if uid is None: - raise PlayerNotFoundError(user_id) - original_data = await self._load_history(uid) - if original_data is None or len(original_data["avatarInfoList"]) == 0: - if isinstance(self.kitsune, str): - photo = self.kitsune - else: - photo = open("resources/img/kitsune.png", "rb") - buttons = [ - [ - InlineKeyboardButton( - "更新面板", - callback_data=f"update_player_card|{user_id}|{uid}", - ) - ] - ] - reply_message = await message.reply_photo( - photo=photo, - caption="角色列表未找到,请尝试点击下方按钮从 Enka.Network 更新角色列表", - reply_markup=InlineKeyboardMarkup(buttons), - ) - if reply_message.photo: - self.kitsune = reply_message.photo[-1].file_id - return - enka_response = EnkaNetworkResponse.parse_obj(copy.deepcopy(original_data)) - if character_name is None: - self.log_user(update, logger.info, "角色卡片查询命令请求") - ttl = await self.cache.ttl(uid) - if enka_response.characters is None or len(enka_response.characters) == 0: - buttons = [ - [ - InlineKeyboardButton( - "更新面板", - callback_data=f"update_player_card|{user_id}|{uid}", - ) - ] - ] - else: - buttons = self.gen_button(enka_response, user_id, uid, update_button=ttl < 0) - if isinstance(self.kitsune, str): - photo = self.kitsune - else: - photo = open("resources/img/kitsune.png", "rb") - reply_message = await message.reply_photo( - photo=photo, - caption="请选择你要查询的角色", - reply_markup=InlineKeyboardMarkup(buttons), - ) - if reply_message.photo: - self.kitsune = reply_message.photo[-1].file_id - return - - self.log_user( - update, - logger.info, - "角色卡片查询命令请求 || character_name[%s] uid[%s]", - character_name, - uid, - ) - for characters in enka_response.characters: - if characters.name == character_name: - break - else: - await message.reply_text( - f"角色展柜中未找到 {character_name} ,请检查角色是否存在于角色展柜中,或者等待角色数据更新后重试" - ) - return - await message.reply_chat_action(ChatAction.UPLOAD_PHOTO) - original_data: Optional[Dict] = None - if GENSHIN_ARTIFACT_FUNCTION_AVAILABLE: - original_data = await self._load_history(uid) - render_result = await RenderTemplate( - uid, - characters, - self.fight_prop_rule, - self.damage_config, - self.template_service, - original_data, - ).render() # pylint: disable=W0631 - await render_result.reply_photo( - message, - filename=f"player_card_{uid}_{character_name}.png", - caption=self.get_caption(characters), - ) - - @handler.callback_query(pattern=r"^update_player_card\|", block=False) - async def update_player_card(self, update: "Update", _: "ContextTypes.DEFAULT_TYPE") -> None: - user = update.effective_user - message = update.effective_message - callback_query = update.callback_query - - async def get_player_card_callback(callback_query_data: str) -> Tuple[int, int]: - _data = callback_query_data.split("|") - _user_id = int(_data[1]) - _uid = int(_data[2]) - logger.debug("callback_query_data函数返回 user_id[%s] uid[%s]", _user_id, _uid) - return _user_id, _uid - - user_id, uid = await get_player_card_callback(callback_query.data) - if user.id != user_id: - await callback_query.answer(text="这不是你的按钮!\n" + config.notice.user_mismatch, show_alert=True) - return - - ttl = await self.cache.ttl(uid) - - if ttl > 0: - await callback_query.answer(text=f"请等待 {ttl} 秒后再更新", show_alert=True) - return - - await message.reply_chat_action(ChatAction.TYPING) - data = await self._update_enka_data(uid) - if isinstance(data, str): - await callback_query.answer(text=data, show_alert=True) - return - if data.characters is None or len(data.characters) == 0: - await callback_query.answer( - "请先将角色加入到角色展柜并允许查看角色详情后再使用此功能,如果已经添加了角色,请等待角色数据更新后重试", - show_alert=True, - ) - await message.delete() - return - self.player_gcsim_scripts.remove_fits(uid) - await callback_query.answer(text="正在从 Enka.Network 获取角色列表 请不要重复点击按钮") - buttons = self.gen_button(data, user.id, uid, update_button=False) - render_data = await self.parse_holder_data(data) - holder = await self.template_service.render( - "genshin/player_card/holder.jinja2", - render_data, - viewport={"width": 750, "height": 580}, - ttl=60 * 10, - caption="更新角色列表成功,请选择你要查询的角色", - ) - await holder.edit_media(message, reply_markup=InlineKeyboardMarkup(buttons)) - - @handler.callback_query(pattern=r"^get_player_card\|", block=False) - async def get_player_cards(self, update: "Update", _: "ContextTypes.DEFAULT_TYPE") -> None: - callback_query = update.callback_query - user = callback_query.from_user - message = callback_query.message - - async def get_player_card_callback( - callback_query_data: str, - ) -> Tuple[str, int, int]: - _data = callback_query_data.split("|") - _user_id = int(_data[1]) - _uid = int(_data[2]) - _result = _data[3] - logger.debug( - "callback_query_data函数返回 result[%s] user_id[%s] uid[%s]", - _result, - _user_id, - _uid, - ) - return _result, _user_id, _uid - - result, user_id, uid = await get_player_card_callback(callback_query.data) - if user.id != user_id: - await callback_query.answer(text="这不是你的按钮!\n" + config.notice.user_mismatch, show_alert=True) - return - if result == "empty_data": - await callback_query.answer(text="此按钮不可用", show_alert=True) - return - page = 0 - if result.isdigit(): - page = int(result) - logger.info( - "用户 %s[%s] 角色卡片查询命令请求 || page[%s] uid[%s]", - user.full_name, - user.id, - page, - uid, - ) - else: - logger.info( - "用户 %s[%s] 角色卡片查询命令请求 || character_name[%s] uid[%s]", - user.full_name, - user.id, - result, - uid, - ) - original_data = await self._load_history(uid) - enka_response = EnkaNetworkResponse.parse_obj(copy.deepcopy(original_data)) - if enka_response.characters is None or len(enka_response.characters) == 0: - await callback_query.answer( - "请先将角色加入到角色展柜并允许查看角色详情后再使用此功能,如果已经添加了角色,请等待角色数据更新后重试", - show_alert=True, - ) - await message.delete() - return - if page: - buttons = self.gen_button(enka_response, user.id, uid, page, await self.cache.ttl(uid) <= 0) - await message.edit_reply_markup(reply_markup=InlineKeyboardMarkup(buttons)) - await callback_query.answer(f"已切换到第 {page} 页", show_alert=False) - return - for characters in enka_response.characters: - if characters.name == result: - break - else: - await message.delete() - await callback_query.answer( - f"角色展柜中未找到 {result} ,请检查角色是否存在于角色展柜中,或者等待角色数据更新后重试", - show_alert=True, - ) - return - await callback_query.answer(text="正在渲染图片中 请稍等 请不要重复点击按钮", show_alert=False) - await message.reply_chat_action(ChatAction.UPLOAD_PHOTO) - render_result = await RenderTemplate( - uid, characters, self.fight_prop_rule, self.damage_config, self.template_service, original_data - ).render() # pylint: disable=W0631 - render_result.filename = f"player_card_{uid}_{result}.png" - render_result.caption = self.get_caption(characters) - await render_result.edit_media(message) - - @staticmethod - def gen_button( - data: EnkaNetworkResponse, - user_id: Union[str, int], - uid: int, - page: int = 1, - update_button: bool = True, - ) -> List[List[InlineKeyboardButton]]: - """生成按钮""" - buttons = [] - if data.characters: - buttons = [ - InlineKeyboardButton( - value.name, - callback_data=f"get_player_card|{user_id}|{uid}|{value.name}", - ) - for value in data.characters - if value.name - ] - all_buttons = [buttons[i : i + 4] for i in range(0, len(buttons), 4)] - send_buttons = all_buttons[(page - 1) * 3 : page * 3] - last_page = page - 1 if page > 1 else 0 - all_page = math.ceil(len(all_buttons) / 3) - next_page = page + 1 if page < all_page and all_page > 1 else 0 - last_button = [] - if last_page: - last_button.append( - InlineKeyboardButton( - "<< 上一页", - callback_data=f"get_player_card|{user_id}|{uid}|{last_page}", - ) - ) - if last_page or next_page: - last_button.append( - InlineKeyboardButton( - f"{page}/{all_page}", - callback_data=f"get_player_card|{user_id}|{uid}|empty_data", - ) - ) - if update_button: - last_button.append( - InlineKeyboardButton( - "更新面板", - callback_data=f"update_player_card|{user_id}|{uid}", - ) - ) - if next_page: - last_button.append( - InlineKeyboardButton( - "下一页 >>", - callback_data=f"get_player_card|{user_id}|{uid}|{next_page}", - ) - ) - if last_button: - send_buttons.append(last_button) - return send_buttons - - async def parse_holder_data(self, data: EnkaNetworkResponse) -> dict: - """ - 生成渲染所需数据 - """ - characters_data = [] - for idx, character in enumerate(data.characters): - characters_data.append( - { - "level": character.level, - "element": character.element.name, - "constellation": character.constellations_unlocked, - "rarity": character.rarity, - "icon": (await self.assets_service.avatar(character.id).icon()).as_uri(), - } - ) - if idx > 6: - break - return { - "uid": mask_number(data.uid), - "level": data.player.level, - "signature": data.player.signature, - "characters": characters_data, - } - - -class Artifact(BaseModel): - """在 enka Equipments model 基础上扩展了圣遗物评分数据""" - - equipment: Equipments - # 圣遗物评分 - score: float = 0 - # 圣遗物评级 - score_label: str = "E" - # 圣遗物评级颜色 - score_class: str = "" - # 圣遗物单行属性评分 - substat_scores: List[float] - - def __init__(self, **kwargs): - super().__init__(**kwargs) - for substat_scores in self.substat_scores: - self.score += substat_scores - self.score = round(self.score, 1) - - for r in ( - ("D", 10), - ("C", 16.5), - ("B", 23.1), - ("A", 29.7), - ("S", 36.3), - ("SS", 42.9), - ("SSS", 49.5), - ("ACE", 56.1), - ("ACE²", 66), - ): - if self.score >= r[1]: - self.score_label = r[0] - self.score_class = self.get_score_class(r[0]) - - @staticmethod - def get_score_class(label: str) -> str: - mapping = { - "D": "text-neutral-400", - "C": "text-neutral-200", - "B": "text-violet-400", - "A": "text-violet-400", - "S": "text-yellow-400", - "SS": "text-yellow-400", - "SSS": "text-yellow-400", - "ACE": "text-red-500", - "ACE²": "text-red-500", - } - return mapping.get(label, "text-neutral-400") - - -class RenderTemplate: - def __init__( - self, - uid: Union[int, str], - character: "CharacterInfo", - fight_prop_rule: Dict[str, Dict[str, float]], - damage_config: Dict, - template_service: TemplateService, - original_data: Optional[Dict] = None, - ): - self.uid = uid - self.template_service = template_service - # 因为需要替换线上 enka 图片地址为本地地址,先克隆数据,避免修改原数据 - self.character = character.copy(deep=True) - self.fight_prop_rule = fight_prop_rule - self.original_data = original_data - self.damage_config = damage_config - - async def render(self): - # 缓存所有图片到本地 - await self.cache_images() - - artifacts = self.find_artifacts() - artifact_total_score: float = sum(artifact.score for artifact in artifacts) - - artifact_total_score = round(artifact_total_score, 1) - - artifact_total_score_label: str = "E" - for r in ( - ("D", 10), - ("C", 16.5), - ("B", 23.1), - ("A", 29.7), - ("S", 36.3), - ("SS", 42.9), - ("SSS", 49.5), - ("ACE", 56.1), - ("ACE²", 66), - ): - if artifact_total_score / 5 >= r[1]: - artifact_total_score_label = r[0] - - data = { - "uid": mask_number(self.uid), - "character": self.character, - "stats": await self.de_stats(), - "weapon": self.find_weapon(), - # 圣遗物评分 - "artifact_total_score": artifact_total_score, - # 圣遗物评级 - "artifact_total_score_label": artifact_total_score_label, - # 圣遗物评级颜色 - "artifact_total_score_class": Artifact.get_score_class(artifact_total_score_label), - "artifacts": artifacts, - # 需要在模板中使用的 enum 类型 - "DigitType": DigitType, - "damage_function_available": False, - "damage_info": [], - } - - if GENSHIN_ARTIFACT_FUNCTION_AVAILABLE: - character_cn_name = idToName(self.character.id) - damage_config = self.damage_config.get(character_cn_name) - if damage_config is not None: - try: - data["damage_info"] = self.render_damage(damage_config) - except JsonParseException as _exc: - logger.error(str(_exc)) - except EnkaParseException as _exc: - logger.error(str(_exc)) - else: - data["damage_function_available"] = True - - return await self.template_service.render( - "genshin/player_card/player_card.jinja2", - data, - full_page=True, - query_selector=".text-neutral-200", - ttl=7 * 24 * 60 * 60, - ) - - def render_damage(self, damage_config: Optional[Dict]) -> List: - character, weapon, artifacts = enka_parser(self.original_data, self.character.id) - character_name = character.name - character_cn_name = idToName(self.character.id) - if damage_config is None: - damage_config = self.damage_config.get(character_cn_name) - skills = damage_config.get("skills") - config_skill = damage_config.get("config_skill") - if config_skill is not None: - config_skill = {character_name: config_skill} - character_config = damage_config.get("config") - artifact_config = damage_config.get("artifact_config") - if character_config is not None: - character.params = {character_name: character_config} - config_weapon = damage_config.get("config_weapon") - if config_weapon is not None: - _weapon_config = config_weapon.get(weapon.name) - if _weapon_config is not None: - weapon.params = {weapon.name: _weapon_config} - damage = [] - for skill in skills: - index = skill.get("index") - skill_info = SkillInterface(index=index, config=config_skill) - calculator_config = CalculatorConfig( - character=character, - weapon=weapon, - artifacts=artifacts, - skill=skill_info, - artifact_config=artifact_config, - ) - damage_key = skill.get("damage_key") - transformative_damage_key = skill.get("transformative_damage_key") - damage_info = {"skill_info": skill, "damage": None, "transformative_damage": None} - if damage_key is not None: - damage_analysis = get_damage_analysis(calculator_config) - damage_value = getattr(damage_analysis, damage_key) - damage_info["damage"] = damage_value - if transformative_damage_key is not None: - transformative_damage = get_transformative_damage(calculator_config) - transformative_damage_value = getattr(transformative_damage, transformative_damage_key) - damage_info["transformative_damage"] = transformative_damage_value - damage.append(damage_info) - - return damage - - async def de_stats(self) -> List[Tuple[str, Any]]: - stats = self.character.stats - items: List[Tuple[str, Any]] = [] - logger.debug(self.character.stats) - - # items.append(("基础生命值", stats.BASE_HP.to_rounded())) - items.append(("生命值", stats.FIGHT_PROP_MAX_HP.to_rounded())) - # items.append(("基础攻击力", stats.FIGHT_PROP_BASE_ATTACK.to_rounded())) - items.append(("攻击力", stats.FIGHT_PROP_CUR_ATTACK.to_rounded())) - # items.append(("基础防御力", stats.FIGHT_PROP_BASE_DEFENSE.to_rounded())) - items.append(("防御力", stats.FIGHT_PROP_CUR_DEFENSE.to_rounded())) - items.append(("暴击率", stats.FIGHT_PROP_CRITICAL.to_percentage_symbol())) - items.append( - ( - "暴击伤害", - stats.FIGHT_PROP_CRITICAL_HURT.to_percentage_symbol(), - ) - ) - items.append( - ( - "元素充能效率", - stats.FIGHT_PROP_CHARGE_EFFICIENCY.to_percentage_symbol(), - ) - ) - items.append(("元素精通", stats.FIGHT_PROP_ELEMENT_MASTERY.to_rounded())) - - # 查找元素伤害加成和治疗加成 - max_stat = StatsPercentage() # 用于记录最高元素伤害加成 避免武器特效影响 - for stat in stats: - if 40 <= stat[1].id <= 46: # 元素伤害加成 - if max_stat.value <= stat[1].value: - max_stat = stat[1] - elif stat[1].id == 29: # 物理伤害加成 - pass - elif stat[1].id != 26: # 治疗加成 - continue - value = stat[1].to_rounded() if isinstance(stat[1], Stats) else stat[1].to_percentage_symbol() - if value in ("0%", 0): - continue - name = DEFAULT_EnkaAssets.get_hash_map(stat[0]) - if name is None: - continue - items.append((name, value)) - - if max_stat.id != 0: - for item in items: - if "元素伤害加成" in item[0] and max_stat.to_percentage_symbol() != item[1]: - items.remove(item) - - return items - - async def cache_images(self) -> None: - """缓存所有图片到本地""" - # TODO: 并发下载所有资源 - c = self.character - # 角色 - c.image.banner.url = await download_resource(c.image.banner.url) - - # 技能 - for item in c.skills: - item.icon.url = await download_resource(item.icon.url) - - # 命座 - for item in c.constellations: - item.icon.url = await download_resource(item.icon.url) - - # 装备,包括圣遗物和武器 - for item in c.equipments: - item.detail.icon.url = await download_resource(item.detail.icon.url) - - def find_weapon(self) -> Optional[Equipments]: - """在 equipments 数组中找到武器,equipments 数组包含圣遗物和武器""" - for item in self.character.equipments: - if item.type == EquipmentsType.WEAPON: - return item - return None - - def find_artifacts(self) -> List[Artifact]: - """在 equipments 数组中找到圣遗物,并转换成带有分数的 model。equipments 数组包含圣遗物和武器""" - - stats = ArtifactStatsTheory(self.character.name, self.fight_prop_rule) - - def substat_score(s: "EquipmentsStats") -> float: - return stats.theory(s) - - return [ - Artifact( - equipment=e, - # 圣遗物单行属性评分 - substat_scores=[substat_score(s) for s in e.detail.substats], - ) - for e in self.character.equipments - if e.type == EquipmentsType.ARTIFACT - ] diff --git a/plugins/genshin/quiz.py b/plugins/genshin/quiz.py deleted file mode 100644 index 4a193f2..0000000 --- a/plugins/genshin/quiz.py +++ /dev/null @@ -1,67 +0,0 @@ -import random - -from telegram import Poll, Update -from telegram.constants import ChatAction -from telegram.error import BadRequest -from telegram.ext import filters, CallbackContext - -from core.plugin import Plugin, handler -from core.services.quiz.services import QuizService -from core.services.users.services import UserService -from utils.log import logger - -__all__ = ("QuizPlugin",) - - -class QuizPlugin(Plugin): - """派蒙的十万个为什么""" - - def __init__(self, quiz_service: QuizService = None, user_service: UserService = None): - self.user_service = user_service - self.quiz_service = quiz_service - self.time_out = 120 - - @handler.message(filters=filters.Regex("来一道题")) - @handler.command(command="quiz", block=False) - async def command_start(self, update: Update, _: CallbackContext) -> None: - message = update.effective_message - chat = update.effective_chat - await message.reply_chat_action(ChatAction.TYPING) - question_id_list = await self.quiz_service.get_question_id_list() - if filters.ChatType.GROUPS.filter(message): - self.log_user(update, logger.info, "在群 %s[%s] 发送挑战问题命令请求", chat.title, chat.id) - if len(question_id_list) == 0: - return None - if len(question_id_list) == 0: - return None - question_id = random.choice(question_id_list) # nosec - question = await self.quiz_service.get_question(question_id) - _options = [] - correct_option = None - for answer in question.answers: - _options.append(answer.text) - if answer.is_correct: - correct_option = answer.text - if correct_option is None: - question_id = question["question_id"] - logger.warning("Quiz模块 correct_option 异常 question_id[%s]", question_id) - return None - random.shuffle(_options) - index = _options.index(correct_option) - try: - poll_message = await message.reply_poll( - question.text, - _options, - correct_option_id=index, - is_anonymous=False, - open_period=self.time_out, - type=Poll.QUIZ, - ) - except BadRequest as exc: - if "Not enough rights" in exc.message: - poll_message = await message.reply_text("出错了呜呜呜 ~ 权限不足,请请检查投票权限是否开启") - else: - raise exc - if filters.ChatType.GROUPS.filter(message): - self.add_delete_message_job(message, delay=300) - self.add_delete_message_job(poll_message, delay=300) diff --git a/plugins/genshin/reg_time.py b/plugins/genshin/reg_time.py deleted file mode 100644 index f95b3c8..0000000 --- a/plugins/genshin/reg_time.py +++ /dev/null @@ -1,96 +0,0 @@ -from datetime import datetime -from typing import TYPE_CHECKING - -from simnet.client.routes import InternationalRoute -from simnet.errors import BadRequest as SIMNetBadRequest -from simnet.utils.player import recognize_genshin_server, recognize_genshin_game_biz -from telegram.ext import filters - -from core.dependence.redisdb import RedisDB -from core.plugin import Plugin, handler -from core.services.cookies import CookiesService -from core.services.users.services import UserService -from plugins.tools.genshin import GenshinHelper -from utils.log import logger - -if TYPE_CHECKING: - from telegram import Update - from telegram.ext import ContextTypes - from simnet import GenshinClient - -try: - import ujson as jsonlib - -except ImportError: - import json as jsonlib - -REG_TIME_URL = InternationalRoute( - overseas="https://sg-hk4e-api.hoyoverse.com/event/e20220928anniversary/game_data", - chinese="https://hk4e-api.mihoyo.com/event/e20220928anniversary/game_data", -) - - -class NotFoundRegTimeError(Exception): - """未找到注册时间""" - - -class RegTimePlugin(Plugin): - """查询原神注册时间""" - - def __init__( - self, - user_service: UserService = None, - cookie_service: CookiesService = None, - helper: GenshinHelper = None, - redis: RedisDB = None, - ): - self.cache = redis.client - self.cache_key = "plugin:reg_time:" - self.user_service = user_service - self.cookie_service = cookie_service - self.helper = helper - - @staticmethod - async def get_reg_time(client: "GenshinClient") -> str: - """获取原神注册时间""" - game_biz = recognize_genshin_game_biz(client.player_id) - region = recognize_genshin_server(client.player_id) - await client.get_hk4e_token_by_cookie_token(game_biz, region) - url = REG_TIME_URL.get_url(client.region) - params = {"game_biz": game_biz, "lang": "zh-cn", "badge_uid": client.player_id, "badge_region": region} - data = await client.request_lab(url, method="GET", params=params) - if time := jsonlib.loads(data.get("data", "{}")).get("1", 0): - return datetime.fromtimestamp(time).strftime("%Y-%m-%d %H:%M:%S") - raise NotFoundRegTimeError - - async def get_reg_time_from_cache(self, client: "GenshinClient") -> str: - """从缓存中获取原神注册时间""" - if reg_time := await self.cache.get(f"{self.cache_key}{client.player_id}"): - return reg_time.decode("utf-8") - reg_time = await self.get_reg_time(client) - await self.cache.set(f"{self.cache_key}{client.player_id}", reg_time) - return reg_time - - @handler.command("reg_time", block=False) - @handler.message(filters.Regex(r"^原神账号注册时间$"), block=False) - async def reg_time(self, update: "Update", _: "ContextTypes.DEFAULT_TYPE") -> None: - user_id = await self.get_real_user_id(update) - uid, offset = self.get_real_uid_or_offset(update) - message = update.effective_message - self.log_user(update, logger.info, "原神注册时间命令请求") - try: - async with self.helper.genshin(user_id, player_id=uid, offset=offset) as client: - reg_time = await self.get_reg_time_from_cache(client) - await message.reply_text(f"你的原神账号注册时间为:{reg_time}") - except SIMNetBadRequest as exc: - if exc.ret_code == -501101: - await message.reply_text("当前角色冒险等阶未达到10级,暂时无法获取信息") - else: - raise exc - except ValueError as exc: - if "cookie_token" in str(exc): - await message.reply_text("呜呜呜出错了请重新绑定账号") - else: - raise exc - except NotFoundRegTimeError: - await message.reply_text("未找到你的原神账号注册时间,仅限 2022 年 10 月 之前注册的账号") diff --git a/plugins/genshin/strategy.py b/plugins/genshin/strategy.py deleted file mode 100644 index 5950552..0000000 --- a/plugins/genshin/strategy.py +++ /dev/null @@ -1,75 +0,0 @@ -from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update -from telegram.constants import ChatAction, ParseMode -from telegram.ext import CallbackContext, filters - -from core.plugin import Plugin, handler -from core.services.game.services import GameStrategyService -from core.services.search.models import StrategyEntry -from core.services.search.services import SearchServices -from metadata.shortname import roleToName, roleToTag -from utils.log import logger - - -class StrategyPlugin(Plugin): - """角色攻略查询""" - - KEYBOARD = [ - [InlineKeyboardButton(text="查看角色攻略列表并查询", switch_inline_query_current_chat="查看角色攻略列表并查询")] - ] - - def __init__( - self, - game_strategy_service: GameStrategyService = None, - search_service: SearchServices = None, - ): - self.game_strategy_service = game_strategy_service - self.search_service = search_service - - @handler.command(command="strategy", block=False) - @handler.message(filters=filters.Regex("^角色攻略查询(.*)"), block=False) - async def command_start(self, update: Update, context: CallbackContext) -> None: - message = update.effective_message - args = self.get_args(context) - if len(args) >= 1: - character_name = args[0] - else: - reply_message = await message.reply_text( - "请回复你要查询的攻略的角色名", reply_markup=InlineKeyboardMarkup(self.KEYBOARD) - ) - if filters.ChatType.GROUPS.filter(reply_message): - self.add_delete_message_job(message) - self.add_delete_message_job(reply_message) - return - character_name = roleToName(character_name) - url = await self.game_strategy_service.get_strategy(character_name) - if url == "": - reply_message = await message.reply_text( - f"没有找到 {character_name} 的攻略", reply_markup=InlineKeyboardMarkup(self.KEYBOARD) - ) - if filters.ChatType.GROUPS.filter(reply_message): - self.add_delete_message_job(message) - self.add_delete_message_job(reply_message) - return - self.log_user(update, logger.info, "查询角色攻略命令请求 || 参数 %s", character_name) - await message.reply_chat_action(ChatAction.UPLOAD_PHOTO) - file_path = await self.download_resource(url, return_path=True) - caption = f"From 米游社 西风驿站 查看原图" - reply_photo = await message.reply_photo( - photo=open(file_path, "rb"), - caption=caption, - filename=f"{character_name}.png", - parse_mode=ParseMode.HTML, - ) - if reply_photo.photo: - tags = roleToTag(character_name) - photo_file_id = reply_photo.photo[0].file_id - entry = StrategyEntry( - key=f"plugin:strategy:{character_name}", - title=character_name, - description=f"{character_name} 角色攻略", - tags=tags, - caption=caption, - parse_mode="HTML", - photo_file_id=photo_file_id, - ) - await self.search_service.add_entry(entry) diff --git a/plugins/genshin/weapon.py b/plugins/genshin/weapon.py deleted file mode 100644 index 3d37494..0000000 --- a/plugins/genshin/weapon.py +++ /dev/null @@ -1,145 +0,0 @@ -from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update -from telegram.constants import ChatAction -from telegram.ext import CallbackContext, CommandHandler, MessageHandler, filters - -from core.dependence.assets import AssetsCouldNotFound, AssetsService -from core.plugin import Plugin, handler -from core.services.search.models import WeaponEntry -from core.services.search.services import SearchServices -from core.services.template.services import TemplateService -from core.services.wiki.services import WikiService -from metadata.genshin import honey_id_to_game_id -from metadata.shortname import weaponToName, weapons as _weapons_data -from modules.wiki.weapon import Weapon -from utils.log import logger - - -class WeaponPlugin(Plugin): - """武器查询""" - - KEYBOARD = [ - [InlineKeyboardButton(text="查看武器列表并查询", switch_inline_query_current_chat="查看武器列表并查询")] - ] - - def __init__( - self, - template_service: TemplateService = None, - wiki_service: WikiService = None, - assets_service: AssetsService = None, - search_service: SearchServices = None, - ): - self.wiki_service = wiki_service - self.template_service = template_service - self.assets_service = assets_service - self.search_service = search_service - - @handler(CommandHandler, command="weapon", block=False) - @handler(MessageHandler, filters=filters.Regex("^武器查询(.*)"), block=False) - async def command_start(self, update: Update, context: CallbackContext) -> None: - message = update.effective_message - args = self.get_args(context) - if len(args) >= 1: - weapon_name = args[0] - else: - reply_message = await message.reply_text( - "请回复你要查询的武器", reply_markup=InlineKeyboardMarkup(self.KEYBOARD) - ) - if filters.ChatType.GROUPS.filter(reply_message): - self.add_delete_message_job(message) - self.add_delete_message_job(reply_message) - return - weapon_name = weaponToName(weapon_name) - self.log_user(update, logger.info, "查询角色攻略命令请求 weapon_name[%s]", weapon_name) - weapons_list = await self.wiki_service.get_weapons_list() - for weapon in weapons_list: - if weapon.name == weapon_name: - weapon_data = weapon - break - else: - reply_message = await message.reply_text( - f"没有找到 {weapon_name}", reply_markup=InlineKeyboardMarkup(self.KEYBOARD) - ) - if filters.ChatType.GROUPS.filter(reply_message): - self.add_delete_message_job(message) - self.add_delete_message_job(reply_message) - return - await message.reply_chat_action(ChatAction.TYPING) - - async def input_template_data(_weapon_data: Weapon): - if weapon.rarity > 2: - bonus = _weapon_data.stats[-1].bonus - if "%" in bonus: - bonus = str(round(float(bonus.rstrip("%")))) + "%" - else: - bonus = str(round(float(bonus))) - _template_data = { - "weapon_name": _weapon_data.name, - "weapon_rarity": _weapon_data.rarity, - "weapon_info_type_img": _weapon_data.weapon_type.name, - "progression_secondary_stat_value": bonus, - "progression_secondary_stat_name": _weapon_data.attribute.type.value, - "weapon_info_source_img": ( - await self.assets_service.weapon(honey_id_to_game_id(_weapon_data.id, "weapon")).icon() - ).as_uri(), - "weapon_info_max_level": _weapon_data.stats[-1].level, - "progression_base_atk": round(_weapon_data.stats[-1].ATK), - "weapon_info_source_list": [ - (await self.assets_service.material(honey_id_to_game_id(mid, "material")).icon()).as_uri() - for mid in _weapon_data.ascension[-3:] - ], - "special_ability_name": _weapon_data.affix.name, - "special_ability_info": _weapon_data.affix.description[0], - "weapon_description": _weapon_data.description, - } - else: - _template_data = { - "weapon_name": _weapon_data.name, - "weapon_rarity": _weapon_data.rarity, - "weapon_info_type_img": _weapon_data.weapon_type.name, - "progression_secondary_stat_value": " ", - "progression_secondary_stat_name": "无其它属性加成", - "weapon_info_source_img": ( - await self.assets_service.weapon(honey_id_to_game_id(_weapon_data.id, "weapon")).icon() - ).as_uri(), - "weapon_info_max_level": _weapon_data.stats[-1].level, - "progression_base_atk": round(_weapon_data.stats[-1].ATK), - "weapon_info_source_list": [ - (await self.assets_service.material(honey_id_to_game_id(mid, "material")).icon()).as_uri() - for mid in _weapon_data.ascension[-3:] - ], - "special_ability_name": "", - "special_ability_info": "", - "weapon_description": _weapon_data.description, - } - return _template_data - - try: - template_data = await input_template_data(weapon_data) - except AssetsCouldNotFound as exc: - logger.warning("%s weapon_name[%s]", exc.message, weapon_name) - reply_message = await message.reply_text(f"数据库中没有找到 {weapon_name}") - if filters.ChatType.GROUPS.filter(reply_message): - self.add_delete_message_job(message) - self.add_delete_message_job(reply_message) - return - png_data = await self.template_service.render( - "genshin/weapon/weapon.jinja2", template_data, {"width": 540, "height": 540}, ttl=31 * 24 * 60 * 60 - ) - await message.reply_chat_action(ChatAction.UPLOAD_PHOTO) - reply_photo = await png_data.reply_photo( - message, - filename=f"{template_data['weapon_name']}.png", - ) - if reply_photo.photo: - description = weapon_data.story - if description: - photo_file_id = reply_photo.photo[0].file_id - tags = _weapons_data.get(weapon_name) - entry = WeaponEntry( - key=f"plugin:weapon:{weapon_name}", - title=weapon_name, - description=description, - tags=tags, - photo_file_id=photo_file_id, - ) - await self.search_service.add_entry(entry) diff --git a/plugins/genshin/wish.py b/plugins/genshin/wish.py deleted file mode 100644 index 601052b..0000000 --- a/plugins/genshin/wish.py +++ /dev/null @@ -1,327 +0,0 @@ -import asyncio -import re -from datetime import datetime -from typing import Any, List, Optional, Tuple, Union - -from bs4 import BeautifulSoup -from telegram import Update -from telegram.constants import ChatAction -from telegram.ext import CallbackContext, CommandHandler, MessageHandler, filters - -from core.dependence.assets import AssetsService -from core.dependence.redisdb import RedisDB -from core.plugin import Plugin, handler -from core.services.template.services import TemplateService -from metadata.genshin import AVATAR_DATA, WEAPON_DATA, avatar_to_game_id, weapon_to_game_id -from metadata.shortname import weaponToName -from modules.apihelper.client.components.gacha import Gacha as GachaClient -from modules.apihelper.models.genshin.gacha import GachaInfo -from modules.wish.banner import GenshinBannerType, GachaBanner -from modules.wish.player.info import PlayerGachaInfo -from modules.wish.system import BannerSystem -from utils.log import logger - -try: - import ujson as jsonlib - -except ImportError: - import json as jsonlib - - -class GachaNotFound(Exception): - """卡池未找到""" - - def __init__(self, gacha_name: str): - self.gacha_name = gacha_name - super().__init__(f"{gacha_name} gacha not found") - - -class GachaDataFound(Exception): - """卡池数据未找到""" - - def __init__(self, item_id: int): - self.item_id = item_id - super().__init__(f"item_id[{item_id}] data not found") - - -class GachaRedis: - def __init__(self, redis: RedisDB): - self.client = redis.client - self.qname = "plugin:wish_simulator:" - self.ex = 60 * 60 * 24 - - async def get(self, user_id: int) -> PlayerGachaInfo: - data = await self.client.get(f"{self.qname}{user_id}") - if data is None: - return PlayerGachaInfo() - return PlayerGachaInfo(**jsonlib.loads(data)) - - async def set(self, user_id: int, player_gacha_info: PlayerGachaInfo): - value = player_gacha_info.json() - await self.client.set(f"{self.qname}{user_id}", value, ex=self.ex) - - -class WishSimulatorHandle: - def __init__(self): - self.hyperion = GachaClient() - - async def de_banner(self, gacha_id: str, gacha_type: int) -> Optional[GachaBanner]: - gacha_info = await self.hyperion.get_gacha_info(gacha_id) - banner = GachaBanner() - banner.banner_id = gacha_id - banner.title, banner.html_title = self.de_title(gacha_info["title"]) - r5_up_items = gacha_info.get("r5_up_items") - if r5_up_items is not None: - for r5_up_item in r5_up_items: - if r5_up_item["item_type"] == "角色": - banner.rate_up_items5.append(avatar_to_game_id(r5_up_item["item_name"])) - elif r5_up_item["item_type"] == "武器": - banner.rate_up_items5.append(weapon_to_game_id(r5_up_item["item_name"])) - r5_prob_list = gacha_info.get("r5_prob_list") - if r5_prob_list is not None: - for r5_prob in gacha_info.get("r5_prob_list", []): - if r5_prob["item_type"] == "角色": - banner.fallback_items5_pool1.append(avatar_to_game_id(r5_prob["item_name"])) - elif r5_prob["item_type"] == "武器": - banner.fallback_items5_pool1.append(weapon_to_game_id(r5_prob["item_name"])) - r4_up_items = gacha_info.get("r4_up_items") - if r4_up_items is not None: - for r4_up_item in r4_up_items: - if r4_up_item["item_type"] == "角色": - banner.rate_up_items4.append(avatar_to_game_id(r4_up_item["item_name"])) - elif r4_up_item["item_type"] == "武器": - banner.rate_up_items4.append(weapon_to_game_id(r4_up_item["item_name"])) - r4_prob_list = gacha_info.get("r4_prob_list") - if r4_prob_list is not None: - for r4_prob in r4_prob_list: - if r4_prob["item_type"] == "角色": - banner.fallback_items4_pool1.append(avatar_to_game_id(r4_prob["item_name"])) - elif r4_prob["item_type"] == "武器": - banner.fallback_items4_pool1.append(weapon_to_game_id(r4_prob["item_name"])) - if gacha_type in {301, 400}: - banner.wish_max_progress = 1 - banner.banner_type = GenshinBannerType.EVENT - banner.weight4 = ((1, 510), (8, 510), (10, 10000)) - banner.weight5 = ((1, 60), (73, 60), (90, 10000)) - elif gacha_type == 302: - banner.wish_max_progress = 2 - banner.banner_type = GenshinBannerType.WEAPON - banner.weight4 = ((1, 600), (7, 600), (10, 10000)) - banner.weight5 = ((1, 70), (62, 70), (90, 10000)) - else: - banner.banner_type = GenshinBannerType.STANDARD - return banner - - async def gacha_base_info(self, gacha_name: str = "角色活动", default: bool = False) -> GachaInfo: - gacha_list_info = await self.hyperion.get_gacha_list_info() - now = datetime.now() - for gacha in gacha_list_info: - if gacha.gacha_name == gacha_name and gacha.begin_time <= now <= gacha.end_time: - return gacha - else: # pylint: disable=W0120 - if default and len(gacha_list_info) > 0: - return gacha_list_info[0] - raise GachaNotFound(gacha_name) - - @staticmethod - def de_title(title: str) -> Union[Tuple[str, None], Tuple[str, Any]]: - title_html = BeautifulSoup(title, "lxml") - re_color = re.search(r"", title, flags=0) - if re_color is None: - return title_html.text, None - color = re_color[1] - title_html.color.name = "span" - title_html.span["style"] = f"color:#{color};" - return title_html.text, title_html.p - - -class WishSimulatorPlugin(Plugin): - """抽卡模拟器(非首模拟器/减寿模拟器)""" - - def __init__(self, assets: AssetsService, template_service: TemplateService, redis: RedisDB): - self.gacha_db = GachaRedis(redis) - self.handle = WishSimulatorHandle() - self.banner_system = BannerSystem() - self.template_service = template_service - self.banner_cache = {} - self._look = asyncio.Lock() - self.assets_service = assets - - async def get_banner(self, gacha_base_info: GachaInfo): - async with self._look: - banner = self.banner_cache.get(gacha_base_info.gacha_id) - if banner is None: - banner = await self.handle.de_banner(gacha_base_info.gacha_id, gacha_base_info.gacha_type) - self.banner_cache.setdefault(gacha_base_info.gacha_id, banner) - return banner - - async def de_item_list(self, item_list: List[int]) -> List[dict]: - gacha_item: List[dict] = [] - for item_id in item_list: - if item_id is None: - continue - if 10000 <= item_id <= 100000: - data = WEAPON_DATA.get(str(item_id)) - avatar = self.assets_service.weapon(item_id) - gacha = await avatar.gacha() - if gacha is None: - raise GachaDataFound(item_id) - data.setdefault("url", gacha.as_uri()) - gacha_item.append(data) - elif 10000000 <= item_id <= 19999999: - data = AVATAR_DATA.get(str(item_id)) - avatar = self.assets_service.avatar(item_id) - gacha = await avatar.gacha_card() - if gacha is None: - raise GachaDataFound(item_id) - data.setdefault("url", gacha.as_uri()) - gacha_item.append(data) - return gacha_item - - async def shutdown(self) -> None: - pass - # todo 目前清理消息无法执行 因为先停止Job导致无法获取全部信息 - # logger.info("正在清理消息") - # job_queue = self.application.telegram.job_queue - # jobs = job_queue.jobs() - # for job in jobs: - # if "wish_simulator" in job.name and not job.removed: - # logger.info("当前Job name %s", job.name) - # try: - # await job.run(job_queue.application) - # except CancelledError: - # continue - # except Exception as exc: - # logger.warning("执行失败 %", str(exc)) - # else: - # logger.info("Jobs为空") - # logger.success("清理卡池消息成功") - - @handler(CommandHandler, command="wish", block=False) - @handler(MessageHandler, filters=filters.Regex("^抽卡模拟器(.*)"), block=False) - async def command_start(self, update: Update, context: CallbackContext) -> None: - user_id = await self.get_real_user_id(update) - message = update.effective_message - args = self.get_args(context) - gacha_name = "角色活动" - if len(args) >= 1: - gacha_name = args[0] - if gacha_name not in ("角色活动-2", "武器活动", "常驻", "角色活动"): - for key, value in {"2": "角色活动-2", "武器": "武器活动", "普通": "常驻"}.items(): - if key == gacha_name: - gacha_name = value - break - try: - gacha_base_info = await self.handle.gacha_base_info(gacha_name) - except GachaNotFound as exc: - await message.reply_text( - f"没有找到名为 {exc.gacha_name} 的卡池,可能是卡池不存在或者卡池已经结束,请检查后重试。如果你想抽取默认卡池,请不要输入参数。" - ) - return - else: - try: - gacha_base_info = await self.handle.gacha_base_info(default=True) - except GachaNotFound: - await message.reply_text("当前卡池正在替换中,请稍后重试。") - return - self.log_user(update, logger.info, "抽卡模拟器命令请求 || 参数 %s", gacha_name) - # 用户数据储存和处理 - await message.reply_chat_action(ChatAction.TYPING) - banner = await self.get_banner(gacha_base_info) - player_gacha_info = await self.gacha_db.get(user_id) - # 检查 wish_item_id - if ( - banner.banner_type == GenshinBannerType.WEAPON - and player_gacha_info.event_weapon_banner.wish_item_id not in banner.rate_up_items5 - ): - player_gacha_info.event_weapon_banner.wish_item_id = 0 - # 执行抽卡 - item_list = self.banner_system.do_pulls(player_gacha_info, banner, 10) - try: - data = await self.de_item_list(item_list) - except GachaDataFound as exc: - logger.warning("角色 item_id[%s] 抽卡立绘未找到", exc.item_id) - reply_message = await message.reply_text("出错了呜呜呜 ~ 卡池部分数据未找到!") - if filters.ChatType.GROUPS.filter(message): - self.add_delete_message_job(reply_message, name="wish_simulator") - self.add_delete_message_job(message, name="wish_simulator") - return - player_gacha_banner_info = player_gacha_info.get_banner_info(banner) - template_data = { - "name": f"{self.get_real_user_name(update)}", - "info": gacha_name, - "banner_name": banner.html_title if banner.html_title else banner.title, - "banner_type": banner.banner_type.name, - "player_gacha_banner_info": player_gacha_banner_info, - "items": [], - "wish_name": "", - } - if player_gacha_banner_info.wish_item_id != 0: - weapon = WEAPON_DATA.get(str(player_gacha_banner_info.wish_item_id)) - if weapon is not None: - template_data["wish_name"] = weapon["name"] - await self.gacha_db.set(user_id, player_gacha_info) - - def take_rang(elem: dict): - return elem["rank"] - - data.sort(key=take_rang, reverse=True) - template_data["items"] = data - await message.reply_chat_action(ChatAction.UPLOAD_PHOTO) - png_data = await self.template_service.render( - "genshin/wish/wish.jinja2", template_data, {"width": 1157, "height": 603}, False - ) - - reply_message = await message.reply_photo(png_data.photo) - if filters.ChatType.GROUPS.filter(message): - self.add_delete_message_job(reply_message, name="wish_simulator") - self.add_delete_message_job(message, name="wish_simulator") - - @handler(CommandHandler, command="set_wish", block=False) - @handler(MessageHandler, filters=filters.Regex("^非首模拟器定轨(.*)"), block=False) - async def set_wish(self, update: Update, context: CallbackContext) -> None: - message = update.effective_message - user = update.effective_user - args = self.get_args(context) - try: - gacha_base_info = await self.handle.gacha_base_info("武器活动") - except GachaNotFound: - reply_message = await message.reply_text("当前还没有武器正在 UP,可能是卡池不存在或者卡池已经结束。") - if filters.ChatType.GROUPS.filter(reply_message): - self.add_delete_message_job(message, delay=30) - self.add_delete_message_job(reply_message, delay=30) - return - banner = await self.get_banner(gacha_base_info) - up_weapons = {} - for rate_up_items5 in banner.rate_up_items5: - weapon = WEAPON_DATA.get(str(rate_up_items5)) - if weapon is None: - continue - up_weapons[weapon["name"]] = rate_up_items5 - up_weapons_text = "当前 UP 武器有:" + "、".join(up_weapons.keys()) - if len(args) >= 1: - weapon_name = args[0] - else: - reply_message = await message.reply_text(f"输入的参数不正确,请输入需要定轨的武器名称。\n{up_weapons_text}") - if filters.ChatType.GROUPS.filter(reply_message): - self.add_delete_message_job(message, delay=30) - self.add_delete_message_job(reply_message, delay=30) - return - weapon_name = weaponToName(weapon_name) - player_gacha_info = await self.gacha_db.get(user.id) - if weapon_name in up_weapons: - player_gacha_info.event_weapon_banner.wish_item_id = up_weapons[weapon_name] - player_gacha_info.event_weapon_banner.failed_chosen_item_pulls = 0 - else: - reply_message = await message.reply_text( - f"输入的参数不正确,可能是没有名为 {weapon_name} 的武器或该武器不存在当前 UP 卡池中\n{up_weapons_text}" - ) - if filters.ChatType.GROUPS.filter(reply_message): - self.add_delete_message_job(message, delay=30) - self.add_delete_message_job(reply_message, delay=30) - return - await self.gacha_db.set(user.id, player_gacha_info) - reply_message = await message.reply_text(f"抽卡模拟器定轨 {weapon_name} 武器成功") - if filters.ChatType.GROUPS.filter(reply_message): - self.add_delete_message_job(message, delay=30) - self.add_delete_message_job(reply_message, delay=30) diff --git a/plugins/genshin/wish_log.py b/plugins/genshin/wish_log.py deleted file mode 100644 index 981d9a7..0000000 --- a/plugins/genshin/wish_log.py +++ /dev/null @@ -1,634 +0,0 @@ -from functools import partial -from io import BytesIO -from typing import Optional, TYPE_CHECKING, List, Union, Tuple -from urllib.parse import urlencode - -from aiofiles import open as async_open -from simnet import GenshinClient, Region -from simnet.models.genshin.wish import BannerType -from telegram import InlineKeyboardButton, InlineKeyboardMarkup, ReplyKeyboardMarkup, ReplyKeyboardRemove -from telegram.constants import ChatAction -from telegram.error import BadRequest -from telegram.ext import ConversationHandler, filters -from telegram.helpers import create_deep_linked_url - -from core.basemodel import RegionEnum -from core.dependence.assets import AssetsService -from core.plugin import Plugin, conversation, handler -from core.services.cookies import CookiesService -from core.services.players import PlayersService -from core.services.template.models import FileType -from core.services.template.services import TemplateService -from gram_core.config import config -from gram_core.plugin.methods.inline_use_data import IInlineUseData -from metadata.scripts.paimon_moe import GACHA_LOG_PAIMON_MOE_PATH, update_paimon_moe_zh -from modules.gacha_log.const import UIGF_VERSION, GACHA_TYPE_LIST_REVERSE -from modules.gacha_log.error import ( - GachaLogAccountNotFound, - GachaLogAuthkeyTimeout, - GachaLogFileError, - GachaLogInvalidAuthkey, - GachaLogMixedProvider, - GachaLogNotFound, - PaimonMoeGachaLogFileError, -) -from modules.gacha_log.helpers import from_url_get_authkey -from modules.gacha_log.log import GachaLog -from modules.gacha_log.migrate import GachaLogMigrate -from modules.gacha_log.models import GachaLogInfo -from plugins.tools.genshin import PlayerNotFoundError -from plugins.tools.player_info import PlayerInfoSystem -from utils.log import logger - -try: - import ujson as jsonlib - -except ImportError: - import json as jsonlib - - -if TYPE_CHECKING: - from telegram import Update, Message, User, Document - from telegram.ext import ContextTypes - from gram_core.services.players.models import Player - from gram_core.services.template.models import RenderResult - -INPUT_URL, INPUT_FILE, CONFIRM_DELETE = range(10100, 10103) -WAITING = f"小{config.notice.bot_name}正在从服务器获取数据,请稍后" -WISHLOG_NOT_FOUND = f"{config.notice.bot_name}没有找到你的抽卡记录,快来私聊{config.notice.bot_name}导入吧~" - - -class WishLogPlugin(Plugin.Conversation): - """抽卡记录导入/导出/分析""" - - IMPORT_HINT = ( - "开始导入祈愿历史记录:请通过 https://paimon.moe/wish/import 获取抽卡记录链接后发送给我" - "(非 paimon.moe 导出的文件数据)\n\n" - f"> 你还可以向{config.notice.bot_name}发送从其他工具导出的 UIGF {UIGF_VERSION} 标准的记录文件\n" - "> 或者从 paimon.moe 、非小酋 导出的 xlsx 记录文件\n" - "> 在绑定 Cookie 时添加 stoken 可能有特殊效果哦(仅限国服)\n" - "注意:导入的数据将会与旧数据进行合并。" - ) - - def __init__( - self, - template_service: TemplateService, - players_service: PlayersService, - assets: AssetsService, - cookie_service: CookiesService, - player_info: PlayerInfoSystem, - ): - self.template_service = template_service - self.players_service = players_service - self.assets_service = assets - self.cookie_service = cookie_service - self.zh_dict = None - self.gacha_log = GachaLog() - self.player_info = player_info - self.wish_photo = None - - async def initialize(self) -> None: - await update_paimon_moe_zh(False) - async with async_open(GACHA_LOG_PAIMON_MOE_PATH, "r", encoding="utf-8") as load_f: - self.zh_dict = jsonlib.loads(await load_f.read()) - - async def get_player_id(self, user_id: int, player_id: int, offset: int) -> int: - """获取绑定的游戏ID""" - logger.debug("尝试获取已绑定的原神账号") - player = await self.players_service.get_player(user_id, player_id=player_id, offset=offset) - if player is None: - raise PlayerNotFoundError(user_id) - return player.player_id - - async def _refresh_user_data( - self, user: "User", player_id: int, data: dict = None, authkey: str = None, verify_uid: bool = True - ) -> str: - """刷新用户数据 - :param user: 用户 - :param data: 数据 - :param authkey: 认证密钥 - :return: 返回信息 - """ - try: - logger.debug("尝试获取已绑定的原神账号") - if authkey: - new_num = await self.gacha_log.get_gacha_log_data(user.id, player_id, authkey) - return "更新完成,本次没有新增数据" if new_num == 0 else f"更新完成,本次共新增{new_num}条抽卡记录" - if data: - new_num = await self.gacha_log.import_gacha_log_data(user.id, player_id, data, verify_uid) - return "更新完成,本次没有新增数据" if new_num == 0 else f"更新完成,本次共新增{new_num}条抽卡记录" - except GachaLogNotFound: - return WAITING - except GachaLogAccountNotFound: - return "导入失败,可能文件包含的祈愿记录所属 uid 与你当前绑定的 uid 不同" - except GachaLogFileError: - return "导入失败,数据格式错误" - except GachaLogInvalidAuthkey: - return "更新数据失败,authkey 无效" - except GachaLogAuthkeyTimeout: - return "更新数据失败,authkey 已经过期" - except GachaLogMixedProvider: - return "导入失败,你已经通过其他方式导入过抽卡记录了,本次无法导入" - except PlayerNotFoundError: - logger.info("未查询到用户 %s[%s] 所绑定的账号信息", user.full_name, user.id) - return config.notice.user_not_found - - async def import_from_file( - self, user: "User", player_id: int, message: "Message", document: "Optional[Document]" = None - ) -> None: - if not document: - document = message.document - # TODO: 使用 mimetype 判断文件类型 - if document.file_name.endswith(".xlsx"): - file_type = "xlsx" - elif document.file_name.endswith(".json"): - file_type = "json" - else: - await message.reply_text( - "文件格式错误,请发送符合 UIGF 标准的抽卡记录文件或者 paimon.moe、非小酋导出的 xlsx 格式的抽卡记录文件" - ) - return - if document.file_size > 5 * 1024 * 1024: - await message.reply_text("文件过大,请发送小于 5 MB 的文件") - return - try: - out = BytesIO() - await (await document.get_file()).download_to_memory(out=out) - if file_type == "json": - # bytesio to json - data = jsonlib.loads(out.getvalue().decode("utf-8")) - elif file_type == "xlsx": - data = self.gacha_log.convert_xlsx_to_uigf(out, self.zh_dict) - else: - await message.reply_text("文件解析失败,请检查文件") - return - except PaimonMoeGachaLogFileError as exc: - await message.reply_text( - f"导入失败,PaimonMoe的抽卡记录当前版本不支持\n支持抽卡记录的版本为 {exc.support_version},你的抽卡记录版本为 {exc.file_version}" - ) - return - except GachaLogFileError: - await message.reply_text(f"文件解析失败,请检查文件是否符合 UIGF {UIGF_VERSION} 标准") - return - except (KeyError, IndexError, ValueError): - await message.reply_text(f"文件解析失败,请检查文件编码是否正确或符合 UIGF {UIGF_VERSION} 标准") - return - except Exception as exc: - logger.error("文件解析失败 %s", repr(exc)) - await message.reply_text(f"文件解析失败,请检查文件是否符合 UIGF {UIGF_VERSION} 标准") - return - await message.reply_chat_action(ChatAction.TYPING) - reply = await message.reply_text("文件解析成功,正在导入数据") - await message.reply_chat_action(ChatAction.TYPING) - try: - text = await self._refresh_user_data(user, player_id, data=data, verify_uid=file_type == "json") - except Exception as exc: # pylint: disable=W0703 - logger.error("文件解析失败 %s", repr(exc)) - text = f"文件解析失败,请检查文件是否符合 UIGF {UIGF_VERSION} 标准" - await reply.edit_text(text) - - async def can_gen_authkey(self, user_id: int, player_id: int) -> bool: - player_info = await self.players_service.get_player(user_id, region=RegionEnum.HYPERION, player_id=player_id) - if player_info is not None: - cookies = await self.cookie_service.get(user_id, account_id=player_info.account_id) - if ( - cookies is not None - and cookies.data - and "stoken" in cookies.data - and next((value for key, value in cookies.data.items() if key in ["ltuid", "login_uid"]), None) - ): - return True - return False - - async def gen_authkey(self, uid: int) -> Optional[str]: - player_info = await self.players_service.get_player(uid, region=RegionEnum.HYPERION) - if player_info is not None: - cookies = await self.cookie_service.get(uid, account_id=player_info.account_id) - if cookies is not None and cookies.data and "stoken" in cookies.data: - if stuid := next((value for key, value in cookies.data.items() if key in ["ltuid", "login_uid"]), None): - cookies.data["stuid"] = stuid - async with GenshinClient( - cookies=cookies.data, region=Region.CHINESE, lang="zh-cn", player_id=player_info.player_id - ) as client: - return await client.get_authkey_by_stoken("webview_gacha") - - @conversation.entry_point - @handler.command(command="wish_log_import", filters=filters.ChatType.PRIVATE, block=False) - @handler.command(command="gacha_log_import", filters=filters.ChatType.PRIVATE, block=False) - @handler.message(filters=filters.Regex("^导入抽卡记录(.*)") & filters.ChatType.PRIVATE, block=False) - @handler.command(command="start", filters=filters.Regex("gacha_log_import$"), block=False) - async def command_start(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> int: - uid, offset = self.get_real_uid_or_offset(update) - message = update.effective_message - user = update.effective_user - player_id = await self.get_player_id(user.id, uid, offset) - context.chat_data["uid"] = player_id - logger.info("用户 %s[%s] 导入抽卡记录命令请求", user.full_name, user.id) - keyboard = None - if await self.can_gen_authkey(user.id, player_id): - keyboard = ReplyKeyboardMarkup([["自动导入"], ["退出"]], one_time_keyboard=True) - await message.reply_text(self.IMPORT_HINT, parse_mode="html", reply_markup=keyboard) - return INPUT_URL - - @conversation.state(state=INPUT_URL) - @handler.message(filters=~filters.COMMAND, block=False) - async def import_data_from_message(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> int: - message = update.effective_message - user = update.effective_user - player_id = context.chat_data["uid"] - if message.document: - await self.import_from_file(user, player_id, message) - return ConversationHandler.END - if not message.text: - await message.reply_text("请发送文件或链接") - return INPUT_URL - if message.text == "自动导入": - authkey = await self.gen_authkey(user.id) - if not authkey: - await message.reply_text( - "自动生成 authkey 失败,请尝试通过其他方式导入。", reply_markup=ReplyKeyboardRemove() - ) - return ConversationHandler.END - elif message.text == "退出": - await message.reply_text("取消导入抽卡记录", reply_markup=ReplyKeyboardRemove()) - return ConversationHandler.END - else: - authkey = from_url_get_authkey(message.text) - reply = await message.reply_text(WAITING, reply_markup=ReplyKeyboardRemove()) - await message.reply_chat_action(ChatAction.TYPING) - text = await self._refresh_user_data(user, player_id, authkey=authkey) - try: - await reply.delete() - except BadRequest: - pass - await message.reply_text(text) - return ConversationHandler.END - - @conversation.entry_point - @handler.command(command="wish_log_delete", filters=filters.ChatType.PRIVATE, block=False) - @handler.command(command="gacha_log_delete", filters=filters.ChatType.PRIVATE, block=False) - @handler.message(filters=filters.Regex("^删除抽卡记录(.*)") & filters.ChatType.PRIVATE, block=False) - async def command_start_delete(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> int: - uid, offset = self.get_real_uid_or_offset(update) - message = update.effective_message - user = update.effective_user - logger.info("用户 %s[%s] 删除抽卡记录命令请求", user.full_name, user.id) - try: - player_id = await self.get_player_id(user.id, uid, offset) - context.chat_data["uid"] = player_id - except PlayerNotFoundError: - logger.info("未查询到用户 %s[%s] 所绑定的账号信息", user.full_name, user.id) - await message.reply_text(config.notice.user_not_found) - return ConversationHandler.END - _, status = await self.gacha_log.load_history_info(str(user.id), str(player_id), only_status=True) - if not status: - await message.reply_text("你还没有导入抽卡记录哦~") - return ConversationHandler.END - await message.reply_text( - "你确定要删除抽卡记录吗?(此项操作无法恢复),如果确定请发送 ”确定“,发送其他内容取消" - ) - return CONFIRM_DELETE - - @conversation.state(state=CONFIRM_DELETE) - @handler.message(filters=filters.TEXT & ~filters.COMMAND, block=False) - async def command_confirm_delete(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> int: - message = update.effective_message - user = update.effective_user - if message.text == "确定": - status = await self.gacha_log.remove_history_info(str(user.id), str(context.chat_data["uid"])) - await message.reply_text("抽卡记录已删除" if status else "抽卡记录删除失败") - return ConversationHandler.END - await message.reply_text("已取消") - return ConversationHandler.END - - @handler.command(command="wish_log_force_delete", block=False, admin=True) - @handler.command(command="gacha_log_force_delete", block=False, admin=True) - async def command_gacha_log_force_delete(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE"): - uid, offset = self.get_real_uid_or_offset(update) - message = update.effective_message - user = update.effective_user - logger.info("用户 %s[%s] 强制删除抽卡记录命令请求", user.full_name, user.id) - args = self.get_args(context) - if not args: - await message.reply_text("请指定用户ID") - return - try: - cid = int(args[0]) - if cid < 0: - raise ValueError("Invalid cid") - player_id = await self.get_player_id(cid, uid, offset) - _, status = await self.gacha_log.load_history_info(str(cid), str(player_id), only_status=True) - if not status: - await message.reply_text("该用户还没有导入抽卡记录") - return - status = await self.gacha_log.remove_history_info(str(cid), str(player_id)) - await message.reply_text("抽卡记录已强制删除" if status else "抽卡记录删除失败") - except GachaLogNotFound: - await message.reply_text("该用户还没有导入抽卡记录") - except PlayerNotFoundError: - await message.reply_text("该用户暂未绑定账号") - except (ValueError, IndexError): - await message.reply_text("用户ID 不合法") - - @handler.command(command="wish_log_export", filters=filters.ChatType.PRIVATE, block=False) - @handler.command(command="gacha_log_export", filters=filters.ChatType.PRIVATE, block=False) - @handler.message(filters=filters.Regex("^导出抽卡记录(.*)") & filters.ChatType.PRIVATE, block=False) - async def command_start_export(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> None: - uid, offset = self.get_real_uid_or_offset(update) - message = update.effective_message - user = update.effective_user - logger.info("用户 %s[%s] 导出抽卡记录命令请求", user.full_name, user.id) - try: - player_id = await self.get_player_id(user.id, uid, offset) - await message.reply_chat_action(ChatAction.TYPING) - path = await self.gacha_log.gacha_log_to_uigf(str(user.id), str(player_id)) - await message.reply_chat_action(ChatAction.UPLOAD_DOCUMENT) - await message.reply_document(document=open(path, "rb+"), caption=f"抽卡记录导出文件 - UIGF {UIGF_VERSION}") - except GachaLogNotFound: - logger.info("未找到用户 %s[%s] 的抽卡记录", user.full_name, user.id) - buttons = [ - [InlineKeyboardButton("点我导入", url=create_deep_linked_url(context.bot.username, "gacha_log_import"))] - ] - await message.reply_text(WISHLOG_NOT_FOUND, reply_markup=InlineKeyboardMarkup(buttons)) - except GachaLogAccountNotFound: - await message.reply_text("导入失败,可能文件包含的祈愿记录所属 uid 与你当前绑定的 uid 不同") - except GachaLogFileError: - await message.reply_text("导入失败,数据格式错误") - except PlayerNotFoundError: - logger.info("未查询到用户 %s[%s] 所绑定的账号信息", user.full_name, user.id) - await message.reply_text(config.notice.user_not_found) - - @handler.command(command="wish_log_url", filters=filters.ChatType.PRIVATE, block=False) - @handler.command(command="gacha_log_url", filters=filters.ChatType.PRIVATE, block=False) - @handler.message(filters=filters.Regex("^抽卡记录链接(.*)") & filters.ChatType.PRIVATE, block=False) - async def command_start_url(self, update: "Update", _: "ContextTypes.DEFAULT_TYPE") -> None: - message = update.effective_message - user = update.effective_user - logger.info("用户 %s[%s] 生成抽卡记录链接命令请求", user.full_name, user.id) - authkey = await self.gen_authkey(user.id) - if not authkey: - await message.reply_text("生成失败,仅国服且绑定 stoken 的用户才能生成抽卡记录链接") - else: - url = "https://hk4e-api.mihoyo.com/event/gacha_info/api/getGachaLog" - params = { - "authkey_ver": 1, - "lang": "zh-cn", - "gacha_type": 301, - "authkey": authkey, - } - await message.reply_text(f"{url}?{urlencode(params)}", disable_web_page_preview=True) - - async def rander_wish_log_analysis( - self, user_id: int, player_id: int, pool_type: BannerType - ) -> Union[str, "RenderResult"]: - data = await self.gacha_log.get_analysis(user_id, player_id, pool_type, self.assets_service) - if isinstance(data, str): - return data - name_card = await self.player_info.get_name_card(player_id, user_id) - data["name_card"] = name_card - png_data = await self.template_service.render( - "genshin/wish_log/wish_log.jinja2", - data, - full_page=True, - file_type=FileType.DOCUMENT if len(data.get("fiveLog")) > 300 else FileType.PHOTO, - query_selector=".body_box", - ) - return png_data - - @staticmethod - def gen_button(user_id: int, uid: int, info: "GachaLogInfo") -> List[List[InlineKeyboardButton]]: - buttons = [] - pools = [] - skip_pools = [] - for k, v in info.item_list.items(): - if k in skip_pools: - continue - if not v: - continue - pools.append(k) - # 2 个一组 - for i in range(0, len(pools), 2): - row = [] - for pool in pools[i : i + 2]: - for k, v in {"log": "", "count": "(按卡池)"}.items(): - row.append( - InlineKeyboardButton( - f"{pool.replace('祈愿', '')}{v}", - callback_data=f"get_wish_log|{user_id}|{uid}|{k}|{pool}", - ) - ) - buttons.append(row) - buttons.append([InlineKeyboardButton("五星抽卡统计", callback_data=f"get_wish_log|{user_id}|{uid}|count|five")]) - return buttons - - async def wish_log_pool_choose(self, user_id: int, player_id: int, message: "Message"): - await message.reply_chat_action(ChatAction.TYPING) - gacha_log, status = await self.gacha_log.load_history_info(str(user_id), str(player_id)) - if not status: - raise GachaLogNotFound - buttons = self.gen_button(user_id, player_id, gacha_log) - if isinstance(self.wish_photo, str): - photo = self.wish_photo - else: - photo = open("resources/img/wish.jpg", "rb") - await message.reply_chat_action(ChatAction.UPLOAD_PHOTO) - reply_message = await message.reply_photo( - photo=photo, - caption="请选择你要查询的卡池", - reply_markup=InlineKeyboardMarkup(buttons), - ) - if reply_message.photo: - self.wish_photo = reply_message.photo[-1].file_id - - async def wish_log_pool_send(self, user_id: int, uid: int, pool_type: "BannerType", message: "Message"): - await message.reply_chat_action(ChatAction.TYPING) - png_data = await self.rander_wish_log_analysis(user_id, uid, pool_type) - if isinstance(png_data, str): - reply = await message.reply_text(png_data) - if filters.ChatType.GROUPS.filter(message): - self.add_delete_message_job(reply) - self.add_delete_message_job(message) - else: - await message.reply_chat_action(ChatAction.UPLOAD_PHOTO) - if png_data.file_type == FileType.DOCUMENT: - await png_data.reply_document(message, filename="抽卡统计.png") - else: - await png_data.reply_photo(message) - - @handler.command(command="wish_log", block=False) - @handler.command(command="gacha_log", block=False) - @handler.message(filters=filters.Regex("^抽卡记录?(武器|角色|常驻|)$"), block=False) - async def command_start_analysis(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> None: - user_id = await self.get_real_user_id(update) - uid, offset = self.get_real_uid_or_offset(update) - message = update.effective_message - pool_type = None - if args := self.get_args(context): - if "角色" in args: - pool_type = BannerType.CHARACTER1 - elif "武器" in args: - pool_type = BannerType.WEAPON - elif "常驻" in args: - pool_type = BannerType.STANDARD - elif "集录" in args: - pool_type = BannerType.CHRONICLED - self.log_user(update, logger.info, "抽卡记录命令请求 || 参数 %s", pool_type.name if pool_type else None) - try: - player_id = await self.get_player_id(user_id, uid, offset) - if pool_type is None: - await self.wish_log_pool_choose(user_id, player_id, message) - else: - await self.wish_log_pool_send(user_id, player_id, pool_type, message) - except GachaLogNotFound: - self.log_user(update, logger.info, "未找到抽卡记录") - buttons = [ - [InlineKeyboardButton("点我导入", url=create_deep_linked_url(context.bot.username, "gacha_log_import"))] - ] - await message.reply_text(WISHLOG_NOT_FOUND, reply_markup=InlineKeyboardMarkup(buttons)) - - @handler.callback_query(pattern=r"^get_wish_log\|", block=False) - async def get_wish_log(self, update: "Update", _: "ContextTypes.DEFAULT_TYPE") -> None: - callback_query = update.callback_query - user = callback_query.from_user - message = callback_query.message - - async def get_wish_log_callback( - callback_query_data: str, - ) -> Tuple[str, str, int, int]: - _data = callback_query_data.split("|") - _user_id = int(_data[1]) - _uid = int(_data[2]) - _t = _data[3] - _result = _data[4] - logger.debug( - "callback_query_data函数返回 result[%s] user_id[%s] uid[%s] show_type[%s]", - _result, - _user_id, - _uid, - _t, - ) - return _result, _t, _user_id, _uid - - try: - pool, show_type, user_id, uid = await get_wish_log_callback(callback_query.data) - except IndexError: - await callback_query.answer("按钮数据已过期,请重新获取。", show_alert=True) - self.add_delete_message_job(message, delay=1) - return - if user.id != user_id: - await callback_query.answer(text="这不是你的按钮!\n" + config.notice.user_mismatch, show_alert=True) - return - if show_type == "count": - await self.get_wish_log_count(update, user_id, uid, pool) - else: - await self.get_wish_log_log(update, user_id, uid, pool) - - async def get_wish_log_log(self, update: "Update", user_id: int, uid: int, pool: str): - callback_query = update.callback_query - message = callback_query.message - - pool_type = GACHA_TYPE_LIST_REVERSE.get(pool) - await message.reply_chat_action(ChatAction.TYPING) - try: - png_data = await self.rander_wish_log_analysis(user_id, uid, pool_type) - except GachaLogNotFound: - png_data = "未找到抽卡记录" - if isinstance(png_data, str): - await callback_query.answer(png_data, show_alert=True) - self.add_delete_message_job(message, delay=1) - else: - await callback_query.answer(text="正在渲染图片中 请稍等 请不要重复点击按钮", show_alert=False) - await message.reply_chat_action(ChatAction.UPLOAD_PHOTO) - if png_data.file_type == FileType.DOCUMENT: - await png_data.reply_document(message, filename="抽卡统计.png") - self.add_delete_message_job(message, delay=1) - else: - await png_data.edit_media(message) - - async def get_wish_log_count(self, update: "Update", user_id: int, uid: int, pool: str): - callback_query = update.callback_query - message = callback_query.message - - all_five = pool == "five" - group = filters.ChatType.GROUPS.filter(message) - pool_type = GACHA_TYPE_LIST_REVERSE.get(pool) - await message.reply_chat_action(ChatAction.TYPING) - try: - if all_five: - png_data = await self.gacha_log.get_all_five_analysis(user_id, uid, self.assets_service) - else: - png_data = await self.gacha_log.get_pool_analysis(user_id, uid, pool_type, self.assets_service, group) - except GachaLogNotFound: - png_data = "未找到抽卡记录" - if isinstance(png_data, str): - await callback_query.answer(png_data, show_alert=True) - self.add_delete_message_job(message, delay=1) - else: - await callback_query.answer(text="正在渲染图片中 请稍等 请不要重复点击按钮", show_alert=False) - name_card = await self.player_info.get_name_card(uid, user_id) - document = False - if png_data["hasMore"] and not group: - document = True - png_data["hasMore"] = False - png_data["name_card"] = name_card - await message.reply_chat_action(ChatAction.UPLOAD_DOCUMENT if document else ChatAction.UPLOAD_PHOTO) - png = await self.template_service.render( - "genshin/wish_count/wish_count.jinja2", - png_data, - full_page=True, - query_selector=".body_box", - file_type=FileType.DOCUMENT if document else FileType.PHOTO, - ) - await message.reply_chat_action(ChatAction.UPLOAD_PHOTO) - if document: - await png.reply_document(message, filename="抽卡统计.png") - self.add_delete_message_job(message, delay=1) - else: - await png.edit_media(message) - - @staticmethod - async def get_migrate_data( - old_user_id: int, new_user_id: int, old_players: List["Player"] - ) -> Optional[GachaLogMigrate]: - return await GachaLogMigrate.create(old_user_id, new_user_id, old_players) - - async def wish_log_use_by_inline( - self, update: "Update", context: "ContextTypes.DEFAULT_TYPE", pool_type: "BannerType" - ): - 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, "抽卡记录命令请求 || 参数 %s", pool_type.name if pool_type else None) - notice = None - try: - render_result = await self.rander_wish_log_analysis(user_id, uid, pool_type) - if isinstance(render_result, str): - notice = render_result - else: - await render_result.edit_inline_media(callback_query, filename="抽卡统计.png") - except GachaLogNotFound: - self.log_user(update, logger.info, "未找到抽卡记录") - notice = "未找到抽卡记录" - if notice: - await callback_query.answer(notice, show_alert=True) - - async def get_inline_use_data(self) -> List[Optional[IInlineUseData]]: - types = { - "角色": BannerType.CHARACTER1, - "武器": BannerType.WEAPON, - "常驻": BannerType.STANDARD, - "集录": BannerType.CHRONICLED, - } - data = [] - for k, v in types.items(): - data.append( - IInlineUseData( - text=f"{k}祈愿", - hash=f"wish_log_{v.value}", - callback=partial(self.wish_log_use_by_inline, pool_type=v), - player=True, - ) - ) - return data diff --git a/plugins/group/captcha.py b/plugins/group/captcha.py deleted file mode 100644 index 007bc16..0000000 --- a/plugins/group/captcha.py +++ /dev/null @@ -1,465 +0,0 @@ -import asyncio -import random -import time -from typing import Tuple, Union, Optional, TYPE_CHECKING - -from telegram import InlineKeyboardButton, InlineKeyboardMarkup, ChatPermissions, ChatMember, Message, User -from telegram.constants import ParseMode -from telegram.error import BadRequest -from telegram.ext import ChatMemberHandler, filters -from telegram.helpers import escape_markdown - -from core.config import config -from core.dependence.mtproto import MTProto -from core.dependence.redisdb import RedisDB -from core.handler.callbackqueryhandler import CallbackQueryHandler -from core.plugin import Plugin, handler -from core.services.quiz.services import QuizService -from plugins.tools.chat_administrators import ChatAdministrators -from utils.chatmember import extract_status_change -from utils.log import logger - -if TYPE_CHECKING: - from telegram.ext import ContextTypes - from telegram import Update - -try: - from pyrogram.errors import BadRequest as MTPBadRequest, FloodWait as MTPFloodWait - - PYROGRAM_AVAILABLE = True -except ImportError: - MTPBadRequest = ValueError - MTPFloodWait = IndexError - PYROGRAM_AVAILABLE = False - -try: - import ujson as jsonlib - -except ImportError: - import json as jsonlib - - -class GroupCaptcha(Plugin): - """群验证模块""" - - def __init__(self, quiz_service: QuizService = None, mtp: MTProto = None, redis: RedisDB = None): - self.quiz_service = quiz_service - self.time_out = 120 - self.kick_time = 120 - self.mtp = mtp.client - self.cache = redis.client - self.ttl = 60 * 60 - self.verify_groups = config.verify_groups - self.user_mismatch = config.notice.user_mismatch - - async def initialize(self): - logger.info("群验证模块正在刷新问题列表") - await self.quiz_service.refresh_quiz() - logger.success("群验证模块刷新问题列表成功") - - @staticmethod - def mention_markdown(user_id: Union[int, str], version: int = 1) -> str: - tg_link = f"tg://user?id={user_id}" - if version == 1: - return f"[{user_id}]({tg_link})" - return f"[{escape_markdown(user_id, version=version)}]({tg_link})" - - async def kick_member_job(self, context: "ContextTypes.DEFAULT_TYPE"): - job = context.job - logger.info("踢出用户 user_id[%s] 在 chat_id[%s]", job.user_id, job.chat_id) - try: - await context.bot.ban_chat_member( - chat_id=job.chat_id, user_id=job.user_id, until_date=int(time.time()) + self.kick_time - ) - except BadRequest as exc: - logger.error( - "GroupCaptcha插件在 chat_id[%s] user_id[%s] 执行kick失败", job.chat_id, job.user_id, exc_info=exc - ) - - @staticmethod - async def clean_message_job(context: "ContextTypes.DEFAULT_TYPE"): - job = context.job - logger.debug("删除消息 chat_id[%s] 的 message_id[%s]", job.chat_id, job.data) - try: - await context.bot.delete_message(chat_id=job.chat_id, message_id=job.data) - except BadRequest as exc: - if "not found" in exc.message: - logger.warning( - "GroupCaptcha插件删除消息 chat_id[%s] message_id[%s]失败 消息不存在", job.chat_id, job.data - ) - elif "Message can't be deleted" in exc.message: - logger.warning( - "GroupCaptcha插件删除消息 chat_id[%s] message_id[%s]失败 消息无法删除 可能是没有授权", - job.chat_id, - job.data, - ) - else: - logger.error( - "GroupCaptcha插件删除消息 chat_id[%s] message_id[%s]失败", job.chat_id, job.data, exc_info=exc - ) - - @staticmethod - async def restore_member(context: "ContextTypes.DEFAULT_TYPE", chat_id: int, user_id: int): - logger.debug("重置用户权限 user_id[%s] 在 chat_id[%s]", chat_id, user_id) - try: - await context.bot.restrict_chat_member( - chat_id=chat_id, user_id=user_id, permissions=ChatPermissions.all_permissions() - ) - except BadRequest as exc: - logger.error("GroupCaptcha插件在 chat_id[%s] user_id[%s] 执行restore失败", chat_id, user_id, exc_info=exc) - - async def get_new_chat_members_message(self, user: User, context: "ContextTypes.DEFAULT_TYPE") -> Optional[Message]: - qname = f"plugin:group_captcha:new_chat_members_message:{user.id}" - result = await self.cache.get(qname) - if result: - data = jsonlib.loads(str(result, encoding="utf-8")) - return Message.de_json(data, context.bot) - return None - - async def set_new_chat_members_message(self, user: User, message: Message): - qname = f"plugin:group_captcha:new_chat_members_message:{user.id}" - await self.cache.set(qname, message.to_json(), ex=60) - - @handler(CallbackQueryHandler, pattern=r"^auth_admin\|", block=False) - async def admin(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> None: - async def admin_callback(callback_query_data: str) -> Tuple[str, int]: - _data = callback_query_data.split("|") - _result = _data[1] - _user_id = int(_data[2]) - logger.debug("admin_callback函数返回 result[%s] user_id[%s]", _result, _user_id) - return _result, _user_id - - callback_query = update.callback_query - user = callback_query.from_user - message = callback_query.message - chat = message.chat - logger.info("用户 %s[%s] 在群 %s[%s] 点击Auth管理员命令", user.full_name, user.id, chat.title, chat.id) - chat_administrators = await ChatAdministrators.get_chat_administrators(self.cache, context, chat_id=chat.id) - if not ChatAdministrators.is_admin(chat_administrators, user.id): - logger.debug("用户 %s[%s] 在群 %s[%s] 非群管理", user.full_name, user.id, chat.title, chat.id) - await callback_query.answer(text="你不是管理!\n" + self.user_mismatch, show_alert=True) - return - result, user_id = await admin_callback(callback_query.data) - try: - member_info = await context.bot.get_chat_member(chat.id, user_id) - except BadRequest as error: - logger.warning("获取用户 %s 在群 %s[%s] 信息失败 \n %s", user_id, chat.title, chat.id, error.message) - member_info = f"{user_id}" - - if result == "pass": - await callback_query.answer(text="放行", show_alert=False) - await self.restore_member(context, chat.id, user_id) - if schedule := context.job_queue.scheduler.get_job(f"{chat.id}|{user_id}|auth_kick"): - schedule.remove() - if isinstance(member_info, ChatMember): - await message.edit_text( - f"{member_info.user.mention_markdown_v2()} 被本群管理员放行", parse_mode=ParseMode.MARKDOWN_V2 - ) - logger.info( - "用户 %s[%s] 在群 %s[%s] 被 %s[%s] 放行", - member_info.user.full_name, - member_info.user.id, - chat.title, - chat.id, - user.full_name, - user.id, - ) - else: - await message.edit_text(f"{member_info} 被本群管理员放行", parse_mode=ParseMode.MARKDOWN_V2) - logger.info( - "用户 %s 在群 %s[%s] 被 %s[%s] 管理放行", member_info, chat.title, chat.id, user.full_name, user.id - ) - elif result == "kick": - await callback_query.answer(text="驱离", show_alert=False) - await context.bot.ban_chat_member(chat.id, user_id) - if isinstance(member_info, ChatMember): - await message.edit_text( - f"{self.mention_markdown(member_info.user.id)} 被本群管理员驱离", parse_mode=ParseMode.MARKDOWN_V2 - ) - logger.info( - "用户 %s[%s] 在群 %s[%s] 被 %s[%s] 被管理驱离", - member_info.user.full_name, - member_info.user.id, - chat.title, - chat.id, - user.full_name, - user.id, - ) - else: - await message.edit_text(f"{member_info} 被本群管理员驱离", parse_mode=ParseMode.MARKDOWN_V2) - logger.info( - "用户 %s 在群 %s[%s] 被 %s[%s] 管理驱离", member_info, chat.title, chat.id, user.full_name, user.id - ) - elif result == "unban": - await callback_query.answer(text="解除驱离", show_alert=False) - await self.restore_member(context, chat.id, user_id) - if schedule := context.job_queue.scheduler.get_job(f"{chat.id}|{user_id}|auth_kick"): - schedule.remove() - if isinstance(member_info, ChatMember): - await message.edit_text( - f"{member_info.user.mention_markdown_v2()} 被本群管理员解除封禁", parse_mode=ParseMode.MARKDOWN_V2 - ) - logger.info( - "用户 %s[%s] 在群 %s[%s] 被 %s[%s] 解除封禁", - member_info.user.full_name, - member_info.user.id, - chat.title, - chat.id, - user.full_name, - user.id, - ) - else: - await message.edit_text(f"{member_info} 被本群管理员解除封禁", parse_mode=ParseMode.MARKDOWN_V2) - logger.info( - "用户 %s 在群 %s[%s] 被 %s[%s] 管理驱离", member_info, chat.title, chat.id, user.full_name, user.id - ) - else: - logger.warning("auth 模块 admin 函数 发现未知命令 result[%s]", result) - await context.bot.send_message(chat.id, f"{config.notice.bot_name}这边收到了错误的消息!请检查详细日记!") - if schedule := context.job_queue.scheduler.get_job(f"{chat.id}|{user_id}|auth_kick"): - schedule.remove() - - @handler(CallbackQueryHandler, pattern=r"^auth_challenge\|", block=False) - async def query(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> None: - async def query_callback(callback_query_data: str) -> Tuple[int, bool, str, str]: - _data = callback_query_data.split("|") - _user_id = int(_data[1]) - _question_id = int(_data[2]) - _answer_id = int(_data[3]) - _answer = await self.quiz_service.get_answer(_answer_id) - _question = await self.quiz_service.get_question(_question_id) - _result = _answer.is_correct - _answer_encode = _answer.text - _question_encode = _question.text - logger.debug( - "query_callback函数返回 user_id[%s] result[%s] \nquestion_encode[%s] answer_encode[%s]", - _user_id, - _result, - _question_encode, - _answer_encode, - ) - return _user_id, _result, _question_encode, _answer_encode - - callback_query = update.callback_query - user = callback_query.from_user - message = callback_query.message - chat = message.chat - user_id, result, question, answer = await query_callback(callback_query.data) - logger.info("用户 %s[%s] 在群 %s[%s] 点击Auth认证命令", user.full_name, user.id, chat.title, chat.id) - if user.id != user_id: - await callback_query.answer(text="这不是你的验证!\n" + self.user_mismatch, show_alert=True) - return - logger.info( - "用户 %s[%s] 在群 %s[%s] 认证结果为 %s", - user.full_name, - user.id, - chat.title, - chat.id, - "通过" if result else "失败", - ) - if result: - buttons = [[InlineKeyboardButton("驱离", callback_data=f"auth_admin|kick|{user.id}")]] - await callback_query.answer(text="验证成功", show_alert=False) - await self.restore_member(context, chat.id, user_id) - if schedule := context.job_queue.scheduler.get_job(f"{chat.id}|{user.id}|auth_kick"): - schedule.remove() - text = ( - f"{user.mention_markdown_v2()} 验证成功,向着星辰与深渊!\n" - f"问题:{escape_markdown(question, version=2)} \n" - f"回答:{escape_markdown(answer, version=2)}" - ) - logger.info("用户 user_id[%s] 在群 %s[%s] 验证成功", user_id, chat.title, chat.id) - else: - buttons = [ - [ - InlineKeyboardButton("驱离", callback_data=f"auth_admin|kick|{user.id}"), - InlineKeyboardButton("撤回驱离", callback_data=f"auth_admin|unban|{user.id}"), - ] - ] - await callback_query.answer(text=f"验证失败,请在 {self.time_out} 秒后重试", show_alert=True) - await asyncio.sleep(3) - await context.bot.ban_chat_member( - chat_id=chat.id, user_id=user_id, until_date=int(time.time()) + self.kick_time - ) - text = ( - f"{user.mention_markdown_v2()} 验证失败,已经赶出提瓦特大陆!\n" - f"问题:{escape_markdown(question, version=2)} \n" - f"回答:{escape_markdown(answer, version=2)}" - ) - logger.info("用户 user_id[%s] 在群 %s[%s] 验证失败", user_id, chat.title, chat.id) - try: - await message.edit_text(text, reply_markup=InlineKeyboardMarkup(buttons), parse_mode=ParseMode.MARKDOWN_V2) - except BadRequest as exc: - if "are exactly the same as " in exc.message: - logger.warning("编辑消息发生异常,可能为用户点按多次键盘导致") - else: - raise exc - if schedule := context.job_queue.scheduler.get_job(f"{chat.id}|{user.id}|auth_kick"): - schedule.remove() - - @handler.message(filters=filters.StatusUpdate.NEW_CHAT_MEMBERS, block=False) - async def new_mem(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> None: - message = update.effective_message - chat = message.chat - if chat.id not in self.verify_groups: - return - for user in message.new_chat_members: - if user.id == context.bot.id: - return - logger.debug("用户 %s[%s] 加入群 %s[%s]", user.full_name, user.id, chat.title, chat.id) - await self.set_new_chat_members_message(user, message) - try: - await message.delete() - except BadRequest as exc: - logger.warning("无法删除 Chat Members Message [%s]", exc.message) - - @handler.chat_member(chat_member_types=ChatMemberHandler.CHAT_MEMBER, block=False) - async def track_users(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> None: - chat = update.effective_chat - if chat.id not in self.verify_groups: - return - new_chat_member = update.chat_member.new_chat_member - from_user = update.chat_member.from_user - user = new_chat_member.user - result = extract_status_change(update.chat_member) - if result is None: - return - was_member, is_member = result - if was_member and not is_member: - logger.info("用户 %s[%s] 退出群聊 %s[%s]", user.full_name, user.id, chat.title, chat.id) - return - if not was_member and is_member: - logger.info("用户 %s[%s] 尝试加入群 %s[%s]", user.full_name, user.id, chat.title, chat.id) - if user.is_bot: - return - chat_administrators = await ChatAdministrators.get_chat_administrators(self.cache, context, chat_id=chat.id) - if ChatAdministrators.is_admin(chat_administrators, from_user.id): - await chat.send_message(f"{config.notice.bot_name}检测到管理员邀请,自动放行了!") - return - question_id_list = await self.quiz_service.get_question_id_list() - if len(question_id_list) == 0: - await chat.send_message("旅行者!!!派蒙的问题清单你还没给我!!快去私聊我给我问题!") - return - try: - await chat.restrict_member(user_id=user.id, permissions=ChatPermissions(can_send_messages=False)) - except BadRequest as exc: - if "Not enough rights" in exc.message: - logger.warning("%s[%s] 权限不够", chat.title, chat.id) - await chat.send_message( - f"{config.notice.bot_name}无法修改 {user.mention_html()} 的权限!请检查是否给{config.notice.bot_name}授权管理了", - parse_mode=ParseMode.HTML, - ) - return - raise exc - question_id = random.choice(question_id_list) # nosec - question = await self.quiz_service.get_question(question_id) - buttons = [ - [ - InlineKeyboardButton( - answer.text, - callback_data=f"auth_challenge|{user.id}|{question.question_id}|{answer.answer_id}", - ) - ] - for answer in question.answers - ] - random.shuffle(buttons) - buttons.append( - [ - InlineKeyboardButton( - "放行", - callback_data=f"auth_admin|pass|{user.id}", - ), - InlineKeyboardButton( - "驱离", - callback_data=f"auth_admin|kick|{user.id}", - ), - ] - ) - reply_message = ( - f"*欢迎 {user.mention_markdown_v2()} 来到「提瓦特」世界!* \n" - f"问题: {escape_markdown(question.text, version=2)} \n" - f"请在*{self.time_out}*秒内回答问题" - ) - logger.debug( - "发送入群验证问题 %s[%s] \n给%s[%s] 在 %s[%s]", - question.text, - question.question_id, - user.full_name, - user.id, - chat.title, - chat.id, - ) - try: - question_message = await chat.send_message( - reply_message, - reply_markup=InlineKeyboardMarkup(buttons), - parse_mode=ParseMode.MARKDOWN_V2, - ) - except BadRequest as exc: - await chat.send_message( - f"{config.notice.bot_name}分心了一下,不小心忘记你了,你只能先退出群再重新进来吧。" - ) - raise exc - context.job_queue.run_once( - callback=self.kick_member_job, - when=self.time_out, - name=f"{chat.id}|{user.id}|auth_kick", - chat_id=chat.id, - user_id=user.id, - job_kwargs={"replace_existing": True, "id": f"{chat.id}|{user.id}|auth_kick"}, - ) - context.job_queue.run_once( - callback=self.clean_message_job, - when=self.time_out, - data=question_message.message_id, - name=f"{chat.id}|{user.id}|auth_clean_question_message", - chat_id=chat.id, - user_id=user.id, - job_kwargs={"replace_existing": True, "id": f"{chat.id}|{user.id}|auth_clean_question_message"}, - ) - if PYROGRAM_AVAILABLE and self.mtp: - new_chat_members_message = await self.get_new_chat_members_message(user, context) - try: - if new_chat_members_message: - if question_message.id - new_chat_members_message.id - 1: - message_ids = list(range(new_chat_members_message.id + 1, question_message.id)) - else: - return - else: - message_ids = [question_message.id - 3, question_message.id] - messages_list = await self.mtp.get_messages(chat.id, message_ids=message_ids) - for find_message in messages_list: - if find_message.empty: - continue - if find_message.from_user and find_message.from_user.id == user.id: - await self.mtp.delete_messages(chat_id=chat.id, message_ids=find_message.id) - text: Optional[str] = None - if find_message.text and "@" in find_message.text: - text = f"{user.full_name} 由于加入群组后,在验证缝隙间发送了带有 @(Mention) 的消息,已被踢出群组,并加入了封禁列表。" - elif find_message.caption and "@" in find_message.caption: - text = f"{user.full_name} 由于加入群组后,在验证缝隙间发送了带有 @(Mention) 的消息,已被踢出群组,并加入了封禁列表。" - elif find_message.forward_from_chat: - text = f"{user.full_name} 由于加入群组后,在验证缝隙间发送了带有 Forward 的消息,已被踢出群组,并加入了封禁列表。" - if text is not None: - await context.bot.ban_chat_member(chat.id, user.id) - button = [ - [InlineKeyboardButton("解除封禁", callback_data=f"auth_admin|pass|{user.id}")] - ] - await question_message.edit_text(text, reply_markup=InlineKeyboardMarkup(button)) - if schedule := context.job_queue.scheduler.get_job(f"{chat.id}|{user.id}|auth_kick"): - schedule.remove() - logger.info( - "用户 %s[%s] 在群 %s[%s] 验证缝隙间发送消息 现已删除", - user.full_name, - user.id, - chat.title, - chat.id, - ) - except BadRequest as exc: - logger.error("后验证处理中发生错误 %s", exc.message) - logger.exception(exc) - except MTPFloodWait: - logger.warning("调用 mtp 触发洪水限制") - except MTPBadRequest as exc: - logger.error("调用 mtp 请求错误") - logger.exception(exc) diff --git a/plugins/jobs/refresh_history.py b/plugins/jobs/refresh_history.py deleted file mode 100644 index c51f011..0000000 --- a/plugins/jobs/refresh_history.py +++ /dev/null @@ -1,155 +0,0 @@ -import datetime -from asyncio import sleep -from typing import TYPE_CHECKING, List - -from simnet.errors import ( - TimedOut as SimnetTimedOut, - BadRequest as SimnetBadRequest, - InvalidCookies, -) -from telegram.constants import ParseMode -from telegram.error import BadRequest, Forbidden - -from core.plugin import Plugin, job -from core.services.history_data.services import HistoryDataAbyssServices, HistoryDataLedgerServices -from gram_core.basemodel import RegionEnum -from gram_core.plugin import handler -from gram_core.services.cookies import CookiesService -from gram_core.services.cookies.models import CookiesStatusEnum -from plugins.genshin.abyss import AbyssPlugin -from plugins.genshin.ledger import LedgerPlugin -from plugins.tools.genshin import GenshinHelper, PlayerNotFoundError, CookiesNotFoundError -from utils.log import logger - -if TYPE_CHECKING: - from telegram import Update - from telegram.ext import ContextTypes - - from simnet import GenshinClient - -REGION = [RegionEnum.HYPERION, RegionEnum.HOYOLAB] -NOTICE_TEXT = """#### %s更新 #### -时间:%s (UTC+8) -UID: %s -结果: 新的%s已保存,可通过命令回顾""" - - -class RefreshHistoryJob(Plugin): - """历史记录定时刷新""" - - def __init__( - self, - cookies: CookiesService, - genshin_helper: GenshinHelper, - history_abyss: HistoryDataAbyssServices, - history_ledger: HistoryDataLedgerServices, - ): - self.cookies = cookies - self.genshin_helper = genshin_helper - self.history_data_abyss = history_abyss - self.history_data_ledger = history_ledger - - @staticmethod - async def send_notice(context: "ContextTypes.DEFAULT_TYPE", user_id: int, notice_text: str): - try: - await context.bot.send_message(user_id, notice_text, parse_mode=ParseMode.HTML) - except (BadRequest, Forbidden) as exc: - logger.error("执行自动刷新历史记录时发生错误 user_id[%s] Message[%s]", user_id, exc.message) - except Exception as exc: - logger.error("执行自动刷新历史记录时发生错误 user_id[%s]", user_id, exc_info=exc) - - async def save_abyss_data(self, client: "GenshinClient") -> bool: - uid = client.player_id - abyss_data = await client.get_genshin_spiral_abyss(uid, previous=False, lang="zh-cn") - avatars = await client.get_genshin_characters(uid, lang="zh-cn") - avatar_data = {i.id: i.constellation for i in avatars} - if abyss_data.unlocked and abyss_data.ranks and abyss_data.ranks.most_kills: - return await AbyssPlugin.save_abyss_data(self.history_data_abyss, uid, abyss_data, avatar_data) - return False - - async def send_abyss_notice(self, context: "ContextTypes.DEFAULT_TYPE", user_id: int, uid: int): - now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") - notice_text = NOTICE_TEXT % ("深渊历史记录", now, uid, "挑战记录") - await self.send_notice(context, user_id, notice_text) - - async def _save_ledger_data(self, client: "GenshinClient", month: int) -> bool: - diary_info = await client.get_genshin_diary(client.player_id, month=month) - return await LedgerPlugin.save_ledger_data(self.history_data_ledger, client.player_id, diary_info) - - @staticmethod - def get_ledger_months() -> List[int]: - now = datetime.datetime.now() - now_time = (now - datetime.timedelta(days=1)) if now.day == 1 and now.hour <= 4 else now - months = [] - last_month = now_time.replace(day=1) - datetime.timedelta(days=1) - months.append(last_month.month) - - last_month = last_month.replace(day=1) - datetime.timedelta(days=1) - months.append(last_month.month) - return months - - async def save_ledger_data(self, client: "GenshinClient") -> bool: - months = self.get_ledger_months() - ok = False - for month in months: - if await self._save_ledger_data(client, month): - ok = True - return ok - - async def send_ledger_notice(self, context: "ContextTypes.DEFAULT_TYPE", user_id: int, uid: int): - now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") - notice_text = NOTICE_TEXT % ("旅行札记历史记录", now, uid, "旅行札记历史记录") - await self.send_notice(context, user_id, notice_text) - - @handler.command(command="remove_same_history", block=False, admin=True) - async def remove_same_history(self, update: "Update", _: "ContextTypes.DEFAULT_TYPE"): - user = update.effective_user - logger.info("用户 %s[%s] remove_same_history 命令请求", user.full_name, user.id) - message = update.effective_message - reply = await message.reply_text("正在执行移除相同数据历史记录任务,请稍后...") - text = "移除相同数据历史记录任务完成\n" - num1 = await self.history_data_abyss.remove_same_data() - text += f"深渊数据移除数量:{num1}\n" - num2 = await self.history_data_ledger.remove_same_data() - text += f"旅行札记数据移除数量:{num2}\n" - await reply.edit_text(text) - - @handler.command(command="refresh_all_history", block=False, admin=True) - async def refresh_all_history(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE"): - user = update.effective_user - logger.info("用户 %s[%s] refresh_all_history 命令请求", user.full_name, user.id) - message = update.effective_message - reply = await message.reply_text("正在执行刷新历史记录任务,请稍后...") - await self.daily_refresh_history(context) - await reply.edit_text("全部账号刷新历史记录任务完成") - - @job.run_daily(time=datetime.time(hour=6, minute=1, second=0), name="RefreshHistoryJob") - async def daily_refresh_history(self, context: "ContextTypes.DEFAULT_TYPE"): - logger.info("正在执行每日刷新历史记录任务") - for database_region in REGION: - for cookie_model in await self.cookies.get_all( - region=database_region, status=CookiesStatusEnum.STATUS_SUCCESS - ): - user_id = cookie_model.user_id - try: - async with self.genshin_helper.genshin(user_id) as client: - if await self.save_abyss_data(client): - await self.send_abyss_notice(context, user_id, client.player_id) - if await self.save_ledger_data(client): - await self.send_ledger_notice(context, user_id, client.player_id) - except (InvalidCookies, PlayerNotFoundError, CookiesNotFoundError): - continue - except SimnetBadRequest as exc: - logger.warning( - "用户 user_id[%s] 请求历史记录失败 [%s]%s", user_id, exc.ret_code, exc.original or exc.message - ) - continue - except SimnetTimedOut: - logger.info("用户 user_id[%s] 请求历史记录超时", user_id) - continue - except Exception as exc: - logger.error("执行自动刷新历史记录时发生错误 user_id[%s]", user_id, exc_info=exc) - continue - await sleep(1) - - logger.success("执行每日刷新历史记录任务完成") diff --git a/plugins/jobs/sign.py b/plugins/jobs/sign.py index e026d05..e4970ea 100644 --- a/plugins/jobs/sign.py +++ b/plugins/jobs/sign.py @@ -2,7 +2,7 @@ import datetime from typing import TYPE_CHECKING from core.plugin import Plugin, job -from plugins.genshin.sign import SignSystem +from plugins.zzz.sign import SignSystem from plugins.tools.sign import SignJobType from utils.log import logger diff --git a/plugins/system/errorhandler.py b/plugins/system/errorhandler.py index 26dc974..746295a 100644 --- a/plugins/system/errorhandler.py +++ b/plugins/system/errorhandler.py @@ -25,6 +25,7 @@ from telegram.helpers import create_deep_linked_url from core.config import config from core.plugin import Plugin, error_handler +from gram_core.services.cookies.error import CookieServiceError as PublicCookieServiceError from gram_core.services.players.error import PlayerNotFoundError from modules.apihelper.error import APIHelperException, APIHelperTimedOut, ResponseException, ReturnCodeError from modules.errorpush import ( @@ -154,7 +155,7 @@ class ErrorHandler(Plugin): elif exc.retcode == 10103: notice = ( self.ERROR_MSG_PREFIX - + "Cookie 有效,但没有绑定到游戏帐户,请尝试登录通行证,在账号管理里面选择账号游戏信息,将原神设置为默认角色。" + + "Cookie 有效,但没有绑定到游戏帐户,请尝试登录通行证,在账号管理里面选择账号游戏信息,将绝区零设置为默认角色。" ) else: logger.error("未知Cookie错误", exc_info=exc) @@ -271,6 +272,13 @@ class ErrorHandler(Plugin): self.create_notice_task(update, context, config.notice.user_not_found) raise ApplicationHandlerStop + @error_handler() + async def process_public_cookies(self, update: object, context: CallbackContext): + if not isinstance(context.error, PublicCookieServiceError) or not isinstance(update, Update): + return + self.create_notice_task(update, context, "公共Cookies池已经耗尽,请稍后重试或者绑定账号") + raise ApplicationHandlerStop + @error_handler(block=False) async def process_z_error(self, update: object, context: CallbackContext) -> None: # 必须 `process_` 加上 `z` 保证该函数最后一个注册 diff --git a/plugins/tools/birthday_card.py b/plugins/tools/birthday_card.py deleted file mode 100644 index ceab7f0..0000000 --- a/plugins/tools/birthday_card.py +++ /dev/null @@ -1,186 +0,0 @@ -from datetime import datetime -from typing import TYPE_CHECKING, List, Optional - -from simnet import Region -from simnet.client.routes import Route -from simnet.errors import BadRequest as SimnetBadRequest, RegionNotSupported, InvalidCookies, TimedOut as SimnetTimedOut -from simnet.utils.player import recognize_genshin_game_biz, recognize_genshin_server -from sqlalchemy.orm.exc import StaleDataError -from telegram.constants import ParseMode -from telegram.error import BadRequest, Forbidden - -from core.plugin import Plugin -from core.services.task.models import TaskStatusEnum -from core.services.task.services import TaskCardServices -from metadata.shortname import roleToId -from modules.apihelper.client.components.calendar import Calendar -from plugins.tools.genshin import GenshinHelper, PlayerNotFoundError, CookiesNotFoundError -from utils.log import logger - -if TYPE_CHECKING: - from simnet import GenshinClient - from telegram.ext import ContextTypes - -BIRTHDAY_URL = Route( - "https://hk4e-api.mihoyo.com/event/birthdaystar/account/post_my_draw", -) - - -def rm_starting_str(string, starting): - """Remove the starting character from a string.""" - while string[0] == str(starting): - string = string[1:] - return string - - -class BirthdayCardNoBirthdayError(Exception): - pass - - -class BirthdayCardAlreadyClaimedError(Exception): - pass - - -class BirthdayCardSystem(Plugin): - def __init__( - self, - card_service: TaskCardServices, - genshin_helper: GenshinHelper, - ): - self.birthday_list = {} - self.card_service = card_service - self.genshin_helper = genshin_helper - - async def initialize(self): - self.birthday_list = await Calendar.async_gen_birthday_list() - self.birthday_list.get("6_1", []).append("派蒙") - - @property - def key(self): - return ( - rm_starting_str(datetime.now().strftime("%m"), "0") - + "_" - + rm_starting_str(datetime.now().strftime("%d"), "0") - ) - - def get_today_birthday(self) -> List[str]: - key = self.key - return (self.birthday_list.get(key, [])).copy() - - @staticmethod - def role_to_id(name: str) -> Optional[int]: - if name == "派蒙": - return -1 - return roleToId(name) - - @staticmethod - async def get_card(client: "GenshinClient", role_id: int) -> None: - """领取画片""" - url = BIRTHDAY_URL.get_url() - params = { - "game_biz": recognize_genshin_game_biz(client.player_id), - "lang": "zh-cn", - "badge_uid": client.player_id, - "badge_region": recognize_genshin_server(client.player_id), - "activity_id": "20220301153521", - } - json = { - "role_id": role_id, - } - try: - await client.request_lab(url, method="POST", params=params, data=json) - except SimnetBadRequest as e: - if e.retcode == -512008: - raise BirthdayCardNoBirthdayError from e # 未过生日 - if e.retcode == -512009: - raise BirthdayCardAlreadyClaimedError from e # 已领取过 - raise e - - async def start_get_card( - self, - client: "GenshinClient", - ) -> str: - if client.region == Region.OVERSEAS: - raise RegionNotSupported - today_list = self.get_today_birthday() - if not today_list: - raise BirthdayCardNoBirthdayError - game_biz = recognize_genshin_game_biz(client.player_id) - region = recognize_genshin_server(client.player_id) - await client.get_hk4e_token_by_cookie_token(game_biz, region) - for name in today_list.copy(): - if role_id := self.role_to_id(name): - try: - await self.get_card(client, role_id) - except BirthdayCardAlreadyClaimedError: - today_list.remove(name) - if today_list: - text = f"成功领取了 {'、'.join(today_list)} 的生日画片~" - else: - raise BirthdayCardAlreadyClaimedError - return text - - async def do_get_card_job(self, context: "ContextTypes.DEFAULT_TYPE"): - if not self.get_today_birthday(): - logger.info("今天没有角色过生日,跳过自动领取生日画片") - return - include_status: List[TaskStatusEnum] = [ - TaskStatusEnum.STATUS_SUCCESS, - TaskStatusEnum.TIMEOUT_ERROR, - ] - task_list = await self.card_service.get_all() - for task_db in task_list: - if task_db.status not in include_status: - continue - user_id = task_db.user_id - try: - async with self.genshin_helper.genshin(user_id) as client: - text = await self.start_get_card(client) - except InvalidCookies: - text = "自动领取生日画片执行失败,Cookie无效" - task_db.status = TaskStatusEnum.INVALID_COOKIES - except BirthdayCardAlreadyClaimedError: - text = "今天旅行者已经领取过了~" - task_db.status = TaskStatusEnum.ALREADY_CLAIMED - except SimnetBadRequest as exc: - text = f"自动领取生日画片执行失败,API返回信息为 {str(exc)}" - task_db.status = TaskStatusEnum.GENSHIN_EXCEPTION - except SimnetTimedOut: - text = "领取失败了呜呜呜 ~ 服务器连接超时 服务器熟啦 ~ " - task_db.status = TaskStatusEnum.TIMEOUT_ERROR - except PlayerNotFoundError: - logger.info("用户 user_id[%s] 玩家不存在 关闭并移除自动领取生日画片", user_id) - await self.card_service.remove(task_db) - continue - except CookiesNotFoundError: - logger.info("用户 user_id[%s] cookie 不存在 关闭并移除自动领取生日画片", user_id) - await self.card_service.remove(task_db) - continue - except RegionNotSupported: - logger.info("用户 user_id[%s] 不支持的服务器 关闭并移除自动领取生日画片", user_id) - await self.card_service.remove(task_db) - continue - except Exception as exc: - logger.error("执行自动领取生日画片时发生错误 user_id[%s]", user_id, exc_info=exc) - text = "自动领取生日画片失败了呜呜呜 ~ 执行自动领取生日画片时发生错误" - else: - task_db.status = TaskStatusEnum.STATUS_SUCCESS - if task_db.chat_id < 0: - text = f'NOTICE {task_db.user_id}\n\n{text}' - try: - await context.bot.send_message(task_db.chat_id, text, parse_mode=ParseMode.HTML) - except BadRequest as exc: - logger.error("执行自动领取生日画片时发生错误 user_id[%s] Message[%s]", user_id, exc.message) - task_db.status = TaskStatusEnum.BAD_REQUEST - except Forbidden as exc: - logger.error("执行自动领取生日画片时发生错误 user_id[%s] message[%s]", user_id, exc.message) - task_db.status = TaskStatusEnum.FORBIDDEN - except Exception as exc: - logger.error("执行自动领取生日画片时发生错误 user_id[%s]", user_id, exc_info=exc) - continue - else: - task_db.status = TaskStatusEnum.STATUS_SUCCESS - try: - await self.card_service.update(task_db) - except StaleDataError: - logger.warning("用户 user_id[%s] 自动领取生日画片数据过期,跳过更新数据", user_id) diff --git a/plugins/tools/challenge.py b/plugins/tools/challenge.py index edcfef3..79f0fd6 100644 --- a/plugins/tools/challenge.py +++ b/plugins/tools/challenge.py @@ -59,7 +59,7 @@ class ChallengeSystem(Plugin): raise ChallengeSystemException("无需验证") if need_verify: try: - await client.get_genshin_notes() + await client.get_zzz_notes() except NeedChallenge: pass else: diff --git a/plugins/tools/daily_note.py b/plugins/tools/daily_note.py index 255b2fe..f4d06fa 100644 --- a/plugins/tools/daily_note.py +++ b/plugins/tools/daily_note.py @@ -3,22 +3,22 @@ from datetime import datetime from typing import TYPE_CHECKING, List, Optional, Union from pydantic import BaseModel, validator -from simnet import Region from simnet.errors import BadRequest as SimnetBadRequest, InvalidCookies, TimedOut as SimnetTimedOut +from simnet.models.zzz.chronicle.notes import ZZZNoteVhsSaleState from sqlalchemy.orm.exc import StaleDataError from telegram.constants import ParseMode from telegram.error import BadRequest, Forbidden from core.plugin import Plugin from core.services.task.models import Task as TaskUser, TaskStatusEnum -from core.services.task.services import TaskResinServices, TaskRealmServices, TaskExpeditionServices, TaskDailyServices +from core.services.task.services import TaskResinServices, TaskExpeditionServices, TaskDailyServices from gram_core.plugin.methods.migrate_data import IMigrateData, MigrateDataException from plugins.tools.genshin import GenshinHelper, PlayerNotFoundError, CookiesNotFoundError from utils.log import logger if TYPE_CHECKING: - from simnet import GenshinClient - from simnet.models.genshin.chronicle.notes import Notes, NotesWidget + from simnet import ZZZClient + from simnet.models.zzz.chronicle.notes import ZZZNote from telegram.ext import ContextTypes from gram_core.services.task.services import TaskServices @@ -33,17 +33,7 @@ class ResinData(TaskDataBase): @validator("notice_num") def notice_num_validator(cls, v): if v < 60 or v > 200: - raise ValueError("树脂提醒数值必须在 60 ~ 200 之间") - return v - - -class RealmData(TaskDataBase): - notice_num: Optional[int] = 2000 - - @validator("notice_num") - def notice_num_validator(cls, v): - if v < 100 or v > 2400: - raise ValueError("洞天宝钱提醒数值必须在 100 ~ 2400 之间") + raise ValueError("电量提醒数值必须在 60 ~ 200 之间") return v @@ -63,7 +53,6 @@ class DailyData(TaskDataBase): class WebAppData(BaseModel): resin: Optional[ResinData] - realm: Optional[RealmData] expedition: Optional[ExpeditionData] daily: Optional[DailyData] @@ -73,17 +62,14 @@ class DailyNoteTaskUser: self, user_id: int, resin_db: Optional[TaskUser] = None, - realm_db: Optional[TaskUser] = None, expedition_db: Optional[TaskUser] = None, daily_db: Optional[TaskUser] = None, ): self.user_id = user_id self.resin_db = resin_db - self.realm_db = realm_db self.expedition_db = expedition_db self.daily_db = daily_db self.resin = ResinData(**self.resin_db.data) if self.resin_db else None - self.realm = RealmData(**self.realm_db.data) if self.realm_db else None self.expedition = ExpeditionData(**self.expedition_db.data) if self.expedition_db else None self.daily = DailyData(**self.daily_db.data) if self.daily_db else None @@ -92,7 +78,6 @@ class DailyNoteTaskUser: return max( [ self.resin_db.status if self.resin_db else TaskStatusEnum.STATUS_SUCCESS, - self.realm_db.status if self.realm_db else TaskStatusEnum.STATUS_SUCCESS, self.expedition_db.status if self.expedition_db else TaskStatusEnum.STATUS_SUCCESS, self.daily_db.status if self.daily_db else TaskStatusEnum.STATUS_SUCCESS, ] @@ -102,8 +87,6 @@ class DailyNoteTaskUser: def status(self, value: TaskStatusEnum): if self.resin_db: self.resin_db.status = value - if self.realm_db: - self.realm_db.status = value if self.expedition_db: self.expedition_db.status = value if self.daily: @@ -125,7 +108,6 @@ class DailyNoteTaskUser: ( WebAppData( resin=self.set_model_noticed(self.resin) if self.resin else None, - realm=self.set_model_noticed(self.realm) if self.realm else None, expedition=self.set_model_noticed(self.expedition) if self.expedition else None, daily=self.set_model_noticed(self.daily) if self.daily else None, ).json() @@ -135,8 +117,6 @@ class DailyNoteTaskUser: def save(self): if self.resin_db: self.resin_db.data = self.resin.dict() - if self.realm_db: - self.realm_db.data = self.realm.dict() if self.expedition_db: self.expedition_db.data = self.expedition.dict() if self.daily_db: @@ -148,38 +128,34 @@ class DailyNoteSystem(Plugin): self, genshin_helper: GenshinHelper, resin_service: TaskResinServices, - realm_service: TaskRealmServices, expedition_service: TaskExpeditionServices, daily_service: TaskDailyServices, ): self.genshin_helper = genshin_helper self.resin_service = resin_service - self.realm_service = realm_service self.expedition_service = expedition_service self.daily_service = daily_service async def get_single_task_user(self, user_id: int) -> DailyNoteTaskUser: resin_db = await self.resin_service.get_by_user_id(user_id) - realm_db = await self.realm_service.get_by_user_id(user_id) expedition_db = await self.expedition_service.get_by_user_id(user_id) daily_db = await self.daily_service.get_by_user_id(user_id) return DailyNoteTaskUser( user_id=user_id, resin_db=resin_db, - realm_db=realm_db, expedition_db=expedition_db, daily_db=daily_db, ) @staticmethod - def get_resin_notice(user: DailyNoteTaskUser, notes: Union["Notes", "NotesWidget"]) -> str: + def get_resin_notice(user: DailyNoteTaskUser, notes: Union["ZZZNote"]) -> str: notice = None - if user.resin_db and notes.max_resin > 0: - if notes.current_resin >= user.resin.notice_num: + if user.resin_db and notes.max_stamina > 0: + if notes.current_stamina >= user.resin.notice_num: if not user.resin.noticed: notice = ( - f"### 树脂提示 ####\n\n当前树脂为 {notes.current_resin} / {notes.max_resin} ,记得使用哦~\n" - f"预计全部恢复完成:{notes.resin_recovery_time.strftime('%Y-%m-%d %H:%M')}" + f"### 电量提示 ####\n\n当前电量为 {notes.current_stamina} / {notes.max_stamina} ,记得使用哦~\n" + f"预计全部恢复完成:{notes.stamina_recover_time.strftime('%Y-%m-%d %H:%M')}" ) user.resin.noticed = True else: @@ -187,46 +163,28 @@ class DailyNoteSystem(Plugin): return notice @staticmethod - def get_realm_notice(user: DailyNoteTaskUser, notes: Union["Notes", "NotesWidget"]) -> str: + def get_expedition_notice(user: DailyNoteTaskUser, notes: Union["ZZZNote"]) -> str: notice = None - if user.realm_db and notes.max_realm_currency > 0: - if notes.current_realm_currency >= user.realm.notice_num: - if not user.realm.noticed: - notice = ( - f"### 洞天宝钱提示 ####\n\n" - f"当前存储为 {notes.current_realm_currency} / {notes.max_realm_currency} ,记得领取哦~" - ) - user.realm.noticed = True - else: - user.realm.noticed = False - return notice - - @staticmethod - def get_expedition_notice(user: DailyNoteTaskUser, notes: Union["Notes", "NotesWidget"]) -> str: - notice = None - if user.expedition_db and len(notes.expeditions) > 0: - all_finished = all(i.status == "Finished" for i in notes.expeditions) + if user.expedition_db and notes.vhs_sale.sale_state: + all_finished = notes.vhs_sale.sale_state == ZZZNoteVhsSaleState.DONE if all_finished: if not user.expedition.noticed: - notice = "### 探索派遣提示 ####\n\n所有探索派遣已完成,记得重新派遣哦~" + notice = "### 录像店经营提示 ####\n\n录像店经营已完成,记得重新上架哦~" user.expedition.noticed = True else: user.expedition.noticed = False return notice @staticmethod - def get_daily_notice(user: DailyNoteTaskUser, notes: Union["Notes", "NotesWidget"]) -> str: + def get_daily_notice(user: DailyNoteTaskUser, notes: Union["ZZZNote"]) -> str: notice = None now_hour = datetime.now().hour if user.daily_db: if now_hour == user.daily.notice_hour: - if (notes.completed_commissions != notes.max_commissions or (not notes.claimed_commission_reward)) and ( - not user.daily.noticed - ): - notice_ = "已领取奖励" if notes.claimed_commission_reward else "未领取奖励" + if (notes.current_train_score != notes.max_train_score) and (not user.daily.noticed): notice = ( f"### 每日任务提示 ####\n\n" - f"当前进度为 {notes.completed_commissions} / {notes.max_commissions} ({notice_}) ,记得完成哦~" + f"当前进度为 {notes.current_train_score} / {notes.max_train_score} ,记得完成哦~" ) user.daily.noticed = True else: @@ -235,18 +193,14 @@ class DailyNoteSystem(Plugin): @staticmethod async def start_get_notes( - client: "GenshinClient", + client: "ZZZClient", user: DailyNoteTaskUser = None, ) -> List[str]: - if client.region == Region.OVERSEAS: - notes = await client.get_genshin_notes() - else: - notes = await client.get_genshin_notes_by_stoken() + notes = await client.get_zzz_notes() if not user: return [] notices = [ DailyNoteSystem.get_resin_notice(user, notes), - DailyNoteSystem.get_realm_notice(user, notes), DailyNoteSystem.get_expedition_notice(user, notes), DailyNoteSystem.get_daily_notice(user, notes), ] @@ -255,14 +209,11 @@ class DailyNoteSystem(Plugin): async def get_all_task_users(self) -> List[DailyNoteTaskUser]: resin_list = await self.resin_service.get_all() - realm_list = await self.realm_service.get_all() expedition_list = await self.expedition_service.get_all() daily_list = await self.daily_service.get_all() user_list = set() for i in resin_list: user_list.add(i.user_id) - for i in realm_list: - user_list.add(i.user_id) for i in expedition_list: user_list.add(i.user_id) for i in daily_list: @@ -271,7 +222,6 @@ class DailyNoteSystem(Plugin): DailyNoteTaskUser( user_id=i, resin_db=next((x for x in resin_list if x.user_id == i), None), - realm_db=next((x for x in realm_list if x.user_id == i), None), expedition_db=next((x for x in expedition_list if x.user_id == i), None), daily_db=next((x for x in daily_list if x.user_id == i), None), ) @@ -281,8 +231,6 @@ class DailyNoteSystem(Plugin): async def remove_task_user(self, user: DailyNoteTaskUser): if user.resin_db: await self.resin_service.remove(user.resin_db) - if user.realm_db: - await self.realm_service.remove(user.realm_db) if user.expedition_db: await self.expedition_service.remove(user.expedition_db) if user.daily_db: @@ -293,17 +241,12 @@ class DailyNoteSystem(Plugin): try: await self.resin_service.update(user.resin_db) except StaleDataError: - logger.warning("用户 user_id[%s] 自动便签提醒 - 树脂数据过期,跳过更新数据", user.user_id) - if user.realm_db: - try: - await self.realm_service.update(user.realm_db) - except StaleDataError: - logger.warning("用户 user_id[%s] 自动便签提醒 - 洞天宝钱数据过期,跳过更新数据", user.user_id) + logger.warning("用户 user_id[%s] 自动便签提醒 - 电量数据过期,跳过更新数据", user.user_id) if user.expedition_db: try: await self.expedition_service.update(user.expedition_db) except StaleDataError: - logger.warning("用户 user_id[%s] 自动便签提醒 - 探索派遣数据过期,跳过更新数据", user.user_id) + logger.warning("用户 user_id[%s] 自动便签提醒 - 录像店经营数据过期,跳过更新数据", user.user_id) if user.daily_db: try: await self.daily_service.update(user.daily_db) @@ -315,8 +258,6 @@ class DailyNoteSystem(Plugin): need_verify = False if web_config.resin and web_config.resin.noticed: need_verify = True - if web_config.realm and web_config.realm.noticed: - need_verify = True if web_config.expedition and web_config.expedition.noticed: need_verify = True if web_config.daily and web_config.daily.noticed: @@ -344,26 +285,6 @@ class DailyNoteSystem(Plugin): user.resin_db = None user.resin = None - async def import_web_config_realm(self, user: DailyNoteTaskUser, web_config: WebAppData): - user_id = user.user_id - if web_config.realm.noticed: - if not user.realm_db: - realm = self.realm_service.create( - user_id, - user_id, - status=TaskStatusEnum.STATUS_SUCCESS, - data=RealmData(notice_num=web_config.realm.notice_num).dict(), - ) - await self.realm_service.add(realm) - else: - user.realm.notice_num = web_config.realm.notice_num - user.realm.noticed = False - else: - if user.realm_db: - await self.realm_service.remove(user.realm_db) - user.realm_db = None - user.realm = None - async def import_web_config_expedition(self, user: DailyNoteTaskUser, web_config: WebAppData): user_id = user.user_id if web_config.expedition.noticed: @@ -408,8 +329,6 @@ class DailyNoteSystem(Plugin): user = await self.get_single_task_user(user_id) if web_config.resin: await self.import_web_config_resin(user, web_config) - if web_config.realm: - await self.import_web_config_realm(user, web_config) if web_config.expedition: await self.import_web_config_expedition(user, web_config) if web_config.daily: @@ -453,9 +372,7 @@ class DailyNoteSystem(Plugin): text = "获取便签失败了呜呜呜 ~ 执行自动便签提醒时发生错误" else: task_db.status = TaskStatusEnum.STATUS_SUCCESS - for idx, task_user_db in enumerate( - [task_db.resin_db, task_db.realm_db, task_db.expedition_db, task_db.daily_db] - ): + for idx, task_user_db in enumerate([task_db.resin_db, task_db.expedition_db, task_db.daily_db]): if task_user_db is None: continue notice_text = text[idx] if isinstance(text, list) else text diff --git a/plugins/tools/genshin.py b/plugins/tools/genshin.py index 713423d..feb2a6c 100644 --- a/plugins/tools/genshin.py +++ b/plugins/tools/genshin.py @@ -6,7 +6,7 @@ from typing import Optional from typing import TYPE_CHECKING, Union from pydantic import ValidationError -from simnet import GenshinClient, Region +from simnet import ZZZClient, Region from simnet.errors import BadRequest as SimnetBadRequest, InvalidCookies, NetworkError, CookieException, NeedChallenge from simnet.models.genshin.calculator import CalculatorCharacterDetails from simnet.models.genshin.chronicle.characters import Character @@ -158,7 +158,7 @@ class CharacterDetails(Plugin): return None async def get_character_details( - self, client: "GenshinClient", character: "Union[int,Character]" + self, client: "ZZZClient", character: "Union[int,Character]" ) -> Optional["CalculatorCharacterDetails"]: """缓存 character_details 并定时对其进行数据存储 当遇到 Too Many Requests 可以获取以前的数据""" uid = client.player_id @@ -220,7 +220,7 @@ class GenshinHelper(Plugin): @asynccontextmanager async def genshin( # skipcq: PY-R1000 # self, user_id: int, region: Optional[RegionEnum] = None, player_id: int = None, offset: int = 0 - ) -> GenshinClient: + ) -> ZZZClient: player = await self.players_service.get_player(user_id, region, player_id, offset) if player is None: raise PlayerNotFoundError(user_id) @@ -246,7 +246,7 @@ class GenshinHelper(Plugin): device_id = devices.device_id device_fp = devices.device_fp - async with GenshinClient( + async with ZZZClient( cookies, region=region, account_id=player.account_id, @@ -308,7 +308,7 @@ class GenshinHelper(Plugin): async def get_genshin_client( self, user_id: int, region: Optional[RegionEnum] = None, player_id: int = None, offset: int = 0 - ) -> GenshinClient: + ) -> ZZZClient: player = await self.players_service.get_player(user_id, region, player_id, offset) if player is None: raise PlayerNotFoundError(user_id) @@ -334,7 +334,7 @@ class GenshinHelper(Plugin): device_id = devices.device_id device_fp = devices.device_fp - return GenshinClient( + return ZZZClient( cookies, region=region, account_id=player.account_id, @@ -347,7 +347,7 @@ class GenshinHelper(Plugin): @asynccontextmanager async def public_genshin( self, user_id: int, region: Optional[RegionEnum] = None, uid: Optional[int] = None - ) -> GenshinClient: + ) -> ZZZClient: if not (region or uid): player = await self.players_service.get_player(user_id, region) if player: @@ -370,7 +370,7 @@ class GenshinHelper(Plugin): device_id = devices.device_id device_fp = devices.device_fp - async with GenshinClient( + async with ZZZClient( cookies.data, region=region, player_id=uid, @@ -392,7 +392,7 @@ class GenshinHelper(Plugin): region: Optional[RegionEnum] = None, uid: Optional[int] = None, offset: int = 0, - ) -> GenshinClient: + ) -> ZZZClient: try: async with self.genshin(user_id, region, uid, offset) as client: client.public = False diff --git a/plugins/tools/player_info.py b/plugins/tools/player_info.py index ce20727..4090568 100644 --- a/plugins/tools/player_info.py +++ b/plugins/tools/player_info.py @@ -2,7 +2,6 @@ from typing import Optional from enkanetwork import Assets -from core.dependence.assets import AssetsService from core.plugin import Plugin from core.services.players.services import PlayerInfoService, PlayersService from metadata.genshin import AVATAR_DATA @@ -13,10 +12,8 @@ class PlayerInfoSystem(Plugin): def __init__( self, player_service: PlayersService = None, - assets_service: AssetsService = None, player_info_service: PlayerInfoService = None, ) -> None: - self.assets_service = assets_service self.player_info_service = player_info_service self.player_service = player_service diff --git a/plugins/tools/sign.py b/plugins/tools/sign.py index eed613a..1e54294 100644 --- a/plugins/tools/sign.py +++ b/plugins/tools/sign.py @@ -27,7 +27,7 @@ from plugins.tools.recognize import RecognizeSystem from utils.log import logger if TYPE_CHECKING: - from simnet import GenshinClient + from simnet import ZZZClient from telegram.ext import ContextTypes @@ -106,7 +106,7 @@ class SignSystem(Plugin): async def start_sign( self, - client: "GenshinClient", + client: "ZZZClient", challenge: Optional[str] = None, validate: Optional[str] = None, is_sleep: bool = False, diff --git a/plugins/genshin/__init__.py b/plugins/zzz/__init__.py similarity index 100% rename from plugins/genshin/__init__.py rename to plugins/zzz/__init__.py diff --git a/plugins/genshin/daily_note.py b/plugins/zzz/daily_note.py similarity index 64% rename from plugins/genshin/daily_note.py rename to plugins/zzz/daily_note.py index ff62d9c..58dcbeb 100644 --- a/plugins/genshin/daily_note.py +++ b/plugins/zzz/daily_note.py @@ -16,7 +16,7 @@ from utils.log import logger from utils.uid import mask_number if TYPE_CHECKING: - from simnet import GenshinClient + from simnet import ZZZClient from telegram import Update from telegram.ext import ContextTypes @@ -34,62 +34,30 @@ class DailyNotePlugin(Plugin): self.template_service = template self.helper = helper - async def _get_daily_note(self, client: "GenshinClient") -> RenderResult: - daily_info = await client.get_genshin_notes(client.player_id) + async def _get_daily_note(self, client: "ZZZClient") -> RenderResult: + daily_info = await client.get_zzz_notes(client.player_id) day = datetime.now().strftime("%m-%d %H:%M") + " 星期" + "一二三四五六日"[datetime.now().weekday()] resin_recovery_time = ( - daily_info.resin_recovery_time.strftime("%m-%d %H:%M") - if daily_info.max_resin - daily_info.current_resin + daily_info.stamina_recover_time.strftime("%m-%d %H:%M") + if daily_info.max_stamina - daily_info.current_stamina else None ) - realm_recovery_time = ( - (datetime.now().astimezone() + daily_info.remaining_realm_currency_recovery_time).strftime("%m-%d %H:%M") - if daily_info.max_realm_currency - daily_info.current_realm_currency - else None - ) - remained_time = None - for i in daily_info.expeditions: - if remained_time: - if remained_time < i.remaining_time: - remained_time = i.remaining_time - else: - remained_time = i.remaining_time - if remained_time: - remained_time = (datetime.now().astimezone() + remained_time).strftime("%m-%d %H:%M") - - transformer, transformer_ready, transformer_recovery_time = False, None, None - if daily_info.remaining_transformer_recovery_time is not None: - transformer = True - transformer_ready = daily_info.remaining_transformer_recovery_time.total_seconds() == 0 - transformer_recovery_time = daily_info.transformer_recovery_time.strftime("%m-%d %H:%M") render_data = { "uid": mask_number(client.player_id), "day": day, "resin_recovery_time": resin_recovery_time, - "current_resin": daily_info.current_resin, - "max_resin": daily_info.max_resin, - "realm_recovery_time": realm_recovery_time, - "current_realm_currency": daily_info.current_realm_currency, - "max_realm_currency": daily_info.max_realm_currency, - "claimed_commission_reward": daily_info.claimed_commission_reward, - "completed_commissions": daily_info.completed_commissions, - "max_commissions": daily_info.max_commissions, - "expeditions": bool(daily_info.expeditions), - "remained_time": remained_time, - "current_expeditions": len(daily_info.expeditions), - "max_expeditions": daily_info.max_expeditions, - "remaining_resin_discounts": daily_info.remaining_resin_discounts, - "max_resin_discounts": daily_info.max_resin_discounts, - "transformer": transformer, - "transformer_ready": transformer_ready, - "transformer_recovery_time": transformer_recovery_time, + "current_resin": daily_info.current_stamina, + "max_resin": daily_info.max_stamina, + "exp_status": daily_info.vhs_sale.sale_state.name, + "current_train_score": daily_info.current_train_score, + "max_train_score": daily_info.max_train_score, } render_result = await self.template_service.render( - "genshin/daily_note/daily_note.jinja2", + "zzz/daily_note/daily_note.jinja2", render_data, - {"width": 600, "height": 548}, + {"width": 600, "height": 300}, full_page=False, ttl=8 * 60, ) diff --git a/plugins/genshin/daily_note_tasks.py b/plugins/zzz/daily_note_tasks.py similarity index 91% rename from plugins/genshin/daily_note_tasks.py rename to plugins/zzz/daily_note_tasks.py index 05a6cd0..e50a3a6 100644 --- a/plugins/genshin/daily_note_tasks.py +++ b/plugins/zzz/daily_note_tasks.py @@ -17,7 +17,7 @@ from plugins.tools.genshin import GenshinHelper, CookiesNotFoundError, PlayerNot from utils.log import logger if TYPE_CHECKING: - from simnet import GenshinClient + from simnet import ZZZClient __all__ = ("DailyNoteTasksPlugin",) @@ -54,7 +54,7 @@ class DailyNoteTasksPlugin(Plugin.Conversation): await message.reply_text(text, reply_markup=ReplyKeyboardRemove()) return ConversationHandler.END note_user = await self.note_system.get_single_task_user(user.id) - url = f"{config.pass_challenge_user_web}/tasks1?command=tasks&bot_data={note_user.web_config}" + url = f"{config.pass_challenge_user_web}/tasks4?command=tasks&bot_data={note_user.web_config}" text = ( f'你好 {user.mention_markdown_v2()} {escape_markdown("!请点击下方按钮,开始设置,或者回复退出取消操作")}' ) @@ -72,12 +72,9 @@ class DailyNoteTasksPlugin(Plugin.Conversation): async def check_genshin_user(self, user_id: int, request_note: bool) -> str: try: async with self.helper.genshin(user_id) as client: - client: "GenshinClient" + client: "ZZZClient" if request_note: - if client.region == Region.CHINESE: - await client.get_genshin_notes_by_stoken() - else: - await client.get_genshin_notes() + await client.get_zzz_notes() return "ok" except ValueError: return "Cookies 缺少 stoken ,请尝试重新绑定账号。" @@ -112,7 +109,7 @@ class DailyNoteTasksPlugin(Plugin.Conversation): validate = WebAppData(**result.data) except ValidationError: await message.reply_text( - "数据错误\n树脂提醒数值必须在 60 ~ 200 之间\n洞天宝钱提醒数值必须在 100 ~ 2400 之间\n每日任务提醒时间必须在 0 ~ 23 之间", + "数据错误\n电量提醒数值必须在 60 ~ 200 之间\n每日任务提醒时间必须在 0 ~ 23 之间", reply_markup=ReplyKeyboardRemove(), ) return ConversationHandler.END diff --git a/plugins/genshin/help.py b/plugins/zzz/help.py similarity index 100% rename from plugins/genshin/help.py rename to plugins/zzz/help.py diff --git a/plugins/genshin/help_raw.py b/plugins/zzz/help_raw.py similarity index 100% rename from plugins/genshin/help_raw.py rename to plugins/zzz/help_raw.py diff --git a/plugins/genshin/redeem/redeem.py b/plugins/zzz/redeem/redeem.py similarity index 98% rename from plugins/genshin/redeem/redeem.py rename to plugins/zzz/redeem/redeem.py index 9079050..2fce165 100644 --- a/plugins/genshin/redeem/redeem.py +++ b/plugins/zzz/redeem/redeem.py @@ -15,7 +15,7 @@ from gram_core.basemodel import RegionEnum from gram_core.services.cookies import CookiesService from gram_core.services.cookies.models import CookiesStatusEnum from gram_core.services.users.services import UserAdminService -from plugins.genshin.redeem.runner import RedeemRunner, RedeemResult, RedeemQueueFull +from plugins.zzz.redeem.runner import RedeemRunner, RedeemResult, RedeemQueueFull from plugins.tools.genshin import GenshinHelper from utils.log import logger diff --git a/plugins/genshin/redeem/runner.py b/plugins/zzz/redeem/runner.py similarity index 96% rename from plugins/genshin/redeem/runner.py rename to plugins/zzz/redeem/runner.py index a354ef7..94bf6fd 100644 --- a/plugins/genshin/redeem/runner.py +++ b/plugins/zzz/redeem/runner.py @@ -10,7 +10,7 @@ from gram_core.basemodel import RegionEnum from plugins.tools.genshin import GenshinHelper if TYPE_CHECKING: - from simnet import GenshinClient + from simnet import ZZZClient @dataclass @@ -19,7 +19,7 @@ class RedeemResult: code: str message: Optional[Message] = None error: Optional[str] = None - uid: Optional[int] = 0 + uid: Optional[int] = None count: Optional[List[int]] = None @@ -82,7 +82,7 @@ class RedeemRunner: region=RegionEnum.HOYOLAB if only_region else None, player_id=result.uid, ) as client: - client: "GenshinClient" + client: "ZZZClient" result.uid = client.player_id await client.redeem_code_by_hoyolab(result.code) except RegionNotSupported: diff --git a/plugins/genshin/sign.py b/plugins/zzz/sign.py similarity index 100% rename from plugins/genshin/sign.py rename to plugins/zzz/sign.py diff --git a/plugins/genshin/stats.py b/plugins/zzz/stats.py similarity index 68% rename from plugins/genshin/stats.py rename to plugins/zzz/stats.py index 5ae8a67..1a52be2 100644 --- a/plugins/genshin/stats.py +++ b/plugins/zzz/stats.py @@ -16,8 +16,7 @@ from utils.uid import mask_number if TYPE_CHECKING: from telegram import Update from telegram.ext import ContextTypes - from simnet.models.genshin.chronicle.stats import GenshinUserStats - from simnet import GenshinClient + from simnet import ZZZClient __all__ = ("PlayerStatsPlugins",) @@ -33,6 +32,7 @@ class PlayerStatsPlugins(Plugin): @handler.message(filters.Regex("^玩家统计查询(.*)"), player=True, block=False) async def command_start(self, update: "Update", _: "ContextTypes.DEFAULT_TYPE") -> Optional[int]: user_id = await self.get_real_user_id(update) + nickname = self.get_real_user_name(update) uid, offset = self.get_real_uid_or_offset(update) message = update.effective_message self.log_user(update, logger.info, "查询游戏用户命令请求") @@ -40,7 +40,7 @@ class PlayerStatsPlugins(Plugin): async with self.helper.genshin_or_public(user_id, uid=uid, offset=offset) as client: if not client.public: await client.get_record_cards() - render_result = await self.render(client, client.player_id) + render_result = await self.render(client, nickname) except TooManyRequestPublicCookies: await message.reply_text("用户查询次数过多 请稍后重试") return @@ -56,65 +56,38 @@ class PlayerStatsPlugins(Plugin): await message.reply_chat_action(ChatAction.UPLOAD_PHOTO) await render_result.reply_photo(message, filename=f"{client.player_id}.png") - async def render(self, client: "GenshinClient", uid: Optional[int] = None) -> RenderResult: - if uid is None: - uid = client.player_id - - user_info = await client.get_genshin_user(uid) + async def render(self, client: "ZZZClient", nickname: str) -> RenderResult: + uid = client.player_id + user_info = await client.get_zzz_user(uid) # 因为需要替换线上图片地址为本地地址,先克隆数据,避免修改原数据 user_info = user_info.copy(deep=True) data = { "uid": mask_number(uid), - "info": user_info.info, "stats": user_info.stats, - "explorations": user_info.explorations, - "skip_explor": [10], - "teapot": user_info.teapot, + "nickname": nickname, "stats_labels": [ - ("活跃天数", "days_active"), - ("成就达成数", "achievements"), - ("获取角色数", "characters"), - ("深境螺旋", "spiral_abyss"), - ("解锁传送点", "unlocked_waypoints"), - ("解锁秘境", "unlocked_domains"), - ("奇馈宝箱数", "remarkable_chests"), - ("华丽宝箱数", "luxurious_chests"), - ("珍贵宝箱数", "precious_chests"), - ("精致宝箱数", "exquisite_chests"), - ("普通宝箱数", "common_chests"), - ("风神瞳", "anemoculi"), - ("岩神瞳", "geoculi"), - ("雷神瞳", "electroculi"), - ("草神瞳", "dendroculi"), - ("水神瞳", "hydroculi"), + ("活跃天数", "active_days"), + ("获取角色数", "avatar_num"), + ("绳网声望", "world_level_name"), + ("式舆防卫战防线", "cur_period_zone_layer_count"), + ("获得邦布数", "buddy_num"), ], "style": random.choice(["mondstadt", "liyue"]), # nosec } - await self.cache_images(user_info) - return await self.template_service.render( - "genshin/stats/stats.jinja2", + "zzz/stats/stats.jinja2", data, - {"width": 650, "height": 800}, + {"width": 650, "height": 400}, full_page=True, ) - async def cache_images(self, data: "GenshinUserStats") -> None: - """缓存所有图片到本地""" - # TODO: 并发下载所有资源 - - # 探索地区 - for item in data.explorations: - item.__config__.allow_mutation = True - item.icon = await self.download_resource(item.icon) - item.cover = await self.download_resource(item.cover) - async def stats_use_by_inline(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE"): callback_query = update.callback_query user = update.effective_user + nickname = self.get_real_user_name(update) user_id = user.id uid = IInlineUseData.get_uid_from_context(context) @@ -124,7 +97,7 @@ class PlayerStatsPlugins(Plugin): async with self.helper.genshin_or_public(user_id, uid=uid) as client: if not client.public: await client.get_record_cards() - render_result = await self.render(client, client.player_id) + render_result = await self.render(client, nickname) except TooManyRequestPublicCookies: notice = "用户查询次数过多 请稍后重试" except AttributeError as exc: diff --git a/plugins/genshin/verify.py b/plugins/zzz/verify.py similarity index 100% rename from plugins/genshin/verify.py rename to plugins/zzz/verify.py diff --git a/pyproject.toml b/pyproject.toml index c17dc4b..33e6b28 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,6 @@ dependencies = [ "playwright==1.44.0", "aiosqlite[sqlite]<1.0.0,>=0.20.0", "simnet @ git+https://github.com/PaiGramTeam/SIMNet", - "gcsim-pypi<3.0.0,>=2.23.0", "psutil<7.0.0,>=6.0.0", "influxdb-client[async,ciso]>=1.43.0", ] diff --git a/requirements.txt b/requirements.txt index 1468f7c..2f393ca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,10 +7,10 @@ aiohttp==3.9.5 aiolimiter==1.1.0 aiosignal==1.3.1 aiosqlite[sqlite]==0.20.0 -alembic==1.13.1 +alembic==1.13.2 anyio==4.4.0 apscheduler==3.10.4 -arko-wrapper==0.2.8 +arko-wrapper==0.3.0 async-lru==2.0.4 async-timeout==4.0.3; python_full_version < "3.11.3" asyncmy==0.2.9 @@ -19,7 +19,7 @@ backports-zoneinfo==0.2.1; python_version < "3.9" beautifulsoup4==4.12.3 black==24.4.2 cachetools==5.3.3 -certifi==2024.6.2 +certifi==2024.7.4 cffi==1.16.0; platform_python_implementation != "PyPy" ciso8601==2.3.1 click==8.1.7 @@ -31,12 +31,11 @@ email-validator==2.2.0 enkanetwork-py @ git+https://github.com/PaiGramTeam/EnkaNetwork.py@ca2d8e5fa14714755c6990e986fc30fe430b0e4a et-xmlfile==1.1.0 exceptiongroup==1.2.1; python_version < "3.11" -fakeredis==2.23.2 +fakeredis==2.23.3 fastapi==0.111.0 fastapi-cli==0.0.4 flaky==3.8.1 frozenlist==1.4.1 -gcsim-pypi==2.23.0 gitdb==4.0.11 gitpython==3.1.43 greenlet==3.0.3 @@ -57,11 +56,11 @@ markupsafe==2.1.5 mdurl==0.1.2 multidict==6.0.5 mypy-extensions==1.0.0 -openpyxl==3.1.4 -orjson==3.10.5 +openpyxl==3.1.5 +orjson==3.10.6 packaging==24.1 pathspec==0.12.1 -pillow==10.3.0 +pillow==10.4.0 platformdirs==4.2.2 playwright==1.44.0 pluggy==1.5.0 @@ -82,14 +81,14 @@ python-multipart==0.0.9 python-telegram-bot[ext,rate-limiter]==21.3 pytz==2024.1 pyyaml==6.0.1 -rapidfuzz==3.9.3 +rapidfuzz==3.9.4 reactivex==4.0.4 redis==5.0.7 rich==13.7.1 -sentry-sdk==2.7.0 -setuptools==70.1.1 +sentry-sdk==2.7.1 +setuptools==70.2.0 shellingham==1.5.4 -simnet @ git+https://github.com/PaiGramTeam/SIMNet@277a33321a20909541b46bf4ecf794fd47e19fb1 +simnet @ git+https://github.com/PaiGramTeam/SIMNet@05fcb568d6c1fe44a4f917c996198bfe62a00053 six==1.16.0 smmap==5.0.1 sniffio==1.3.1 diff --git a/resources/bot/help/example.html b/resources/bot/help/example.html index 6a17454..9cb7218 100644 --- a/resources/bot/help/example.html +++ b/resources/bot/help/example.html @@ -11,7 +11,7 @@
-

PaiGram

+

MibooGram

使用说明

@@ -33,18 +33,6 @@
-
-
/weapon
-
查询武器
-
-
-
/strategy
-
查询角色攻略
-
-
-
/material
-
角色培养素材查询
-
@@ -53,13 +41,6 @@
玩家统计查询
-
-
- /player_card - -
-
角色卡片
-
@@ -68,63 +49,6 @@
查询实时便笺
-
-
- /ledger - -
-
查询当月旅行札记
-
-
-
- /abyss - -
-
查询当期深渊战绩
-
-
-
- /abyss_team - -
-
查询深渊推荐配队
-
-
-
- /avatars - -
-
查询角色练度
-
-
-
- /reg_time - -
-
原神账号注册时间
-
- -
-
- /gacha_log - -
-
抽卡记录
-
-
-
- /gacha_count - -
-
抽卡统计
-
-
-
- /pay_log - -
-
充值记录
-
/sign @@ -132,30 +56,6 @@
每日签到 | 查询
-
-
/daily_material
-
每日素材
-
- -
-
/hilichurls
-
丘丘语字典
-
-
-
/birthday
-
角色生日
-
-
-
- /birthday_card - -
-
领取角色生日画片
-
-
-
/calendar
-
活动日历
-
@@ -167,46 +67,6 @@

其他命令

-
-
/wish
-
抽卡模拟器(非洲人模拟器)
-
-
-
/set_wish
-
抽卡模拟器定轨
-
-
-
/quiz
-
- 派蒙的十万个为什么 -
-
- -
-
/gacha_log_import
-
导入抽卡记录
-
-
-
/gacha_log_export
-
导出抽卡记录
-
-
-
/gacha_log_delete
-
删除抽卡记录
-
- -
-
/pay_log_import
-
导入充值记录
-
-
-
/pay_log_export
-
导出充值记录
-
-
-
/pay_log_delete
-
删除充值记录
-
/setuid
diff --git a/resources/zzz/daily_note/daily_note.css b/resources/zzz/daily_note/daily_note.css new file mode 100644 index 0000000..e5fa037 --- /dev/null +++ b/resources/zzz/daily_note/daily_note.css @@ -0,0 +1,121 @@ +@font-face { + font-family: "tttgbnumber"; + src: url("../../fonts/tttgbnumber.ttf"); + font-weight: normal; + font-style: normal; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; + user-select: none; +} + +body { + font-size: 16px; + font-family: "tttgbnumber", system-ui; + transform: scale(1.5); + transform-origin: 0 0; + color: #1e1f20; +} + +.container { + width: 400px; + height: 365px; + background: #f0eae3; + padding: 8px 16px; +} + +.title { + display: flex; + position: relative; + margin-bottom: 9px; + color: #504c49; +} + +.title .id { + flex: 1; + line-height: 18px; + padding-left: 10px; +} + +.title .id:before { + content: " "; + position: absolute; + width: 5px; + height: 24px; + border-radius: 1px; + left: 0px; + top: -3px; + background: #d3bc8d; +} + +.title .day { + line-height: 18px; +} + +.item { + border: 1px solid #dfd8d1; + display: flex; + height: 49px; + margin-top: 5px; +} + +.item .main { + display: flex; + flex: 1; + background-color: #f5f1eb; + position: relative; + /* font-weight: bold; */ +} + +.item .main .bg { + width: 100px; + height: 100%; + position: absolute; + left: 0; + background-size: 100% auto; + background-image: url(./items/bg.png); +} + +.item .main .icon { + width: 25px; + height: 25px; + margin: 11px 8px 0 8px; +} + +.item .main .info { + padding-top: 7px; +} + +.item .main .info .name { + font-size: 14px; + /* color: #5f5f5d; */ + line-height: 1; + margin-bottom: 7px; +} + +.item .main .info .time { + font-size: 12px; + /* font-weight: 400; */ + color: #5f5f5d; + line-height: 1; +} + +.item .right { + display: flex; + align-items: center; + justify-content: center; + width: 96px; + height: 100%; + background-color: #ece3d8; + font-size: 16px; + color: #504c49; + line-height: 55px; +} + +.item .right .red { + color: #f24e4c; +} + diff --git a/resources/zzz/daily_note/daily_note.jinja2 b/resources/zzz/daily_note/daily_note.jinja2 new file mode 100644 index 0000000..1863ec3 --- /dev/null +++ b/resources/zzz/daily_note/daily_note.jinja2 @@ -0,0 +1,81 @@ + + + + + + + + +
+
+
+ ID:{{ uid }} +
+
+ {{ day }} +
+
+
+
+
+ +
+
电量
+
+ {% if resin_recovery_time %} + 将于{{ resin_recovery_time }} 全部恢复 + {% else %} + 电量已完全恢复 + {% endif %} +
+
+
+
+ + {{ current_resin }}/{{ max_resin }} + +
+
+
+
+
+ +
+
录像店经营
+
+
+
+ + {% if exp_status == 'DONE' %} + 已完成 + {% else %} + 正在营业 + {% endif %} + +
+
+
+
+
+ +
+
每日活跃
+
+ {% if current_train_score == max_train_score %} + 每日活跃已完成 + {% else %} + 每日活跃未完成 + {% endif %} +
+
+
+
+ {{ current_train_score }}/{{ max_train_score }} +
+
+
+ + + diff --git a/resources/zzz/daily_note/items/Inter-Knot-Credit.png b/resources/zzz/daily_note/items/Inter-Knot-Credit.png new file mode 100644 index 0000000..a4ca6f3 Binary files /dev/null and b/resources/zzz/daily_note/items/Inter-Knot-Credit.png differ diff --git a/resources/zzz/daily_note/items/Investigation-Point.png b/resources/zzz/daily_note/items/Investigation-Point.png new file mode 100644 index 0000000..3379976 Binary files /dev/null and b/resources/zzz/daily_note/items/Investigation-Point.png differ diff --git a/resources/zzz/daily_note/items/bg.png b/resources/zzz/daily_note/items/bg.png new file mode 100644 index 0000000..b123888 Binary files /dev/null and b/resources/zzz/daily_note/items/bg.png differ diff --git a/resources/zzz/daily_note/items/电量.png b/resources/zzz/daily_note/items/电量.png new file mode 100644 index 0000000..7fd4958 Binary files /dev/null and b/resources/zzz/daily_note/items/电量.png differ diff --git a/resources/zzz/stats/background/liyue.png b/resources/zzz/stats/background/liyue.png new file mode 100644 index 0000000..1a16959 Binary files /dev/null and b/resources/zzz/stats/background/liyue.png differ diff --git a/resources/zzz/stats/background/mondstadt.png b/resources/zzz/stats/background/mondstadt.png new file mode 100644 index 0000000..d36b7df Binary files /dev/null and b/resources/zzz/stats/background/mondstadt.png differ diff --git a/resources/zzz/stats/example/liyue.css b/resources/zzz/stats/example/liyue.css new file mode 100644 index 0000000..4cca478 --- /dev/null +++ b/resources/zzz/stats/example/liyue.css @@ -0,0 +1,62 @@ +:root { + --primary: #ffeabd; +} + +body { + background-color: #f5f6fb; +} + +.header { + background-image: url(../background/liyue.png); + box-shadow: 0 0 16px rgb(255 233 144 / 50%); +} + +.box { + background-color: #9c433d; + box-shadow: 0 0 16px rgb(255 233 144 / 50%); +} + +.box-title { + background-color: rgb(255, 200, 122, 0.1); + --tw-ring-color: #ff9966; +} + +.pointer-bar { + width: 95%; + height: 8px; + display: inline-block; + background-color: rgb(0, 0, 0, 0.1); + border-radius: 0.25rem; +} + +.pointer-progress-bar { + border-radius: 0.25rem; + height: 100%; + background: linear-gradient(to bottom, #f5efcd, #f8eabd, #ffdf90); +} + +.name { + background: linear-gradient(to bottom, #ffffff, #ffeabd, #ffdf90); + background-clip: text; + -webkit-background-clip: text; + color: transparent; +} + +.uid { + color: var(--primary); + background: linear-gradient(to right, rgb(0, 0, 0, 0), #cc6666, rgb(0, 0, 0, 0)); +} + +.about { + background-color: #e0dad3; + color: #8a4d30; +} + +.box-stats { + color: var(--primary); +} + +.box-stats-label { + color: var(--primary) !important; + opacity: 0.65; +} diff --git a/resources/zzz/stats/example/liyue.html b/resources/zzz/stats/example/liyue.html new file mode 100644 index 0000000..1abd585 --- /dev/null +++ b/resources/zzz/stats/example/liyue.html @@ -0,0 +1,256 @@ + + + + Title + + + + + + +
+
+
+

+ 小何 + lv.58 +

+

UID - 125324176

+
+
+
+
+

+ 数据总览 +

+
+
+
493
+
活跃天数
+
+ +
+
536
+
成就达成数
+
+ +
+
38
+
获取角色数
+
+ +
+
12-3
+
深境螺旋
+
+ +
+
227
+
解锁传送点
+
+ +
+
41
+
解锁秘境
+
+ +
+
58
+
奇馈宝箱数
+
+ +
+
128
+
华丽宝箱数
+
+ +
+
316
+
珍贵宝箱数
+
+ +
+
1184
+
精致宝箱数
+
+ +
+
1594
+
普通宝箱数
+
+ +
+
65
+
风神瞳
+
+ +
+
131
+
岩神瞳
+
+ +
+
180
+
雷神瞳
+
+ +
+
80
+
草神瞳
+
+
+
+ +
+

+ 世界探索 +

+
+
+ +
须弥
+
28.0%
+
+
+
+
+ +
+ +
层岩巨渊·地下矿区
+
98.7%
+
+
+
+
+ +
+ +
层岩巨渊
+
92.9%
+
+
+
+
+ +
+ +
渊下宫
+
98.3%
+
+
+
+
+ +
+ +
稻妻
+
100.0%
+
+
+
+
+ +
+ +
龙脊雪山
+
83.6%
+
+
+
+
+ +
+ +
璃月
+
95.9%
+
+
+
+
+ +
+ +
蒙德
+
100.0%
+
+
+
+
+
+
+ +
+ 所有数据会有一小时延迟 以游戏内为准 此处仅供参考 +
+
+
+ + diff --git a/resources/zzz/stats/example/mondstadt.css b/resources/zzz/stats/example/mondstadt.css new file mode 100644 index 0000000..17979cd --- /dev/null +++ b/resources/zzz/stats/example/mondstadt.css @@ -0,0 +1,49 @@ +body { + background-color: #f5f6fb; +} + +.header { + background-image: url(../background/mondstadt.png); + box-shadow: 0 0 8px rgb(123 242 248 / 50%); +} + +.box { + background-color: #fdfdf3; + box-shadow: 0 0 8px rgb(123 242 248 / 50%); +} + +.box-title { + background-color: #43849abb; + --tw-ring-color: #43849a; +} + +.pointer-bar { + width: 95%; + height: 8px; + display: inline-block; + background-color: rgb(0, 0, 0, 0.2); + border-radius: 0.25rem; +} + +.pointer-progress-bar { + border-radius: 0.25rem; + height: 100%; + background: #fff6e2; +} + +.name { + background: linear-gradient(to bottom, #66bbee, #5ddddd, #55dddd); + background-clip: text; + -webkit-background-clip: text; + color: transparent; +} + +.uid { + color: #fff; + background: linear-gradient(to right, rgb(0, 0, 0, 0), #5ddddd, rgb(0, 0, 0, 0)); +} + +.about { + background-color: #e0dad3; + color: #8a4d30; +} \ No newline at end of file diff --git a/resources/zzz/stats/example/mondstadt.html b/resources/zzz/stats/example/mondstadt.html new file mode 100644 index 0000000..1d9c14e --- /dev/null +++ b/resources/zzz/stats/example/mondstadt.html @@ -0,0 +1,261 @@ + + + + + Title + + + + + + +
+
+
+

+ 小何 + lv.58 +

+

UID - 125324176

+
+
+
+
+

+ 数据总览 +

+
+ +
+
491
+
活跃天数
+
+ +
+
536
+
成就达成数
+
+ +
+
38
+
获取角色数
+
+ +
+
12-3
+
深境螺旋
+
+ +
+
227
+
解锁传送点
+
+ +
+
41
+
解锁秘境
+
+ +
+
58
+
奇馈宝箱数
+
+ +
+
127
+
华丽宝箱数
+
+ +
+
316
+
珍贵宝箱数
+
+ +
+
1180
+
精致宝箱数
+
+ +
+
1591
+
普通宝箱数
+
+ +
+
65
+
风神瞳
+
+ +
+
131
+
岩神瞳
+
+ +
+
180
+
雷神瞳
+
+ +
+
79
+
草神瞳
+
+ +
+
+ +
+

+ 世界探索 +

+
+ +
+ +
须弥
+
26.0%
+
+
+
+
+ +
+ +
层岩巨渊·地下矿区
+
98.7%
+
+
+
+
+ +
+ +
层岩巨渊
+
92.9%
+
+
+
+
+ +
+ +
渊下宫
+
98.3%
+
+
+
+
+ +
+ +
稻妻
+
100.0%
+
+
+
+
+ +
+ +
龙脊雪山
+
83.6%
+
+
+
+
+ +
+ +
璃月
+
95.9%
+
+
+
+
+ +
+ +
蒙德
+
100.0%
+
+
+
+
+ +
+
+ +
+ 所有数据会有一小时延迟 以游戏内为准 此处仅供参考 +
+
+
+ + \ No newline at end of file diff --git a/resources/zzz/stats/liyue.css b/resources/zzz/stats/liyue.css new file mode 100644 index 0000000..5e73809 --- /dev/null +++ b/resources/zzz/stats/liyue.css @@ -0,0 +1,62 @@ +:root { + --primary: #ffeabd; +} + +body { + background-color: #f5f6fb; +} + +.header { + background-image: url(./background/liyue.png); + box-shadow: 0 0 16px rgb(255 233 144 / 50%); +} + +.box { + background-color: #9c433d; + box-shadow: 0 0 16px rgb(255 233 144 / 50%); +} + +.box-title { + background-color: rgb(255, 200, 122, 0.1); + --tw-ring-color: #ff9966; +} + +.pointer-bar { + width: 95%; + height: 8px; + display: inline-block; + background-color: rgb(0, 0, 0, 0.1); + border-radius: 0.25rem; +} + +.pointer-progress-bar { + border-radius: 0.25rem; + height: 100%; + background: linear-gradient(to bottom, #f5efcd, #f8eabd, #ffdf90); +} + +.name { + background: linear-gradient(to bottom, #ffffff, #ffeabd, #ffdf90); + background-clip: text; + -webkit-background-clip: text; + color: transparent; +} + +.uid { + color: var(--primary); + background: linear-gradient(to right, rgb(0, 0, 0, 0), #cc6666, rgb(0, 0, 0, 0)); +} + +.about { + background-color: #e0dad3; + color: #8a4d30; +} + +.box-stats { + color: var(--primary); +} + +.box-stats-label { + color: var(--primary) !important; + opacity: 0.65; +} diff --git a/resources/zzz/stats/mondstadt.css b/resources/zzz/stats/mondstadt.css new file mode 100644 index 0000000..4122e18 --- /dev/null +++ b/resources/zzz/stats/mondstadt.css @@ -0,0 +1,49 @@ +body { + background-color: #f5f6fb; +} + +.header { + background-image: url(background/mondstadt.png); + box-shadow: 0 0 8px rgb(123 242 248 / 50%); +} + +.box { + background-color: #fdfdf3; + box-shadow: 0 0 8px rgb(123 242 248 / 50%); +} + +.box-title { + background-color: #43849abb; + --tw-ring-color: #43849a; +} + +.pointer-bar { + width: 95%; + height: 8px; + display: inline-block; + background-color: rgb(0, 0, 0, 0.2); + border-radius: 0.25rem; +} + +.pointer-progress-bar { + border-radius: 0.25rem; + height: 100%; + background: #fff6e2; +} + +.name { + background: linear-gradient(to bottom, #66bbee, #5ddddd, #55dddd); + background-clip: text; + -webkit-background-clip: text; + color: transparent; +} + +.uid { + color: #fff; + background: linear-gradient(to right, rgb(0, 0, 0, 0), #5ddddd, rgb(0, 0, 0, 0)); +} + +.about { + background-color: #e0dad3; + color: #8a4d30; +} \ No newline at end of file diff --git a/resources/zzz/stats/old/info.css b/resources/zzz/stats/old/info.css new file mode 100644 index 0000000..2b88125 --- /dev/null +++ b/resources/zzz/stats/old/info.css @@ -0,0 +1,92 @@ +body { + background-repeat: no-repeat; + background-size: cover; + background-position: center; + background-attachment: fixed; +} + + +#container { + +} + +.account-center-header { + padding: 10px 8px; + background-color: rgba(225, 225, 225, 0.5); + /*background-image: url("./0.jpg"); + background-size: cover;*/ +} + +.user-avatar { + width: 100%; + height: 100%; +} + +.user-info-1 { + padding: 10px 8px; + background-color: rgba(225, 225, 225, 0.5); +} + +.world-exploration { + padding: 10px 8px; + background-color: rgba(225, 225, 225, 0.5); +} + +.teapot { + padding: 10px 8px; + background-color: rgba(225, 225, 225, 0.5); +} + +.account-center-header-avatar { + width: 120px; + height: 120px; +} + +.teapot-info-icon { + height: 96px; + overflow: hidden; +} + +.teapot-info-img { + width: 100%; +} + +.world-exploration-info { + border: 2px solid rgb(0, 0, 0, 0.2); +} + +.world-exploration-info-icon { + height: 96px; + overflow: hidden; +} + +.world-exploration-info-img { + filter: brightness(0); + height: 100%; +} + +.world-exploration-info-hr-1 { + width: 95%; +} + +.world-exploration-info-pointer-bar-body { + width: 95%; + height: 4px; + display: inline-block; + border-radius: 2px; + background-color: rgb(0, 0, 0, 0.3); +} + +.world-exploration-info-pointer-progress-bar { + height: 100%; + background-color: #000000; + border-radius: 2px; +} + +.background-color { + background-color: rgb(225, 225, 225, 0.75); +} + +.teapot-info-name { + background-color: rgba(225, 225, 225, 0.5); +} \ No newline at end of file diff --git a/resources/zzz/stats/old/info.html b/resources/zzz/stats/old/info.html new file mode 100644 index 0000000..ca027e5 --- /dev/null +++ b/resources/zzz/stats/old/info.html @@ -0,0 +1,183 @@ + + + + + Title + + + + + + +
+
+ + + +
+
+ + \ No newline at end of file diff --git a/resources/zzz/stats/stats.jinja2 b/resources/zzz/stats/stats.jinja2 new file mode 100644 index 0000000..5e688ac --- /dev/null +++ b/resources/zzz/stats/stats.jinja2 @@ -0,0 +1,45 @@ + + + + + Title + + + + + + +
+
+
+

+ {{ nickname }} +

+

UID - {{ uid }}

+
+
+
+
+

+ 数据总览 +

+
+ {% for label, key in stats_labels %} +
+
{{ stats[key] }}
+
{{ label }}
+
+ {% endfor %} +
+
+ +
+ 所有数据会有一小时延迟 以游戏内为准 此处仅供参考 +
+
+
+ + diff --git a/utils/uid.py b/utils/uid.py index 2ae6563..90f040d 100644 --- a/utils/uid.py +++ b/utils/uid.py @@ -4,6 +4,8 @@ import re def mask_number(number): number_str = str(number) masked_number = None + if len(number_str) == 8: + masked_number = re.sub(r"(\d{2})(\d{3})(\d{3})", r"\1***\3", number_str) if len(number_str) == 9: masked_number = re.sub(r"(\d{2})(\d{4})(\d{3})", r"\1****\3", number_str) if len(number_str) == 10: