diff --git a/core/dependence/assets.py b/core/dependence/assets.py index 9077eb8..c1c625e 100644 --- a/core/dependence/assets.py +++ b/core/dependence/assets.py @@ -23,7 +23,6 @@ from metadata.genshin import AVATAR_DATA, HONEY_DATA, MATERIAL_DATA, NAMECARD_DA 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 @@ -39,7 +38,7 @@ NAME_MAP_TYPE = Dict[str, StrOrURL] ASSETS_PATH = PROJECT_ROOT.joinpath("resources/assets") ASSETS_PATH.mkdir(exist_ok=True, parents=True) - +HONEY_HOST = "" DATA_MAP = {"avatar": AVATAR_DATA, "weapon": WEAPON_DATA, "material": MATERIAL_DATA} DEFAULT_EnkaAssets = EnkaAssets(lang="chs") @@ -472,7 +471,6 @@ class _NamecardAssets(_AssetsService): def __call__(self, target: int) -> "_NamecardAssets": 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 @@ -533,14 +531,5 @@ class AssetsService(BaseService.Dependence): ): 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/services/game/cache.py b/core/services/game/cache.py index 48531ca..c0bffb3 100644 --- a/core/services/game/cache.py +++ b/core/services/game/cache.py @@ -27,3 +27,12 @@ class GameCache: class GameCacheForStrategy(BaseService.Component, GameCache): qname = "game:strategy" + + async def get_file(self, character_name: str): + qname = f"{self.qname}:{character_name}" + return await self.client.get(qname) + + async def set_file(self, character_name: str, file: str): + qname = f"{self.qname}:{character_name}" + await self.client.set(qname, file) + await self.client.expire(qname, self.ttl) diff --git a/core/services/game/services.py b/core/services/game/services.py index 5b30a3f..3e0f778 100644 --- a/core/services/game/services.py +++ b/core/services/game/services.py @@ -1,52 +1,17 @@ -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): + def __init__(self, cache: GameCacheForStrategy): self._cache = cache - self._hyperion = Hyperion() - if collections is None: - self._collections = [839176, 839179, 839181, 1180811] - 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_cache(self, character_name: str) -> str: + cache = await self._cache.get_file(character_name) + if cache is not None: + return cache - 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] + async def set_strategy_cache(self, character_name: str, file: str) -> None: + await self._cache.set_file(character_name, file) 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/cache.py b/core/services/wiki/cache.py deleted file mode 100644 index 213227f..0000000 --- a/core/services/wiki/cache.py +++ /dev/null @@ -1,37 +0,0 @@ -import ujson as json - -from core.base_service import BaseService -from core.dependence.redisdb import RedisDB -from modules.wiki.base import Model - -__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 = json.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 = json.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] = json.loads(item) - return result diff --git a/core/services/wiki/services.py b/core/services/wiki/services.py index b8758d6..be4f0e4 100644 --- a/core/services/wiki/services.py +++ b/core/services/wiki/services.py @@ -1,103 +1,48 @@ -from typing import List, NoReturn, Optional +from typing import NoReturn 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 modules.wiki.material import Material +from modules.wiki.monster import Monster +from modules.wiki.relic import Relic +from modules.wiki.light_cone import LightCone +from modules.wiki.raider import Raider 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 + def __init__(self): + self.character = Character() + self.material = Material() + self.monster = Monster() + self.relic = Relic() + self.light_cone = LightCone() + self.raider = Raider() - async def refresh_weapon(self) -> NoReturn: - 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) -> NoReturn: - 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 initialize(self) -> None: + logger.info("正在加载 Wiki 数据") + await self.character.read() + await self.material.read() + await self.monster.read() + await self.relic.read() + await self.light_cone.read() + await self.raider.read() + logger.info("加载 Wiki 数据完成") async def refresh_wiki(self) -> NoReturn: - """ - 用于把Redis的缓存全部加载进Python - :return: - """ logger.info("正在重新获取Wiki") - logger.info("正在重新获取武器信息") - await self.refresh_weapon() logger.info("正在重新获取角色信息") - await self.refresh_characters() + await self.character.refresh() + logger.info("正在重新获取材料信息") + await self.material.refresh() + logger.info("正在重新获取敌对生物信息") + await self.monster.refresh() + logger.info("正在重新获取遗器信息") + await self.relic.refresh() + logger.info("正在重新获取光锥信息") + await self.light_cone.refresh() + logger.info("正在重新获取攻略信息") + await self.raider.refresh() logger.info("刷新成功") - - async def init(self) -> NoReturn: - """ - 用于把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/metadata/scripts/honey.py b/metadata/scripts/honey.py index e18cdb1..dd6cf2f 100644 --- a/metadata/scripts/honey.py +++ b/metadata/scripts/honey.py @@ -7,12 +7,12 @@ from typing import Dict, List, Optional import ujson as json from aiofiles import open as async_open from httpx import AsyncClient, HTTPError, Response - -from modules.wiki.base import HONEY_HOST from utils.const import PROJECT_ROOT from utils.log import logger from utils.typedefs import StrOrInt +HONEY_HOST = "" + __all__ = [ "get_avatar_data", "get_artifact_data", diff --git a/modules/apihelper/client/base/httpxrequest.py b/modules/apihelper/client/base/httpxrequest.py index 0a7bb3c..9c864fe 100644 --- a/modules/apihelper/client/base/httpxrequest.py +++ b/modules/apihelper/client/base/httpxrequest.py @@ -19,7 +19,7 @@ timeout = httpx.Timeout( class HTTPXRequest(AbstractAsyncContextManager): def __init__(self, *args, headers=None, **kwargs): - self._client = httpx.AsyncClient(headers=headers, timeout=timeout, *args, **kwargs) + self._client = httpx.AsyncClient(headers=headers, *args, **kwargs) async def __aenter__(self): try: diff --git a/modules/wiki/base.py b/modules/wiki/base.py index d2b5fad..abe4d1e 100644 --- a/modules/wiki/base.py +++ b/modules/wiki/base.py @@ -1,248 +1,39 @@ -import asyncio -import re -from abc import abstractmethod -from asyncio import Queue -from multiprocessing import Value -from ssl import SSLZeroReturnError -from typing import AsyncIterator, ClassVar, List, Optional, Tuple, Union +from pathlib import Path +from typing import List, Dict -import anyio -from bs4 import BeautifulSoup -from httpx import URL, AsyncClient, HTTPError, Response -from pydantic import BaseConfig as PydanticBaseConfig -from pydantic import BaseModel as PydanticBaseModel -from typing_extensions import Self - -try: - import ujson as jsonlib -except ImportError: - import json as jsonlib - -__all__ = ["Model", "WikiModel", "HONEY_HOST"] - -HONEY_HOST = URL("https://genshin.honeyhunterworld.com/") +import aiofiles +import ujson as jsonlib +from httpx import AsyncClient -class Model(PydanticBaseModel): - """基类""" +class WikiModel: + BASE_URL = "https://raw.githubusercontent.com/PaiGramTeam/HonkaiStarRailWikiDataParser/remote/data/" + BASE_PATH = Path("data/wiki") + BASE_PATH.mkdir(parents=True, exist_ok=True) - def __new__(cls, *args, **kwargs): - # 让每次new的时候都解析 - cls.update_forward_refs() - return super(Model, cls).__new__(cls) # pylint: disable=E1120 + def __init__(self): + self.client = AsyncClient(timeout=120.0) - class Config(PydanticBaseConfig): - # 使用 ujson 作为解析库 - json_dumps = jsonlib.dumps - json_loads = jsonlib.loads - - -class WikiModel(Model): - # noinspection PyUnresolvedReferences - """wiki所用到的基类 - - Attributes: - id (:obj:`int`): ID - name (:obj:`str`): 名称 - rarity (:obj:`int`): 星级 - - _client (:class:`httpx.AsyncClient`): 发起 http 请求的 client - """ - _client: ClassVar[AsyncClient] = AsyncClient() - - id: str - name: str - rarity: int + async def remote_get(self, url: str): + return await self.client.get(url) @staticmethod - @abstractmethod - def scrape_urls() -> List[URL]: - """爬取的目标网页集合 - - 例如有关武器的页面有: - [单手剑](https://genshin.honeyhunterworld.com/fam_sword/?lang=CHS) - [双手剑](https://genshin.honeyhunterworld.com/fam_claymore/?lang=CHS) - [长柄武器](https://genshin.honeyhunterworld.com/fam_polearm/?lang=CHS) - 。。。 - 这个函数就是返回这些页面的网址所组成的 List - - """ - - @classmethod - async def _client_get(cls, url: Union[URL, str], retry_times: int = 5, sleep: float = 1) -> Response: - """用自己的 client 发起 get 请求的快捷函数 - - Args: - url: 发起请求的 url - retry_times: 发生错误时的重复次数。不能小于 0 . - sleep: 发生错误后等待重试的时间,单位为秒。 - Returns: - 返回对应的请求 - Raises: - 请求所需要的异常 - """ - for _ in range(retry_times): - try: - return await cls._client.get(url, follow_redirects=True) - except (HTTPError, SSLZeroReturnError): - await anyio.sleep(sleep) - return await cls._client.get(url, follow_redirects=True) # 防止 retry_times 等于 0 的时候无法发生请求 - - @classmethod - @abstractmethod - async def _parse_soup(cls, soup: BeautifulSoup) -> Self: - """解析 soup 生成对应 WikiModel - - Args: - soup: 需要解析的 soup - Returns: - 返回对应的 WikiModel - """ - - @classmethod - async def _scrape(cls, url: Union[URL, str]) -> Self: - """从 url 中爬取数据,并返回对应的 Model - - Args: - url: 目标 url. 可以为字符串 str , 也可以为 httpx.URL - Returns: - 返回对应的 WikiModel - """ - response = await cls._client_get(url) - return await cls._parse_soup(BeautifulSoup(response.text, "lxml")) - - @classmethod - async def get_by_id(cls, id_: str) -> Self: - """通过ID获取Model - - Args: - id_: 目标 ID - Returns: - 返回对应的 WikiModel - """ - return await cls._scrape(await cls.get_url_by_id(id_)) - - @classmethod - async def get_by_name(cls, name: str) -> Optional[Self]: - """通过名称获取Model - - Args: - name: 目标名 - Returns: - 返回对应的 WikiModel - """ - url = await cls.get_url_by_name(name) - return None if url is None else await cls._scrape(url) - - @classmethod - async def get_full_data(cls) -> List[Self]: - """获取全部数据的 Model - - Returns: - 返回能爬到的所有的 Model 所组成的 List - """ - return [i async for i in cls.full_data_generator()] - - @classmethod - async def full_data_generator(cls) -> AsyncIterator[Self]: - """Model 生成器 - - 这是一个异步生成器,该函数在使用时会爬取所有数据,并将其转为对应的 Model,然后存至一个队列中 - 当有需要时,再一个一个地迭代取出 - - Returns: - 返回能爬到的所有的 WikiModel 所组成的 List - """ - queue: Queue[Self] = Queue() # 存放 Model 的队列 - signal = Value("i", 0) # 一个用于异步任务同步的信号 - - async def task(u): - # 包装的爬虫任务 - await queue.put(await cls._scrape(u)) # 爬取一条数据,并将其放入队列中 - signal.value -= 1 # 信号量减少 1 ,说明该爬虫任务已经完成 - - for _, url in await cls.get_name_list(with_url=True): # 遍历爬取所有需要爬取的页面 - signal.value += 1 # 信号量增加 1 ,说明有一个爬虫任务被添加 - asyncio.create_task(task(url)) # 创建一个爬虫任务 - - while signal.value > 0 or not queue.empty(): # 当还有未完成的爬虫任务或存放数据的队列不为空时 - yield await queue.get() # 取出并返回一个存放的 Model - - def __str__(self) -> str: - return f"<{self.__class__.__name__} {super(WikiModel, self).__str__()}>" - - def __repr__(self) -> str: - return self.__str__() + async def dump(datas, path: Path): + async with aiofiles.open(path, "w", encoding="utf-8") as f: + await f.write(jsonlib.dumps(datas, indent=4, ensure_ascii=False)) @staticmethod - async def get_url_by_id(id_: str) -> URL: - """根据 id 获取对应的 url + async def read(path: Path) -> List[Dict]: + async with aiofiles.open(path, "r", encoding="utf-8") as f: + datas = jsonlib.loads(await f.read()) + return datas - 例如神里绫华的ID为 ayaka_002,对应的数据页url为 https://genshin.honeyhunterworld.com/ayaka_002/?lang=CHS + @staticmethod + async def save_file(data, path: Path): + async with aiofiles.open(path, "wb") as f: + await f.write(data) - Args: - id_ : 实列ID - Returns: - 返回对应的 url - """ - return HONEY_HOST.join(f"{id_}/?lang=CHS") - - @classmethod - async def _name_list_generator(cls, *, with_url: bool = False) -> AsyncIterator[Union[str, Tuple[str, URL]]]: - """一个 Model 的名称 和 其对应 url 的异步生成器 - - Args: - with_url: 是否返回相应的 url - Returns: - 返回对应的名称列表 或者 名称与url 的列表 - """ - urls = cls.scrape_urls() - queue: Queue[Union[str, Tuple[str, URL]]] = Queue() # 存放 Model 的队列 - signal = Value("i", len(urls)) # 一个用于异步任务同步的信号,初始值为存放所需要爬取的页面数 - - async def task(page: URL): - """包装的爬虫任务""" - response = await cls._client_get(page) - # 从页面中获取对应的 chaos data (未处理的json格式字符串) - chaos_data = re.findall(r"sortable_data\.push\((.*?)\);\s*sortable_cur_page", response.text)[0] - json_data = jsonlib.loads(chaos_data) # 转为 json - for data in json_data: # 遍历 json - data_name = re.findall(r">(.*)<", data[1])[0].strip() # 获取 Model 的名称 - if with_url: # 如果需要返回对应的 url - data_url = HONEY_HOST.join(re.findall(r"\"(.*?)\"", data[0])[0]) - await queue.put((data_name, data_url)) - else: - await queue.put(data_name) - signal.value = signal.value - 1 # 信号量减少 1 ,说明该爬虫任务已经完成 - - for url in urls: # 遍历需要爬出的页面 - asyncio.create_task(task(url)) # 添加爬虫任务 - while signal.value > 0 or not queue.empty(): # 当还有未完成的爬虫任务或存放数据的队列不为空时 - yield await queue.get() # 取出并返回一个存放的 Model - - @classmethod - async def get_name_list(cls, *, with_url: bool = False) -> List[Union[str, Tuple[str, URL]]]: - """获取全部 Model 的 名称 - - Returns: - 返回能爬到的所有的 Model 的名称所组成的 List - """ - return [i async for i in cls._name_list_generator(with_url=with_url)] - - @classmethod - async def get_url_by_name(cls, name: str) -> Optional[URL]: - """通过 Model 的名称获取对应的 url - - Args: - name: 实列名 - Returns: - 若有对应的实列,则返回对应的 url; 若没有, 则返回 None - """ - async for n, url in cls._name_list_generator(with_url=True): - if name == n: - return url - - @property - @abstractmethod - def icon(self): - """返回此 Model 的图标链接""" + @staticmethod + async def read_file(path: Path): + async with aiofiles.open(path, "rb") as f: + return await f.read() diff --git a/modules/wiki/character.py b/modules/wiki/character.py index 6820c77..a5b2baf 100644 --- a/modules/wiki/character.py +++ b/modules/wiki/character.py @@ -1,199 +1,46 @@ -import re -from typing import List, Optional +from typing import List, Dict, Optional -from bs4 import BeautifulSoup -from httpx import URL - -from modules.wiki.base import HONEY_HOST, Model, WikiModel -from modules.wiki.other import Association, Element, WeaponType - - -class Birth(Model): - """生日 - Attributes: - day: 天 - month: 月 - """ - - day: int - month: int - - -class CharacterAscension(Model): - """角色的突破材料 - - Attributes: - level: 等级突破材料 - skill: 技能/天赋培养材料 - """ - - level: List[str] = [] - skill: List[str] = [] - - -class CharacterState(Model): - """角色属性值 - - Attributes: - level: 等级 - HP: 生命 - ATK: 攻击力 - DEF: 防御力 - CR: 暴击率 - CD: 暴击伤害 - bonus: 突破属性 - """ - - level: str - HP: int - ATK: float - DEF: float - CR: str - CD: str - bonus: str - - -class CharacterIcon(Model): - icon: str - side: str - gacha: str - splash: Optional[str] +from modules.wiki.base import WikiModel +from modules.wiki.models.avatar import Avatar class Character(WikiModel): - """角色 - Attributes: - title: 称号 - occupation: 所属 - association: 地区 - weapon_type: 武器类型 - element: 元素 - birth: 生日 - constellation: 命之座 - cn_cv: 中配 - jp_cv: 日配 - en_cv: 英配 - kr_cv: 韩配 - description: 描述 - """ + avatar_url = WikiModel.BASE_URL + "avatars.json" + avatar_path = WikiModel.BASE_PATH / "avatars.json" - id: str - title: str - occupation: str - association: Association - weapon_type: WeaponType - element: Element - birth: Optional[Birth] - constellation: str - cn_cv: str - jp_cv: str - en_cv: str - kr_cv: str - description: str - ascension: CharacterAscension + def __init__(self): + super().__init__() + self.all_avatars: List[Avatar] = [] + self.all_avatars_map: Dict[int, Avatar] = {} + self.all_avatars_name: Dict[str, Avatar] = {} - stats: List[CharacterState] + def clear_class_data(self) -> None: + self.all_avatars.clear() + self.all_avatars_map.clear() + self.all_avatars_name.clear() - @classmethod - def scrape_urls(cls) -> List[URL]: - return [HONEY_HOST.join("fam_chars/?lang=CHS")] + async def refresh(self): + datas = await self.remote_get(self.avatar_url) + await self.dump(datas.json(), self.avatar_path) + await self.read() - @classmethod - async def _parse_soup(cls, soup: BeautifulSoup) -> "Character": - """解析角色页""" - soup = soup.select(".wp-block-post-content")[0] - tables = soup.find_all("table") - table_rows = tables[0].find_all("tr") + async def read(self): + if not self.avatar_path.exists(): + await self.refresh() + return + datas = await WikiModel.read(self.avatar_path) + self.clear_class_data() + for data in datas: + m = Avatar(**data) + self.all_avatars.append(m) + self.all_avatars_map[m.id] = m + self.all_avatars_name[m.name] = m - def get_table_text(row_num: int) -> str: - """一个快捷函数,用于返回表格对应行的最后一个单元格中的文本""" - return table_rows[row_num].find_all("td")[-1].text.replace("\xa0", "") + def get_by_id(self, cid: int) -> Optional[Avatar]: + return self.all_avatars_map.get(cid, None) - id_ = re.findall(r"img/(.*?_\d+)_.*", table_rows[0].find("img").attrs["src"])[0] - name = get_table_text(0) - if name != "旅行者": # 如果角色名不是 旅行者 - title = get_table_text(1) - occupation = get_table_text(2) - association = Association.convert(get_table_text(3).lower().title()) - rarity = len(table_rows[4].find_all("img")) - weapon_type = WeaponType[get_table_text(5)] - element = Element[get_table_text(6)] - birth = Birth(day=int(get_table_text(7)), month=int(get_table_text(8))) - constellation = get_table_text(10) - cn_cv = get_table_text(11) - jp_cv = get_table_text(12) - en_cv = get_table_text(13) - kr_cv = get_table_text(14) - else: - name = "空" if id_.endswith("5") else "荧" - title = get_table_text(0) - occupation = get_table_text(1) - association = Association.convert(get_table_text(2).lower().title()) - rarity = len(table_rows[3].find_all("img")) - weapon_type = WeaponType[get_table_text(4)] - element = Element[get_table_text(5)] - birth = None - constellation = get_table_text(7) - cn_cv = get_table_text(8) - jp_cv = get_table_text(9) - en_cv = get_table_text(10) - kr_cv = get_table_text(11) - description = get_table_text(-3) - ascension = CharacterAscension( - level=[ - target[0] - for i in table_rows[-2].find_all("a") - if (target := re.findall(r"/(.*)/", i.attrs["href"])) # 过滤掉错误的材料(honey网页的bug) - ], - skill=[re.findall(r"/(.*)/", i.attrs["href"])[0] for i in table_rows[-1].find_all("a")], - ) - stats = [] - for row in tables[2].find_all("tr")[1:]: - cells = row.find_all("td") - stats.append( - CharacterState( - level=cells[0].text, - HP=cells[1].text, - ATK=cells[2].text, - DEF=cells[3].text, - CR=cells[4].text, - CD=cells[5].text, - bonus=cells[6].text, - ) - ) - return Character( - id=id_, - name=name, - title=title, - occupation=occupation, - association=association, - weapon_type=weapon_type, - element=element, - birth=birth, - constellation=constellation, - cn_cv=cn_cv, - jp_cv=jp_cv, - rarity=rarity, - en_cv=en_cv, - kr_cv=kr_cv, - description=description, - ascension=ascension, - stats=stats, - ) + def get_by_name(self, name: str) -> Optional[Avatar]: + return self.all_avatars_name.get(name, None) - @classmethod - async def get_url_by_name(cls, name: str) -> Optional[URL]: - # 重写此函数的目的是处理主角名字的 ID - _map = {"荧": "playergirl_007", "空": "playerboy_005"} - if (id_ := _map.get(name)) is not None: - return await cls.get_url_by_id(id_) - return await super(Character, cls).get_url_by_name(name) - - @property - def icon(self) -> CharacterIcon: - return CharacterIcon( - icon=str(HONEY_HOST.join(f"/img/{self.id}_icon.webp")), - side=str(HONEY_HOST.join(f"/img/{self.id}_side_icon.webp")), - gacha=str(HONEY_HOST.join(f"/img/{self.id}_gacha_card.webp")), - splash=str(HONEY_HOST.join(f"/img/{self.id}_gacha_splash.webp")), - ) + def get_name_list(self) -> List[str]: + return list(self.all_avatars_name.keys()) diff --git a/modules/wiki/light_cone.py b/modules/wiki/light_cone.py new file mode 100644 index 0000000..fbf436f --- /dev/null +++ b/modules/wiki/light_cone.py @@ -0,0 +1,46 @@ +from typing import List, Dict, Optional + +from modules.wiki.base import WikiModel +from modules.wiki.models.light_cone import LightCone as LightConeModel + + +class LightCone(WikiModel): + light_cone_url = WikiModel.BASE_URL + "light_cones.json" + light_cone_path = WikiModel.BASE_PATH / "light_cones.json" + + def __init__(self): + super().__init__() + self.all_light_cones: List[LightConeModel] = [] + self.all_light_cones_map: Dict[int, LightConeModel] = {} + self.all_light_cones_name: Dict[str, LightConeModel] = {} + + def clear_class_data(self) -> None: + self.all_light_cones.clear() + self.all_light_cones_map.clear() + self.all_light_cones_name.clear() + + async def refresh(self): + datas = await self.remote_get(self.light_cone_url) + await self.dump(datas.json(), self.light_cone_path) + await self.read() + + async def read(self): + if not self.light_cone_path.exists(): + await self.refresh() + return + datas = await WikiModel.read(self.light_cone_path) + self.clear_class_data() + for data in datas: + m = LightConeModel(**data) + self.all_light_cones.append(m) + self.all_light_cones_map[m.id] = m + self.all_light_cones_name[m.name] = m + + def get_by_id(self, cid: int) -> Optional[LightConeModel]: + return self.all_light_cones_map.get(cid, None) + + def get_by_name(self, name: str) -> Optional[LightConeModel]: + return self.all_light_cones_name.get(name, None) + + def get_name_list(self) -> List[str]: + return list(self.all_light_cones_name.keys()) diff --git a/modules/wiki/material.py b/modules/wiki/material.py index ce9b193..35f4618 100644 --- a/modules/wiki/material.py +++ b/modules/wiki/material.py @@ -1,81 +1,46 @@ -import re -from typing import List, Optional, Tuple, Union +from typing import List, Dict, Optional -from bs4 import BeautifulSoup -from httpx import URL - -from modules.wiki.base import HONEY_HOST, WikiModel - -__all__ = ["Material"] - -WEEKDAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] +from modules.wiki.base import WikiModel +from modules.wiki.models.material import Material as MaterialModel class Material(WikiModel): - # noinspection PyUnresolvedReferences - """武器、角色培养素材 + material_url = WikiModel.BASE_URL + "materials.json" + material_path = WikiModel.BASE_PATH / "materials.json" - Attributes: - type: 类型 - weekdays: 每周开放的时间 - source: 获取方式 - description: 描述 - """ - type: str - source: Optional[List[str]] = None - weekdays: Optional[List[int]] = None - description: str + def __init__(self): + super().__init__() + self.all_materials: List[MaterialModel] = [] + self.all_materials_map: Dict[int, MaterialModel] = {} + self.all_materials_name: Dict[str, MaterialModel] = {} - @staticmethod - def scrape_urls() -> List[URL]: - weapon = [HONEY_HOST.join(f"fam_wep_{i}/?lang=CHS") for i in ["primary", "secondary", "common"]] - talent = [HONEY_HOST.join(f"fam_talent_{i}/?lang=CHS") for i in ["book", "boss", "common", "reward"]] - return weapon + talent + def clear_class_data(self) -> None: + self.all_materials.clear() + self.all_materials_map.clear() + self.all_materials_name.clear() - @classmethod - async def get_name_list(cls, *, with_url: bool = False) -> List[Union[str, Tuple[str, URL]]]: - return list(sorted(set(await super(Material, cls).get_name_list(with_url=with_url)), key=lambda x: x[0])) + async def refresh(self): + datas = await self.remote_get(self.material_url) + await self.dump(datas.json(), self.material_path) + await self.read() - @classmethod - async def _parse_soup(cls, soup: BeautifulSoup) -> "Material": - """解析突破素材页""" - soup = soup.select(".wp-block-post-content")[0] - tables = soup.find_all("table") - table_rows = tables[0].find_all("tr") + async def read(self): + if not self.material_path.exists(): + await self.refresh() + return + datas = await WikiModel.read(self.material_path) + self.clear_class_data() + for data in datas: + m = MaterialModel(**data) + self.all_materials.append(m) + self.all_materials_map[m.id] = m + self.all_materials_name[m.name] = m - def get_table_row(target: str): - """一个便捷函数,用于返回对应表格头的对应行的最后一个单元格中的文本""" - for row in table_rows: - if target in row.find("td").text: - return row.find_all("td")[-1] - return None + def get_by_id(self, cid: int) -> Optional[MaterialModel]: + return self.all_materials_map.get(cid, None) - def get_table_text(row_num: int) -> str: - """一个便捷函数,用于返回表格对应行的最后一个单元格中的文本""" - return table_rows[row_num].find_all("td")[-1].text.replace("\xa0", "") + def get_by_name(self, name: str) -> Optional[MaterialModel]: + return self.all_materials_name.get(name, None) - id_ = re.findall(r"/img/(.*?)\.webp", str(table_rows[0]))[0] - name = get_table_text(0) - rarity = len(table_rows[3].find_all("img")) - type_ = get_table_text(1) - if (item_source := get_table_row("Item Source")) is not None: - item_source = list( - # filter 在这里的作用是过滤掉为空的数据 - filter(lambda x: x, item_source.encode_contents().decode().split("
")) - ) - if (alter_source := get_table_row("Alternative Item")) is not None: - alter_source = list( - # filter 在这里的作用是过滤掉为空的数据 - filter(lambda x: x, alter_source.encode_contents().decode().split("
")) - ) - source = list(sorted(set((item_source or []) + (alter_source or [])))) - if (weekdays := get_table_row("Weekday")) is not None: - weekdays = [*(WEEKDAYS.index(weekdays.text.replace("\xa0", "").split(",")[0]) + 3 * i for i in range(2)), 6] - description = get_table_text(-1) - return Material( - id=id_, name=name, rarity=rarity, type=type_, description=description, source=source, weekdays=weekdays - ) - - @property - def icon(self) -> str: - return str(HONEY_HOST.join(f"/img/{self.id}.webp")) + def get_name_list(self) -> List[str]: + return list(self.all_materials_name.keys()) diff --git a/plugins/__init__.py b/modules/wiki/models/__init__.py similarity index 100% rename from plugins/__init__.py rename to modules/wiki/models/__init__.py diff --git a/modules/wiki/models/avatar.py b/modules/wiki/models/avatar.py new file mode 100644 index 0000000..51759fa --- /dev/null +++ b/modules/wiki/models/avatar.py @@ -0,0 +1,60 @@ +from typing import List +from pydantic import BaseModel +from .enums import Quality, Destiny, Element +from .material import Material + + +class AvatarInfo(BaseModel): + occupation: str = "" + """所属""" + faction: str = "" + """派系""" + + +class AvatarItem(BaseModel): + item: Material + """物品""" + count: int + """数量""" + + +class AvatarPromote(BaseModel): + required_level: int + """突破所需等级""" + promote_level: int = 0 + """突破等级""" + max_level: int + """解锁的等级上限""" + + coin: int = 0 + """信用点""" + items: list[AvatarItem] + """突破所需材料""" + + +class AvatarSoul(BaseModel): + name: str + """ 名称 """ + desc: str + """ 介绍 """ + + +class Avatar(BaseModel): + id: int + """角色ID""" + name: str + """名称""" + icon: str + """图标""" + quality: Quality + """品质""" + destiny: Destiny + """命途""" + element: Element + """属性""" + information: AvatarInfo + """角色信息""" + promote: List[AvatarPromote] + """角色突破数据""" + soul: List[AvatarSoul] + """角色星魂数据""" diff --git a/modules/wiki/models/enums.py b/modules/wiki/models/enums.py new file mode 100644 index 0000000..28ce262 --- /dev/null +++ b/modules/wiki/models/enums.py @@ -0,0 +1,84 @@ +from enum import Enum + + +class Quality(str, Enum): + """ 星级 """ + Five = "五星" + Four = "四星" + Three = "三星" + Two = "二星" + One = "一星" + + +class Destiny(str, Enum): + """ 命途 """ + HuiMie = "毁灭" + ZhiShi = "智识" + XunLie = "巡猎" + CunHu = "存护" + FengRao = "丰饶" + TongXie = "同谐" + XuWu = "虚无" + + +class Element(str, Enum): + """ 属性 """ + Physical = "物理" + Pyro = "火" + Anemo = "风" + Electro = "雷" + Cryo = "冰" + Nombre = "虚数" + Quantum = "量子" + Null = "NULL" + """无""" + + +class MonsterType(str, Enum): + """ 怪物种类 """ + Normal = "普通" + Elite = "精英" + Leader = "首领" + Boss = "历战余响" + + +class Area(str, Enum): + """ 地区 """ + Herta = "空间站「黑塔」" + YaLiLuo = "雅利洛-VI" + LuoFu = "仙舟「罗浮」" + NULL = "未知" + + +class MaterialType(str, Enum): + """ 材料类型 """ + AvatarUpdate = "角色晋阶材料" + XingJi = "行迹材料" + LightConeUpdate = "光锥晋阶材料" + Exp = "经验材料" + Grow = "养成材料" + Synthetic = "合成材料" + Task = "任务道具" + Important = "贵重物" + Consumable = "消耗品" + TaskMaterial = "任务材料" + Other = "其他材料" + + +class PropType(str, Enum): + """ 遗器套装效果 """ + HP = "基础-生命值" + Defense = "基础-防御力" + Attack = "基础-攻击力" + Critical = "基础-效果命中" + Physical = "伤害类-物理" + Pyro = "伤害类-火" + Anemo = "伤害类-风" + Electro = "伤害类-雷" + Cryo = "伤害类-冰" + Nombre = "伤害类-虚数" + Quantum = "伤害类-量子" + Add = "伤害类-追加伤害" + Heal = "其他-治疗加成" + OtherCritical = "其他-效果命中" + Charge = "其他-能量充能效率" diff --git a/modules/wiki/models/light_cone.py b/modules/wiki/models/light_cone.py new file mode 100644 index 0000000..0cb5a60 --- /dev/null +++ b/modules/wiki/models/light_cone.py @@ -0,0 +1,45 @@ +# 光锥 +from pydantic import BaseModel + +from .enums import Quality, Destiny +from .material import Material + + +class LightConeItem(BaseModel): + item: Material + """物品""" + count: int + """数量""" + + +class LightConePromote(BaseModel): + required_level: int + """突破所需等级""" + promote_level: int = 0 + """突破等级""" + max_level: int + """解锁的等级上限""" + + coin: int = 0 + """信用点""" + items: list[LightConeItem] + """突破所需材料""" + + +class LightCone(BaseModel): + id: int + """"光锥ID""" + name: str + """名称""" + desc: str + """描述""" + icon: str + """图标""" + big_pic: str + """大图""" + quality: Quality + """稀有度""" + destiny: Destiny + """命途""" + promote: list[LightConePromote] + """晋阶信息""" diff --git a/modules/wiki/models/material.py b/modules/wiki/models/material.py new file mode 100644 index 0000000..88ab22f --- /dev/null +++ b/modules/wiki/models/material.py @@ -0,0 +1,19 @@ +# 材料 +from pydantic import BaseModel + +from .enums import Quality, MaterialType + + +class Material(BaseModel): + id: int + """材料ID""" + name: str + """名称""" + desc: str + """介绍""" + icon: str + """图标""" + quality: Quality + """稀有度""" + type: MaterialType + """类型""" diff --git a/modules/wiki/models/monster.py b/modules/wiki/models/monster.py new file mode 100644 index 0000000..ccc20d6 --- /dev/null +++ b/modules/wiki/models/monster.py @@ -0,0 +1,26 @@ +# 敌对物种 +from pydantic import BaseModel + +from .enums import MonsterType, Area + + +class Monster(BaseModel): + id: int + """怪物ID""" + name: str + """名称""" + desc: str + """介绍""" + icon: str + """图标""" + big_pic: str + """大图""" + type: MonsterType + """种类""" + area: Area + """地区""" + resistance: str + """抗性""" + find_area: str + """发现地点""" + diff --git a/modules/wiki/models/relic.py b/modules/wiki/models/relic.py new file mode 100644 index 0000000..7272640 --- /dev/null +++ b/modules/wiki/models/relic.py @@ -0,0 +1,13 @@ +# 遗器套装 +from pydantic import BaseModel + + +class Relic(BaseModel): + id: int + """遗器套装ID""" + name: str + """套装名称""" + icon: str + """套装图标""" + affect: str + """套装效果""" diff --git a/modules/wiki/monster.py b/modules/wiki/monster.py new file mode 100644 index 0000000..98741e3 --- /dev/null +++ b/modules/wiki/monster.py @@ -0,0 +1,46 @@ +from typing import List, Dict, Optional + +from modules.wiki.base import WikiModel +from modules.wiki.models.monster import Monster as MonsterModel + + +class Monster(WikiModel): + monster_url = WikiModel.BASE_URL + "monsters.json" + monster_path = WikiModel.BASE_PATH / "monsters.json" + + def __init__(self): + super().__init__() + self.all_monsters: List[MonsterModel] = [] + self.all_monsters_map: Dict[int, MonsterModel] = {} + self.all_monsters_name: Dict[str, MonsterModel] = {} + + def clear_class_data(self) -> None: + self.all_monsters.clear() + self.all_monsters_map.clear() + self.all_monsters_name.clear() + + async def refresh(self): + datas = await self.remote_get(self.monster_url) + await self.dump(datas.json(), self.monster_path) + await self.read() + + async def read(self): + if not self.monster_path.exists(): + await self.refresh() + return + datas = await WikiModel.read(self.monster_path) + self.clear_class_data() + for data in datas: + m = MonsterModel(**data) + self.all_monsters.append(m) + self.all_monsters_map[m.id] = m + self.all_monsters_name[m.name] = m + + def get_by_id(self, cid: int) -> Optional[MonsterModel]: + return self.all_monsters_map.get(cid, None) + + def get_by_name(self, name: str) -> Optional[MonsterModel]: + return self.all_monsters_name.get(name, None) + + def get_name_list(self) -> List[str]: + return list(self.all_monsters_name.keys()) diff --git a/modules/wiki/raider.py b/modules/wiki/raider.py new file mode 100644 index 0000000..d5c8895 --- /dev/null +++ b/modules/wiki/raider.py @@ -0,0 +1,40 @@ +from typing import List +from modules.wiki.base import WikiModel + + +class Raider(WikiModel): + raider_url = WikiModel.BASE_URL + "raiders/" + raider_path = WikiModel.BASE_PATH / "raiders" + raider_info_path = WikiModel.BASE_PATH / "raiders" / "info.json" + raider_path.mkdir(parents=True, exist_ok=True) + + def __init__(self): + super().__init__() + self.all_raiders: List[str] = [] + + def clear_class_data(self) -> None: + self.all_raiders.clear() + + async def refresh(self): + datas = await self.remote_get(self.raider_url + "info.json") + data = datas.json() + for name in data: + photo = await self.remote_get(f"{self.raider_url}{name}.png") + await self.save_file(photo.content, self.raider_path / f"{name}.png") + self.all_raiders.append(name) + await self.dump(data, self.raider_info_path) + + async def read(self): + if not self.raider_info_path.exists(): + await self.refresh() + return + datas = await WikiModel.read(self.raider_info_path) + self.clear_class_data() + for data in datas: + self.all_raiders.append(data) + + def get_name_list(self) -> List[str]: + return self.all_raiders.copy() + + def get_item_id(self, name: str) -> int: + return self.all_raiders.index(name) diff --git a/modules/wiki/relic.py b/modules/wiki/relic.py new file mode 100644 index 0000000..674e263 --- /dev/null +++ b/modules/wiki/relic.py @@ -0,0 +1,46 @@ +from typing import List, Dict, Optional + +from modules.wiki.base import WikiModel +from modules.wiki.models.relic import Relic as RelicModel + + +class Relic(WikiModel): + relic_url = WikiModel.BASE_URL + "relics.json" + relic_path = WikiModel.BASE_PATH / "relics.json" + + def __init__(self): + super().__init__() + self.all_relics: List[RelicModel] = [] + self.all_relics_map: Dict[int, RelicModel] = {} + self.all_relics_name: Dict[str, RelicModel] = {} + + def clear_class_data(self) -> None: + self.all_relics.clear() + self.all_relics_map.clear() + self.all_relics_name.clear() + + async def refresh(self): + datas = await self.remote_get(self.relic_url) + await self.dump(datas.json(), self.relic_path) + await self.read() + + async def read(self): + if not self.relic_path.exists(): + await self.refresh() + return + datas = await WikiModel.read(self.relic_path) + self.clear_class_data() + for data in datas: + m = RelicModel(**data) + self.all_relics.append(m) + self.all_relics_map[m.id] = m + self.all_relics_name[m.name] = m + + def get_by_id(self, cid: int) -> Optional[RelicModel]: + return self.all_relics_map.get(cid, None) + + def get_by_name(self, name: str) -> Optional[RelicModel]: + return self.all_relics_name.get(name, None) + + def get_name_list(self) -> List[str]: + return list(self.all_relics_name.keys()) diff --git a/plugins/README.md b/plugins/README.md deleted file mode 100644 index 2b96087..0000000 --- a/plugins/README.md +++ /dev/null @@ -1,163 +0,0 @@ -# plugins 目录 - -## 说明 - -该目录仅限处理交互层和业务层数据交换的任务 - -如有任何核心接口,请转到 `core` 目录添加 - -如有任何API请求接口,请转到 `models` 目录添加 - -## 新版插件 Plugin 的写法 - -### 关于路径 - -插件应该写在 `plugins` 文件夹下,可以是一个包或者是一个文件,但文件名、文件夹名中不能包含`_`字符 - -### 关于类 - -1. 除了要使用`ConversationHandler` 的插件外,都要继承 `core.plugin.Plugin` - - ```python - from core.plugin import Plugin - - - class TestPlugin(Plugin): - pass - ``` - -2. 针对要用 `ConversationHandler` 的插件,要继承 `core.plugin.Plugin.Conversation` - - ```python - from core.plugin import Plugin - - - class TestConversationPlugin(Plugin.Conversation): - pass - ``` - -3. 关于初始化方法以及依赖注入 - - 初始化类, 可写在 `__init__` 和 `__async_init__` 中, 其中 `__async_init__` 应该是异步方法, - 用于执行初始化时需要的异步操作. 这两个方法的执行顺序是 `__init__` 在前, `__async_init__` 在后 - - 若需要注入依赖, 直接在插件类的`__init__`方法中,提供相应的参数以及标注标注即可, 例如我需要注入一个 `MySQL` - - ```python - from service.mysql import MySQL - from core.plugin import Plugin - - class TestPlugin(Plugin): - def __init__(self, mysql: MySQL): - self.mysql = mysql - - async def __async_init__(self): - """do something""" - - ``` - -## 关于 `handler` - -给函数加上 `core.plugin.handler` 这一装饰器即可将这个函数注册为`handler` - -### 非 `ConversationHandler` 的 `handler` - -1. 直接使用 `core.plugin.handler` 装饰器 - - 第一个参数是 `handler` 的种类,后续参数为该 `handler` 除 `callback` 参数外的其余参数 - - ```python - from core.plugin import Plugin, handler - from telegram import Update - from telegram.ext import CommandHandler, CallbackContext - - - class TestPlugin(Plugin): - @handler(CommandHandler, command='start', block=False) - async def start(self, update: Update, context: CallbackContext): - await update.effective_chat.send_message('hello world!') - ``` - - 比如上面代码中的 `command='start', block=False` 就是 `CommandHandler` 的参数 - -2. 使用 `core.plugin.handler` 的子装饰器 - - 这种方式比第一种简单, 不需要声明 `handler` 的类型 - - ```python - from core.plugin import Plugin, handler - from telegram import Update - from telegram.ext import CallbackContext - - - class TestPlugin(Plugin): - @handler.command(command='start', block=False) - async def start(self, update: Update, context: CallbackContext): - await update.effective_chat.send_message('hello world!') - ``` - -### 对于 `ConversationHandler` - -由于 `ConversationHandler` 比较特殊,所以**一个 Plugin 类中只能存在一个 `ConversationHandler`** - -`conversation.entry_point` 、`conversation.state` 和 `conversation.fallback` 装饰器分别对应 -`ConversationHandler` 的 `entry_points`、`stats` 和 `fallbacks` 参数 - -```python -from telegram import Update -from telegram.ext import CallbackContext, filters - -from core.plugin import Plugin, conversation, handler - -STATE_A, STATE_B, STATE_C = range(3) - - -class TestConversation(Plugin.Conversation, allow_reentry=True, block=False): - - @conversation.entry_point # 标注这个handler是ConversationHandler的一个entry_point - @handler.command(command='entry') - async def entry_point(self, update: Update, context: CallbackContext): - """do something""" - - @conversation.state(state=STATE_A) - @handler.message(filters=filters.TEXT) - async def state(self, update: Update, context: CallbackContext): - """do something""" - - @conversation.fallback - @handler.message(filters=filters.TEXT) - async def fallback(self, update: Update, context: CallbackContext): - """do something""" - - @handler.inline_query() # 你可以在此 Plugin 下定义其它类型的 handler - async def inline_query(self, update: Update, context: CallbackContext): - """do something""" - -``` - -### 对于 `Job` - -1. 依然需要继承 `core.plugin.Plugin` -2. 直接使用 `core.plugin.job` 装饰器 参数都与官方 `JobQueue` 类对应 - -```python -from core.plugin import Plugin, job - -class TestJob(Plugin): - - @job.run_repeating(interval=datetime.timedelta(hours=2), name="TestJob") - async def refresh(self, _: CallbackContext): - logger.info("TestJob") -``` - -### 注意 - -被注册到 `handler` 的函数需要添加 `error_callable` 修饰器作为错误统一处理 - -被注册到 `handler` 的函数必须使用 `@restricts()` 修饰器 **预防洪水攻击** 但 `ConversationHandler` 外只需要注册入口函数使用 - -如果引用服务,参数需要声明需要引用服务的类型并设置默认传入为 `None` - -必要的函数必须捕获异常后通知用户或者直接抛出异常 - -**部分修饰器为带参修饰器,必须带括号,否则会出现调用错误** \ No newline at end of file diff --git a/plugins/account/account.py b/plugins/account/account.py index 5b4062d..35a6979 100644 --- a/plugins/account/account.py +++ b/plugins/account/account.py @@ -16,7 +16,6 @@ from core.services.players.models import PlayersDataBase as Player, PlayerInfoSQ from core.services.players.services import PlayersService, PlayerInfoService from utils.log import logger - if TYPE_CHECKING: from telegram import Update from telegram.ext import ContextTypes @@ -86,11 +85,13 @@ class BindAccountPlugin(Plugin.Conversation): if message.text == "米游社": bind_account_plugin_data.region = RegionEnum.HYPERION elif message.text == "HoYoLab": + await message.reply_text("很抱歉,暂不支持HoYoLab服务器", reply_markup=ReplyKeyboardRemove()) + return ConversationHandler.END bind_account_plugin_data.region = RegionEnum.HOYOLAB else: await message.reply_text("选择错误,请重新选择") return CHECK_SERVER - reply_keyboard = [["通过玩家ID", "用过账号ID"], ["退出"]] + reply_keyboard = [["通过玩家ID", "通过账号ID"], ["退出"]] await message.reply_markdown_v2( "请选择你要绑定的方式", reply_markup=ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True) ) @@ -133,15 +134,20 @@ class BindAccountPlugin(Plugin.Conversation): await message.reply_text("用户查询次数过多,请稍后重试", reply_markup=ReplyKeyboardRemove()) return ConversationHandler.END if region == RegionEnum.HYPERION: - client = genshin.Client(cookies=cookies.data, game=types.Game.GENSHIN, region=types.Region.CHINESE) + client = genshin.Client(cookies=cookies.data, game=types.Game.STARRAIL, region=types.Region.CHINESE) elif region == RegionEnum.HOYOLAB: client = genshin.Client( - cookies=cookies.data, game=types.Game.GENSHIN, region=types.Region.OVERSEAS, lang="zh-cn" + cookies=cookies.data, game=types.Game.STARRAIL, region=types.Region.OVERSEAS, lang="zh-cn" ) else: return ConversationHandler.END try: - record_card = await client.get_record_card(account_id) + record_cards = await client.get_record_card(account_id) + record_card = record_cards[0] + for card in record_cards: + if card.game == types.Game.STARRAIL: + record_card = card + break except DataNotPublic: await message.reply_text("角色未公开", reply_markup=ReplyKeyboardRemove()) logger.warning("获取账号信息发生错误 %s 账户信息未公开", account_id) @@ -151,8 +157,8 @@ class BindAccountPlugin(Plugin.Conversation): logger.error("获取账号信息发生错误") logger.exception(exc) return ConversationHandler.END - if record_card.game != types.Game.GENSHIN: - await message.reply_text("角色信息查询返回非原神游戏信息,请设置展示主界面为原神", reply_markup=ReplyKeyboardRemove()) + if record_card.game != types.Game.STARRAIL: + await message.reply_text("角色信息查询返回无星穹铁道游戏信息,请确定你有星穹铁道账号", reply_markup=ReplyKeyboardRemove()) return ConversationHandler.END player_info = await self.players_service.get( user.id, player_id=record_card.uid, region=bind_account_plugin_data.region @@ -197,10 +203,10 @@ class BindAccountPlugin(Plugin.Conversation): await message.reply_text("用户查询次数过多,请稍后重试", reply_markup=ReplyKeyboardRemove()) return ConversationHandler.END if region == RegionEnum.HYPERION: - client = genshin.Client(cookies=cookies.data, game=types.Game.GENSHIN, region=types.Region.CHINESE) + client = genshin.Client(cookies=cookies.data, game=types.Game.STARRAIL, region=types.Region.CHINESE) elif region == RegionEnum.HOYOLAB: client = genshin.Client( - cookies=cookies.data, game=types.Game.GENSHIN, region=types.Region.OVERSEAS, lang="zh-cn" + cookies=cookies.data, game=types.Game.STARRAIL, region=types.Region.OVERSEAS, lang="zh-cn" ) else: return ConversationHandler.END @@ -273,16 +279,14 @@ class BindAccountPlugin(Plugin.Conversation): is_chosen=is_chosen, # todo 多账号 ) await self.players_service.add(player) - player_info = await self.player_info_service.get(player) - if player_info is None: - player_info = PlayerInfoSQLModel( - user_id=player.user_id, - player_id=player.player_id, - nickname=nickname, - create_time=datetime.now(), - is_update=True, - ) # 不添加更新时间 - await self.player_info_service.add(player_info) + player_info = PlayerInfoSQLModel( + user_id=player.user_id, + player_id=player.player_id, + nickname=nickname, + create_time=datetime.now(), + is_update=True, + ) # 不添加更新时间 + await self.player_info_service.add(player_info) logger.success("用户 %s[%s] 绑定UID账号成功", user.full_name, user.id) await message.reply_text("保存成功", reply_markup=ReplyKeyboardRemove()) return ConversationHandler.END diff --git a/plugins/account/cookies.py b/plugins/account/cookies.py index 95b1600..7d5b2ee 100644 --- a/plugins/account/cookies.py +++ b/plugins/account/cookies.py @@ -129,6 +129,8 @@ class AccountCookiesPlugin(Plugin.Conversation): region = RegionEnum.HYPERION bbs_name = "米游社" elif message.text == "HoYoLab": + await message.reply_text("很抱歉,暂不支持HoYoLab服务器", reply_markup=ReplyKeyboardRemove()) + return ConversationHandler.END bbs_name = "HoYoLab" region = RegionEnum.HOYOLAB else: @@ -236,7 +238,8 @@ class AccountCookiesPlugin(Plugin.Conversation): logger.warning("用户 %s[%s] region[%s] 也许是不正确的", user.full_name, user.id, client.region.name) else: account_cookies_plugin_data.account_id = client.cookie_manager.user_id - genshin_accounts = await client.genshin_accounts() + accounts = await client.get_game_accounts() + starrail_accounts = [account for account in accounts if account.game == types.Game.STARRAIL] except DataNotPublic: logger.info("用户 %s[%s] 账号疑似被注销", user.full_name, user.id) await message.reply_text("账号疑似被注销,请检查账号状态", reply_markup=ReplyKeyboardRemove()) @@ -283,19 +286,19 @@ class AccountCookiesPlugin(Plugin.Conversation): if account_cookies_plugin_data.account_id is None: await message.reply_text("无法获取账号ID,请检查Cookie是否正确或请稍后重试") return ConversationHandler.END - genshin_account: Optional[GenshinAccount] = None + starrail_account: Optional[GenshinAccount] = None level: int = 0 # todo : 多账号绑定 - for temp in genshin_accounts: + for temp in starrail_accounts: if temp.level >= level: # 获取账号等级最高的 level = temp.level - genshin_account = temp - if genshin_account is None: - await message.reply_text("未找到原神账号,请确认账号信息无误。") + starrail_account = temp + if starrail_account is None: + await message.reply_text("未找到星穹铁道账号,请确认账号信息无误。") return ConversationHandler.END - account_cookies_plugin_data.genshin_account = genshin_account + account_cookies_plugin_data.genshin_account = starrail_account player_info = await self.players_service.get( - user.id, player_id=genshin_account.uid, region=account_cookies_plugin_data.region + user.id, player_id=starrail_account.uid, region=account_cookies_plugin_data.region ) account_cookies_plugin_data.player = player_info if player_info: @@ -308,14 +311,14 @@ class AccountCookiesPlugin(Plugin.Conversation): reply_keyboard = [["确认", "退出"]] await message.reply_text("获取角色基础信息成功,请检查是否正确!") logger.info( - "用户 %s[%s] 获取账号 %s[%s] 信息成功", user.full_name, user.id, genshin_account.nickname, genshin_account.uid + "用户 %s[%s] 获取账号 %s[%s] 信息成功", user.full_name, user.id, starrail_account.nickname, starrail_account.uid ) text = ( f"*角色信息*\n" - f"角色名称:{escape_markdown(genshin_account.nickname, version=2)}\n" - f"角色等级:{genshin_account.level}\n" - f"UID:`{genshin_account.uid}`\n" - f"服务器名称:`{genshin_account.server_name}`\n" + f"角色名称:{escape_markdown(starrail_account.nickname, version=2)}\n" + f"角色等级:{starrail_account.level}\n" + f"UID:`{starrail_account.uid}`\n" + f"服务器名称:`{starrail_account.server_name}`\n" ) await message.reply_markdown_v2(text, reply_markup=ReplyKeyboardMarkup(reply_keyboard, one_time_keyboard=True)) account_cookies_plugin_data.cookies = cookies.to_dict() @@ -358,16 +361,14 @@ class AccountCookiesPlugin(Plugin.Conversation): region=account_cookies_plugin_data.region, is_chosen=True, # todo 多账号 ) - player_info = await self.player_info_service.get(player) - if player_info is None: - player_info = PlayerInfoSQLModel( - user_id=player.user_id, - player_id=player.player_id, - nickname=genshin_account.nickname, - create_time=datetime.now(), - is_update=True, - ) # 不添加更新时间 - await self.player_info_service.add(player_info) + player_info = PlayerInfoSQLModel( + user_id=player.user_id, + player_id=player.player_id, + nickname=genshin_account.nickname, + create_time=datetime.now(), + is_update=True, + ) # 不添加更新时间 + await self.player_info_service.add(player_info) await self.players_service.add(player) cookies = Cookies( user_id=user.id, diff --git a/plugins/admin/get_chat.py b/plugins/admin/get_chat.py index 6eb4dc5..39ca50e 100644 --- a/plugins/admin/get_chat.py +++ b/plugins/admin/get_chat.py @@ -59,7 +59,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 f0aa4f1..14949fd 100644 --- a/plugins/admin/post.py +++ b/plugins/admin/post.py @@ -52,8 +52,8 @@ class Post(Plugin.Conversation): MENU_KEYBOARD = ReplyKeyboardMarkup([["推送频道", "添加TAG"], ["编辑文字", "删除图片"], ["退出"]], True, True) def __init__(self): - self.gids = 2 - self.short_name = "ys" + self.gids = 6 + self.short_name = "sr" self.bbs = Hyperion( timeout=Timeout( connect=config.connect_timeout, @@ -300,10 +300,12 @@ class Post(Plugin.Conversation): post_subject = post_data["subject"] post_soup = BeautifulSoup(post_data["content"], features="html.parser") post_text = self.parse_post_text(post_soup, post_subject) - post_text += f"[source](https://www.miyoushe.com/{self.short_name}/article/{post_id})" + post_text += f"\n[source](https://www.miyoushe.com/{self.short_name}/article/{post_id})" if 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 e816fa7..0000000 --- a/plugins/admin/quiz.py +++ /dev/null @@ -1,222 +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 a1194a4..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) - 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/app/inline.py b/plugins/app/inline.py index a1449e3..6512f5e 100644 --- a/plugins/app/inline.py +++ b/plugins/app/inline.py @@ -6,6 +6,7 @@ from telegram import ( InlineQuery, InlineQueryResultArticle, InlineQueryResultCachedPhoto, + InlineQueryResultCachedDocument, InputTextMessageContent, Update, ) @@ -13,7 +14,6 @@ from telegram.constants import ParseMode from telegram.error import BadRequest from telegram.ext import CallbackContext, InlineQueryHandler -from core.dependence.assets import AssetsCouldNotFound, AssetsService from core.plugin import Plugin, handler from core.services.search.services import SearchServices from core.services.wiki.services import WikiService @@ -26,10 +26,8 @@ class Inline(Plugin): def __init__( self, wiki_service: WikiService, - assets_service: AssetsService, search_service: SearchServices, ): - self.assets_service = assets_service self.wiki_service = wiki_service self.weapons_list: List[Dict[str, str]] = [] self.characters_list: List[Dict[str, str]] = [] @@ -37,38 +35,24 @@ class Inline(Plugin): self.search_service = search_service 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: + datas: Dict[str, str] = {} + for character in self.wiki_service.character.all_avatars: + if not character.icon: + logger.warning(f"角色 {character.name} 无图标") continue - except Exception as exc: - logger.error("获取角色信息失败 %s", str(exc)) - continue - data = {"name": character_name, "icon": icon} - self.characters_list.append(data) + datas[character.name] = character.icon + for character in self.wiki_service.raider.get_name_list(): + if character in datas: + self.characters_list.append({"name": character, "icon": datas[character]}) + else: + for key, value in datas.items(): + if character.startswith(key): + self.characters_list.append({"name": character, "icon": value}) + break logger.success("Inline 模块获取角色列表成功") - self.refresh_task.append(asyncio.create_task(task_weapons())) self.refresh_task.append(asyncio.create_task(task_characters())) @handler(InlineQueryHandler, block=False) @@ -81,14 +65,6 @@ class Inline(Plugin): results_list = [] args = query.split(" ") if args[0] == "": - results_list.append( - InlineQueryResultArticle( - id=str(uuid4()), - title="武器图鉴查询", - description="输入武器名称即可查询武器图鉴", - input_message_content=InputTextMessageContent("武器图鉴查询"), - ) - ) results_list.append( InlineQueryResultArticle( id=str(uuid4()), @@ -98,22 +74,7 @@ class Inline(Plugin): ) ) else: - if args[0] == "查看武器列表并查询": - for weapon in self.weapons_list: - name = weapon["name"] - icon = weapon["icon"] - results_list.append( - InlineQueryResultArticle( - id=str(uuid4()), - title=name, - description=f"查看武器列表并查询 {name}", - thumb_url=icon, - input_message_content=InputTextMessageContent( - f"武器查询{name}", parse_mode=ParseMode.MARKDOWN_V2 - ), - ) - ) - elif args[0] == "查看角色攻略列表并查询": + if args[0] == "查看角色攻略列表并查询": for character in self.characters_list: name = character["name"] icon = character["icon"] @@ -128,19 +89,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: @@ -149,33 +97,42 @@ class Inline(Plugin): id=str(uuid4()), title=f"当前查询内容为 {args[0]}", description="如果无查看图片描述 这是正常的 客户端问题", - thumb_url="https://www.miyoushe.com/_nuxt/img/game-ys.dfc535b.jpg", + thumb_url="https://www.miyoushe.com/_nuxt/img/game-sr.4f80911.jpg", input_message_content=InputTextMessageContent(f"当前查询内容为 {args[0]}\n如果无查看图片描述 这是正常的 客户端问题"), ) ) for simple_search_result in simple_search_results: + description = simple_search_result.description + if len(description) >= 10: + description = description[:10] + item = None if simple_search_result.photo_file_id: - description = simple_search_result.description - if len(description) >= 10: - description = description[:10] - results_list.append( - InlineQueryResultCachedPhoto( - id=str(uuid4()), - title=simple_search_result.title, - photo_file_id=simple_search_result.photo_file_id, - description=description, - caption=simple_search_result.caption, - parse_mode=simple_search_result.parse_mode, - ) + item = InlineQueryResultCachedPhoto( + id=str(uuid4()), + title=simple_search_result.title, + photo_file_id=simple_search_result.photo_file_id, + description=description, + caption=simple_search_result.caption, + parse_mode=simple_search_result.parse_mode, ) - + elif simple_search_result.document_file_id: + item = InlineQueryResultCachedDocument( + id=str(uuid4()), + title=simple_search_result.title, + document_file_id=simple_search_result.document_file_id, + description=description, + caption=simple_search_result.caption, + parse_mode=simple_search_result.parse_mode, + ) + if item: + results_list.append(item) if not results_list: results_list.append( InlineQueryResultArticle( id=str(uuid4()), title="好像找不到问题呢", - description="这个问题我也不知道,因为我就是个应急食品。", - input_message_content=InputTextMessageContent("这个问题我也不知道,因为我就是个应急食品。"), + description="这个问题我也不知道。", + input_message_content=InputTextMessageContent("这个问题我也不知道。"), ) ) try: diff --git a/plugins/app/start.py b/plugins/app/start.py index 351b80a..7e3d7e0 100644 --- a/plugins/app/start.py +++ b/plugins/app/start.py @@ -27,17 +27,17 @@ class StartPlugin(Plugin): if args is not None and len(args) >= 1: if args[0] == "inline_message": await message.reply_markdown_v2( - f"你好 {user.mention_markdown_v2()} {escape_markdown('!我是派蒙 !')}\n" + f"你好 {user.mention_markdown_v2()} {escape_markdown('!我是彦卿 !')}\n" f"{escape_markdown('发送 /help 命令即可查看命令帮助')}" ) elif args[0] == "set_cookie": await message.reply_markdown_v2( - f"你好 {user.mention_markdown_v2()} {escape_markdown('!我是派蒙 !')}\n" + f"你好 {user.mention_markdown_v2()} {escape_markdown('!我是彦卿 !')}\n" f"{escape_markdown('发送 /setcookie 命令进入绑定账号流程')}" ) elif args[0] == "set_uid": await message.reply_markdown_v2( - f"你好 {user.mention_markdown_v2()} {escape_markdown('!我是派蒙 !')}\n" + f"你好 {user.mention_markdown_v2()} {escape_markdown('!我是彦卿 !')}\n" f"{escape_markdown('发送 /setuid 或 /setcookie 命令进入绑定账号流程')}" ) elif args[0] == "verify_verification": @@ -54,19 +54,15 @@ class StartPlugin(Plugin): logger.info("用户 %s[%s] 通过start命令 进入签到流程", user.full_name, user.id) await self.process_sign_validate(message, user, _challenge) else: - await message.reply_html(f"你好 {user.mention_html()} !我是派蒙 !\n请点击 /{args[0]} 命令进入对应流程") + await message.reply_html(f"你好 {user.mention_html()} !我是彦卿 !\n请点击 /{args[0]} 命令进入对应流程") return logger.info("用户 %s[%s] 发出start命令", user.full_name, user.id) - await message.reply_markdown_v2(f"你好 {user.mention_markdown_v2()} {escape_markdown('!我是派蒙 !')}") + await message.reply_markdown_v2(f"你好 {user.mention_markdown_v2()} {escape_markdown('!我是彦卿 !')}") @staticmethod async def unknown_command(update: Update, _: CallbackContext) -> None: await update.effective_message.reply_text("前面的区域,以后再来探索吧!") - @staticmethod - async def emergency_food(update: Update, _: CallbackContext) -> None: - await update.effective_message.reply_text("派蒙才不是应急食品!") - @handler(CommandHandler, command="ping", block=False) async def ping(self, update: Update, _: CallbackContext) -> None: await update.effective_message.reply_text("online! ヾ(✿゚▽゚)ノ") diff --git a/plugins/app/title.py b/plugins/app/title.py new file mode 100644 index 0000000..363c0dd --- /dev/null +++ b/plugins/app/title.py @@ -0,0 +1,42 @@ +import contextlib + +from telegram import Update, ChatMemberAdministrator +from telegram.ext import CallbackContext, filters + +from core.plugin import Plugin, handler +from utils.log import logger + + +class TitlePlugin(Plugin): + @handler.command("title", filters=filters.ChatType.SUPERGROUP, block=False) + async def start(self, update: Update, context: CallbackContext) -> None: + user = update.effective_user + message = update.effective_message + args = self.get_args(context) + title = args[0].strip() if args else "" + logger.info("用户 %s[%s] 发出 title 命令", user.full_name, user.id) + is_admin, can_edit = False, False + with contextlib.suppress(Exception): + member = await context.bot.get_chat_member(message.chat.id, user.id) + if isinstance(member, ChatMemberAdministrator): + can_edit = member.can_be_edited + if not can_edit: + reply = await message.reply_text("你没有权限使用此命令。") + self.add_delete_message_job(message) + self.add_delete_message_job(reply) + return + if not title: + reply = await message.reply_text("参数不能为空。") + self.add_delete_message_job(message) + self.add_delete_message_job(reply) + return + try: + await context.bot.set_chat_administrator_custom_title(message.chat.id, user.id, title) + except Exception: + reply = await message.reply_text("设置失败,可能是参数不合法。") + self.add_delete_message_job(message) + self.add_delete_message_job(reply) + return + reply = await message.reply_text("设置成功。") + self.add_delete_message_job(message) + self.add_delete_message_job(reply) diff --git a/plugins/genshin/__init__.py b/plugins/genshin/README.md similarity index 100% rename from plugins/genshin/__init__.py rename to plugins/genshin/README.md diff --git a/plugins/genshin/abyss.py b/plugins/genshin/abyss.py deleted file mode 100644 index cc79307..0000000 --- a/plugins/genshin/abyss.py +++ /dev/null @@ -1,347 +0,0 @@ -"""深渊数据查询""" -import asyncio -import re -from datetime import datetime -from functools import lru_cache, partial -from typing import Any, Coroutine, List, Match, Optional, Tuple, Union - -from arkowrapper import ArkoWrapper -from genshin import Client, GenshinException -from pytz import timezone -from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Message, Update -from telegram.constants import ChatAction, ParseMode -from telegram.ext import CallbackContext, filters -from telegram.helpers import create_deep_linked_url - -from core.dependence.assets import AssetsService -from core.plugin import Plugin, handler -from core.services.cookies.error import TooManyRequestPublicCookies -from core.services.template.models import RenderGroupResult, RenderResult -from core.services.template.services import TemplateService -from metadata.genshin import game_id_to_role_id -from plugins.tools.genshin import GenshinHelper, CookiesNotFoundError, PlayerNotFoundError -from utils.helpers import async_re_sub -from utils.log import logger - -try: - import ujson as jsonlib - -except ImportError: - import json as jsonlib - - -TZ = timezone("Asia/Shanghai") -cmd_pattern = r"(?i)^/abyss\s*((?:\d+)|(?:all))?\s*(pre)?" -msg_pattern = r"^深渊数据((?:查询)|(?:总览))(上期)?\D?(\d*)?.*?$" - -regex_01 = r"['\"]icon['\"]:\s*['\"](.*?)['\"]" -regex_02 = r"['\"]side_icon['\"]:\s*['\"](.*?)['\"]" - - -async def replace_01(match: Match, assets_service: AssetsService) -> str: - aid = game_id_to_role_id(re.findall(r"UI_AvatarIcon_(.*?).png", match.group(1))[0]) - return (await assets_service.avatar(aid).icon()).as_uri() - - -async def replace_02(match: Match, assets_service: AssetsService) -> str: - aid = game_id_to_role_id(re.findall(r"UI_AvatarIcon_Side_(.*?).png", match.group(1))[0]) - return (await assets_service.avatar(aid).side()).as_uri() - - -@lru_cache -def get_args(text: str) -> Tuple[int, bool, bool]: - if text.startswith("/"): - result = re.match(cmd_pattern, text).groups() - try: - floor = int(result[0] or 0) - except ValueError: - floor = 0 - return floor, result[0] == "all", bool(result[1]) - result = re.match(msg_pattern, text).groups() - return int(result[2] or 0), result[0] == "总览", result[1] == "上期" - - -class AbyssUnlocked(Exception): - """根本没动""" - - -class NoMostKills(Exception): - """挑战了但是数据没刷新""" - - -class AbyssNotFoundError(Exception): - """如果查询别人,是无法找到队伍详细,只有数据统计""" - - -class AbyssPlugin(Plugin): - """深渊数据查询""" - - def __init__( - self, - template: TemplateService, - helper: GenshinHelper, - assets_service: AssetsService, - ): - self.template_service = template - self.helper = helper - self.assets_service = assets_service - - @handler.command("abyss", block=False) - @handler.message(filters.Regex(msg_pattern), block=False) - async def command_start(self, update: Update, context: CallbackContext) -> None: - user = update.effective_user - message = update.effective_message - uid: Optional[int] = None - - # 若查询帮助 - 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, - ) - logger.info("用户 %s[%s] 查询[bold]深渊挑战数据[/bold]帮助", user.full_name, user.id, extra={"markup": True}) - return - - # 解析参数 - floor, total, previous = get_args(message.text) - - 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 - - logger.info( - "用户 %s[%s] [bold]深渊挑战数据[/bold]请求: floor=%s total=%s previous=%s", - user.full_name, - user.id, - floor, - total, - previous, - extra={"markup": True}, - ) - - try: - try: - client = await self.helper.get_genshin_client(user.id) - uid = client.uid - except CookiesNotFoundError: - client, uid = await self.helper.get_public_genshin_client(user.id) - except PlayerNotFoundError: # 若未找到账号 - buttons = [[InlineKeyboardButton("点我绑定账号", url=create_deep_linked_url(context.bot.username, "set_uid"))]] - if filters.ChatType.GROUPS.filter(message): - reply_message = await message.reply_text( - "未查询到您所绑定的账号信息,请先私聊派蒙绑定账号", reply_markup=InlineKeyboardMarkup(buttons) - ) - self.add_delete_message_job(reply_message) - self.add_delete_message_job(message) - else: - await message.reply_text("未查询到您所绑定的账号信息,请先绑定账号", reply_markup=InlineKeyboardMarkup(buttons)) - 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 - - async def reply_message_func(content: str) -> None: - _user = await client.get_genshin_user(uid) - _reply_msg = await message.reply_text( - f"旅行者 {_user.info.nickname}({uid}) {content}", parse_mode=ParseMode.HTML - ) - - reply_text: Optional[Message] = None - - if total: - reply_text = await message.reply_text("派蒙需要时间整理深渊数据,还请耐心等待哦~") - - await message.reply_chat_action(ChatAction.TYPING) - - try: - images = await self.get_rendered_pic(client, uid, floor, total, previous) - except GenshinException as exc: - if exc.retcode == 1034 and client.uid != uid: - await message.reply_text("出错了呜呜呜 ~ 请稍后重试") - return - raise exc - except AbyssUnlocked: # 若深渊未解锁 - await reply_message_func("还未解锁深渊哦~") - return - except NoMostKills: # 若深渊还未挑战 - await reply_message_func("还没有挑战本次深渊呢,咕咕咕~") - return - except AbyssNotFoundError: - await reply_message_func("无法查询玩家挑战队伍详情,只能查询统计详情哦~") - return - except IndexError: # 若深渊为挑战此层 - await reply_message_func("还没有挑战本层呢,咕咕咕~") - return - if images is None: - await reply_message_func(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, allow_sending_without_reply=True, write_timeout=60 - ) - - if reply_text is not None: - await reply_text.delete() - - logger.info("用户 %s[%s] [bold]深渊挑战数据[/bold]: 成功发送图片", user.full_name, user.id, extra={"markup": True}) - - async def get_rendered_pic( - self, client: Client, uid: int, floor: int, total: bool, previous: bool - ) -> Union[ - Tuple[ - Union[BaseException, Any], - Union[BaseException, Any], - Union[BaseException, Any], - Union[BaseException, Any], - Union[BaseException, Any], - ], - List[RenderResult], - None, - ]: - """ - 获取渲染后的图片 - - Args: - client (Client): 获取 genshin 数据的 client - uid (int): 需要查询的 uid - floor (int): 层数 - total (bool): 是否为总览 - previous (bool): 是否为上期 - - Returns: - bytes格式的图片 - """ - - def json_encoder(value): - if isinstance(value, datetime): - return value.astimezone(TZ).strftime("%Y-%m-%d %H:%M:%S") - return value - - abyss_data = await client.get_spiral_abyss(uid, previous=previous, lang="zh-cn") - - if not abyss_data.unlocked: - raise AbyssUnlocked() - if not abyss_data.ranks.most_kills: - raise NoMostKills() - if (total or (floor > 0)) and not abyss_data.floors[0].chambers[0].battles: - 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 = await async_re_sub( - regex_01, partial(replace_01, assets_service=self.assets_service), abyss_data.json(encoder=json_encoder) - ) - result = await async_re_sub(regex_02, partial(replace_02, assets_service=self.assets_service), result) - - render_data["time"] = time - render_data["stars"] = total_stars - render_data["uid"] = 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: - avatars = await client.get_genshin_characters(uid, lang="zh-cn") - render_data["avatar_data"] = {i.id: i.constellation for i in avatars} - 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.html", 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.html", - { - **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.html", 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 (floor_data := list(filter(lambda x: x["floor"] == floor, floors))) is None: - return None - avatars = await client.get_genshin_characters(uid, lang="zh-cn") - render_data["avatar_data"] = {i.id: i.constellation for i in avatars} - 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.html", render_data, viewport={"width": 690, "height": 500} - ) - ] diff --git a/plugins/genshin/abyss_team.py b/plugins/genshin/abyss_team.py deleted file mode 100644 index bc05e43..0000000 --- a/plugins/genshin/abyss_team.py +++ /dev/null @@ -1,89 +0,0 @@ -from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update -from telegram.constants import ChatAction -from telegram.ext import CallbackContext, filters -from telegram.helpers import create_deep_linked_url - -from core.dependence.assets import AssetsService -from core.plugin import Plugin, handler -from core.services.template.services import TemplateService -from metadata.shortname import roleToId -from modules.apihelper.client.components.abyss import AbyssTeam as AbyssTeamClient -from plugins.tools.genshin import GenshinHelper, CookiesNotFoundError, PlayerNotFoundError -from utils.log import logger - -__all__ = ("AbyssTeamPlugin",) - - -class AbyssTeamPlugin(Plugin): - """深境螺旋推荐配队查询""" - - 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("^深渊推荐配队(.*)"), block=False) - async def command_start(self, update: Update, context: CallbackContext) -> None: - user = update.effective_user - message = update.effective_message - logger.info("用户 %s[%s] 查深渊推荐配队命令请求", user.full_name, user.id) - - try: - client = await self.helper.get_genshin_client(user.id) - except (CookiesNotFoundError, PlayerNotFoundError): - buttons = [[InlineKeyboardButton("点我绑定账号", url=create_deep_linked_url(context.bot.username, "set_cookie"))]] - if filters.ChatType.GROUPS.filter(message): - reply_message = await message.reply_text( - "未查询到您所绑定的账号信息,请先私聊派蒙绑定账号", reply_markup=InlineKeyboardMarkup(buttons) - ) - self.add_delete_message_job(reply_message, delay=30) - self.add_delete_message_job(message, delay=30) - else: - await message.reply_text("未查询到您所绑定的账号信息,请先绑定账号", reply_markup=InlineKeyboardMarkup(buttons)) - return - - await message.reply_chat_action(ChatAction.TYPING) - team_data = await self.team_data.get_data() - # 尝试获取用户已绑定的原神账号信息 - characters = await client.get_genshin_characters(client.uid) - user_data = [character.name for character in characters] - team_data.sort(user_data) - random_team = team_data.random_team() - abyss_teams_data = {"uid": client.uid, "version": team_data.version, "teams": []} - for i in random_team: - team = { - "up": [], - "up_rate": f"{i.up.rate * 100: .2f}%", - "down": [], - "down_rate": f"{i.down.rate * 100: .2f}%", - } - - for lane in ["up", "down"]: - for member in getattr(i, lane).formation: - name = member.name - temp = { - "icon": (await self.assets_service.avatar(roleToId(name.replace("旅行者", "空"))).icon()).as_uri(), - "name": name, - "star": member.star, - "hava": (name in user_data) if user_data else True, - } - team[lane].append(temp) - - abyss_teams_data["teams"].append(team) - - await message.reply_chat_action(ChatAction.UPLOAD_PHOTO) - render_result = await self.template_service.render( - "genshin/abyss_team/abyss_team.html", - 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", allow_sending_without_reply=True) diff --git a/plugins/genshin/avatar_list.py b/plugins/genshin/avatar_list.py deleted file mode 100644 index 12a6f16..0000000 --- a/plugins/genshin/avatar_list.py +++ /dev/null @@ -1,311 +0,0 @@ -"""练度统计""" -import asyncio -from typing import List, Optional, Sequence - -from aiohttp import ClientConnectorError -from arkowrapper import ArkoWrapper -from enkanetwork import Assets as EnkaAssets, EnkaNetworkAPI, VaildateUIDError, HTTPException, EnkaPlayerNotFound -from genshin import Client, GenshinException, InvalidCookies -from genshin.models import CalculatorCharacterDetails, CalculatorTalent, Character -from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update, User -from telegram.constants import ChatAction, ParseMode -from telegram.ext import CallbackContext, filters -from telegram.helpers import create_deep_linked_url - -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.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 metadata.genshin import AVATAR_DATA, NAMECARD_DATA -from modules.wiki.base import Model -from plugins.tools.genshin import CookiesNotFoundError, GenshinHelper, PlayerNotFoundError, CharacterDetails -from utils.enkanetwork import RedisCache -from utils.log import logger -from utils.patch.aiohttp import AioHttpTimeoutException - - -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, - redis: RedisDB = None, - helper: GenshinHelper = None, - character_details: CharacterDetails = None, - player_info_service: PlayerInfoService = None, - ) -> None: - self.cookies_service = cookies_service - self.assets_service = assets_service - self.template_service = template_service - self.enka_client = EnkaNetworkAPI(lang="chs", user_agent=config.enka_network_api_agent) - self.enka_client.set_cache(RedisCache(redis.client, key="plugin:avatar_list:enka_network", ex=60 * 60 * 3)) - self.enka_assets = EnkaAssets(lang="chs") - self.helper = helper - self.character_details = character_details - self.player_service = player_service - self.player_info_service = player_info_service - - async def get_user_client(self, update: Update, context: CallbackContext) -> Optional[Client]: - message = update.effective_message - user = update.effective_user - try: - return await self.helper.get_genshin_client(user.id) - except PlayerNotFoundError: # 若未找到账号 - buttons = [[InlineKeyboardButton("点我绑定账号", url=create_deep_linked_url(context.bot.username, "set_cookie"))]] - if filters.ChatType.GROUPS.filter(message): - reply_message = await message.reply_text( - "未查询到您所绑定的账号信息,请先私聊派蒙绑定账号", reply_markup=InlineKeyboardMarkup(buttons) - ) - self.add_delete_message_job(reply_message, delay=30) - self.add_delete_message_job(message, delay=30) - else: - await message.reply_text("未查询到您所绑定的账号信息,请先绑定账号", reply_markup=InlineKeyboardMarkup(buttons)) - except CookiesNotFoundError: - buttons = [[InlineKeyboardButton("点我绑定账号", url=create_deep_linked_url(context.bot.username, "set_cookie"))]] - if filters.ChatType.GROUPS.filter(message): - reply_message = await message.reply_text( - "此功能需要绑定cookie后使用,请先私聊派蒙绑定账号", - reply_markup=InlineKeyboardMarkup(buttons), - parse_mode=ParseMode.HTML, - ) - self.add_delete_message_job(reply_message, delay=30) - self.add_delete_message_job(message, delay=30) - else: - await message.reply_text( - "此功能需要绑定cookie后使用,请先私聊派蒙进行绑定", - parse_mode=ParseMode.HTML, - reply_markup=InlineKeyboardMarkup(buttons), - ) - - async def get_avatar_data(self, character: Character, client: Client) -> 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: Client, 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 get_final_data(self, client: Client, characters: Sequence[Character], update: Update): - try: - response = await self.enka_client.fetch_user(client.uid, info=True) - name_card = (await self.assets_service.namecard(response.player.namecard.id).navbar()).as_uri() - avatar = (await self.assets_service.avatar(response.player.avatar.id).icon()).as_uri() - nickname = response.player.nickname - if response.player.avatar.id in [10000005, 10000007]: - rarity = 5 - else: - rarity = {k: v["rank"] for k, v in AVATAR_DATA.items()}[str(response.player.avatar.id)] - return name_card, avatar, nickname, rarity - except (VaildateUIDError, EnkaPlayerNotFound, HTTPException) as exc: - logger.warning("EnkaNetwork 请求失败: %s", str(exc)) - except (AioHttpTimeoutException, ClientConnectorError) as exc: - logger.warning("EnkaNetwork 请求超时: %s", str(exc)) - except Exception as exc: - logger.error("EnkaNetwork 请求失败: %s", exc_info=exc) - choices = ArkoWrapper(characters).filter(lambda x: x.friendship == 10) # 筛选出好感满了的角色 - if choices.length == 0: # 若没有满好感角色、则以好感等级排序 - choices = ArkoWrapper(characters).sort(lambda x: x.friendship, reverse=True) - name_card_choices = ( # 找到与角色对应的满好感名片ID - ArkoWrapper(choices) - .map(lambda x: next(filter(lambda y: y["name"].split("·")[0] == x.name, NAMECARD_DATA.values()), None)) - .filter(lambda x: x) - .map(lambda x: int(x["id"])) - ) - # noinspection PyTypeChecker - name_card = (await self.assets_service.namecard(name_card_choices[0]).navbar()).as_uri() - avatar = (await self.assets_service.avatar(cid := choices[0].id).icon()).as_uri() - nickname = update.effective_user.full_name - if cid in [10000005, 10000007]: - rarity = 5 - else: - rarity = {k: v["rank"] for k, v in AVATAR_DATA.items()}[str(cid)] - return name_card, avatar, nickname, rarity - - async def get_default_final_data(self, player_id: int, characters: Sequence[Character], user: User): - player = await self.player_service.get(user.id, player_id) - player_info = await self.player_info_service.get(player) - nickname = user.full_name - name_card: Optional[str] = None - avatar: Optional[str] = None - rarity: int = 5 - if player_info is not None: - if player_info.nickname is not None: - nickname = player_info.nickname - if player_info.name_card is not None: - name_card = (await self.assets_service.namecard(player_info.name_card).navbar()).as_uri() - if player_info.hand_image is not None: - avatar = (await self.assets_service.avatar(player_info.hand_image).icon()).as_uri() - rarity = {k: v["rank"] for k, v in AVATAR_DATA.items()}[str(player_info.hand_image)] - if name_card is not None: # 须弥·正明 - name_card = (await self.assets_service.namecard(210132).navbar()).as_uri() - if avatar is not None: - if traveller := next(filter(lambda x: x.id in [10000005, 10000007], characters), None): - avatar = (await self.assets_service.avatar(traveller.id).icon()).as_uri() - else: - avatar = (await self.assets_service.avatar(10000005).icon()).as_uri() - return name_card, avatar, nickname, rarity - - @handler.command("avatars", filters.Regex(r"^/avatars\s*(?:(\d+)|(all))?$"), block=False) - @handler.message(filters.Regex(r"^(全部)?练度统计$"), block=False) - async def avatar_list(self, update: Update, context: CallbackContext): - user = update.effective_user - message = update.effective_message - - args = [i.lower() for i in context.match.groups() if i] - - all_avatars = any(["all" in args, "全部" in args]) # 是否发送全部角色 - - logger.info("用户 %s[%s] [bold]练度统计[/bold]: all=%s", user.full_name, user.id, all_avatars, extra={"markup": True}) - - client = await self.get_user_client(update, context) - if not client: - return - - notice = await message.reply_text("派蒙需要收集整理数据,还请耐心等待哦~") - await message.reply_chat_action(ChatAction.TYPING) - - try: - characters = await client.get_genshin_characters(client.uid) - avatar_datas: List[AvatarData] = await self.get_avatars_data( - characters, client, None if all_avatars else 20 - ) - except InvalidCookies as exc: - await notice.delete() - await client.get_genshin_user(client.uid) - logger.warning("用户 %s[%s] 无法请求角色数数据 API返回信息为 [%s]%s", user.full_name, user.id, exc.retcode, exc.original) - reply_message = await message.reply_text("出错了呜呜呜 ~ 当前访问令牌无法请求角色数数据,请尝试重新获取Cookie。") - 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 GenshinException as e: - await notice.delete() - if e.retcode == -502002: - reply_message = await message.reply_html("请先在米游社中使用一次养成计算器后再使用此功能~") - self.add_delete_message_job(reply_message, delay=20) - return - raise e - - try: - name_card, avatar, nickname, rarity = await self.get_final_data(client, characters, update) - except Exception as exc: # pylint: disable=W0703 - logger.error("卡片信息请求失败 %s", str(exc)) - name_card, avatar, nickname, rarity = await self.get_default_final_data(client.uid, characters, user) - - render_data = { - "uid": client.uid, # 玩家uid - "nickname": nickname, # 玩家昵称 - "avatar": avatar, # 玩家头像 - "rarity": rarity, # 玩家头像对应的角色星级 - "namecard": name_card, # 玩家名片 - "avatar_datas": avatar_datas, # 角色数据 - "has_more": len(characters) != len(avatar_datas), # 是否显示了全部角色 - } - - as_document = all_avatars and len(characters) > 20 - - await message.reply_chat_action(ChatAction.UPLOAD_DOCUMENT if as_document else ChatAction.UPLOAD_PHOTO) - - image = await self.template_service.render( - "genshin/avatar_list/main.html", - render_data, - viewport={"width": 1040, "height": 500}, - full_page=True, - query_selector=".container", - file_type=FileType.DOCUMENT if as_document else FileType.PHOTO, - ttl=30 * 24 * 60 * 60, - ) - self.add_delete_message_job(notice, delay=5) - if as_document: - await image.reply_document(message, filename="练度统计.png") - else: - await image.reply_photo(message) - - logger.info( - "用户 %s[%s] [bold]练度统计[/bold]发送%s成功", - user.full_name, - user.id, - "文件" if all_avatars else "图片", - extra={"markup": True}, - ) diff --git a/plugins/genshin/birthday.py b/plugins/genshin/birthday.py deleted file mode 100644 index ebd4302..0000000 --- a/plugins/genshin/birthday.py +++ /dev/null @@ -1,187 +0,0 @@ -import re -from datetime import datetime -from typing import List, Optional - -from genshin import Client, GenshinException -from genshin.client.routes import Route -from genshin.utility import recognize_genshin_server -from telegram import Update, InlineKeyboardMarkup, InlineKeyboardButton -from telegram.constants import ParseMode -from telegram.ext import filters, MessageHandler, CommandHandler, CallbackContext -from telegram.helpers import create_deep_linked_url - -from core.basemodel import RegionEnum -from core.plugin import Plugin, handler -from core.services.cookies import CookiesService -from core.services.users.services import UserService -from metadata.genshin import AVATAR_DATA -from metadata.shortname import roleToId, roleToName -from modules.apihelper.client.components.calendar import Calendar -from plugins.tools.genshin import GenshinHelper, CookiesNotFoundError, PlayerNotFoundError -from utils.genshin import fetch_hk4e_token_by_cookie, recognize_genshin_game_biz -from utils.log import logger - -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 BirthdayPlugin(Plugin): - """Birthday.""" - - def __init__( - self, - user_service: UserService, - helper: GenshinHelper, - cookie_service: CookiesService, - ): - """Load Data.""" - self.birthday_list = {} - self.user_service = user_service - self.cookie_service = cookie_service - self.helper = helper - - async def initialize(self): - self.birthday_list = await Calendar.async_gen_birthday_list() - self.birthday_list.get("6_1", []).append("派蒙") - - async def get_today_birthday(self) -> List[str]: - key = ( - rm_starting_str(datetime.now().strftime("%m"), "0") - + "_" - + rm_starting_str(datetime.now().strftime("%d"), "0") - ) - return (self.birthday_list.get(key, [])).copy() - - @handler.command(command="birthday", block=False) - async def command_start(self, update: Update, context: CallbackContext) -> None: - message = update.effective_message - user = update.effective_user - key = ( - rm_starting_str(datetime.now().strftime("%m"), "0") - + "_" - + rm_starting_str(datetime.now().strftime("%d"), "0") - ) - args = self.get_args(context) - - if len(args) >= 1: - msg = args[0] - logger.info("用户 %s[%s] 查询角色生日命令请求 || 参数 %s", user.full_name, user.id, 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.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: - logger.info("用户 %s[%s] 查询今日角色生日列表", user.full_name, user.id) - today_list = await self.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) - - @staticmethod - async def get_card(client: Client, role_id: int) -> None: - """领取画片""" - url = BIRTHDAY_URL.get_url() - params = { - "game_biz": recognize_genshin_game_biz(client.uid), - "lang": "zh-cn", - "badge_uid": client.uid, - "badge_region": recognize_genshin_server(client.uid), - "activity_id": "20220301153521", - } - json = { - "role_id": role_id, - } - await client.cookie_manager.request(url, method="POST", params=params, json=json) - - @staticmethod - def role_to_id(name: str) -> Optional[int]: - if name == "派蒙": - return -1 - return roleToId(name) - - @handler(CommandHandler, command="birthday_card", block=False) - @handler(MessageHandler, filters=filters.Regex("^领取角色生日画片$"), block=False) - async def command_birthday_card_start(self, update: Update, context: CallbackContext) -> None: - message = update.effective_message - user = update.effective_user - logger.info("用户 %s[%s] 领取生日画片命令请求", user.full_name, user.id) - today_list = await self.get_today_birthday() - if not today_list: - 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 - try: - client = await self.helper.get_genshin_client(user.id) - except (CookiesNotFoundError, PlayerNotFoundError): - buttons = [[InlineKeyboardButton("点我绑定账号", url=create_deep_linked_url(context.bot.username, "set_cookie"))]] - if filters.ChatType.GROUPS.filter(message): - reply_msg = await message.reply_text( - "此功能需要绑定cookie后使用,请先私聊派蒙绑定账号", - reply_markup=InlineKeyboardMarkup(buttons), - parse_mode=ParseMode.HTML, - ) - self.add_delete_message_job(reply_msg, delay=30) - self.add_delete_message_job(message, delay=30) - else: - await message.reply_text( - "此功能需要绑定cookie后使用,请先私聊派蒙进行绑定", - parse_mode=ParseMode.HTML, - reply_markup=InlineKeyboardMarkup(buttons), - ) - return - if client.region == RegionEnum.HOYOLAB: - text = "此功能当前只支持国服账号哦~" - else: - await fetch_hk4e_token_by_cookie(client) - for name in today_list.copy(): - if role_id := self.role_to_id(name): - try: - await self.get_card(client, role_id) - except GenshinException as e: - if e.retcode in {-512008, -512009}: # 未过生日、已领取过 - today_list.remove(name) - if today_list: - text = f"成功领取了 {'、'.join(today_list)} 的生日画片~" - else: - text = "没有领取到生日画片哦 ~ 可能是已经领取过了" - 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 3a9d3f7..0000000 --- a/plugins/genshin/calendar.py +++ /dev/null @@ -1,60 +0,0 @@ -from datetime import datetime, timedelta -from typing import Dict - -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 modules.apihelper.client.components.calendar import Calendar -from utils.log import logger - -try: - import ujson as jsonlib -except ImportError: - import json as jsonlib - - -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 - - @handler.command("calendar", block=False) - @handler(MessageHandler, filters=filters.Regex(r"^(活动)+(日历|日历列表)$"), block=False) - async def command_start(self, update: Update, _: CallbackContext) -> None: - user = update.effective_user - message = update.effective_message - mode = "list" if "列表" in message.text else "calendar" - logger.info("用户 %s[%s] 查询日历 | 模式 %s", user.full_name, user.id, mode) - await message.reply_chat_action(ChatAction.TYPING) - data = await self._fetch_data() - data["display_mode"] = mode - image = await self.template_service.render( - "genshin/calendar/calendar.html", - data, - query_selector=".container", - ) - await message.reply_chat_action(ChatAction.UPLOAD_PHOTO) - await image.reply_photo(message) diff --git a/plugins/genshin/daily/material.py b/plugins/genshin/daily/material.py deleted file mode 100644 index 0d10793..0000000 --- a/plugins/genshin/daily/material.py +++ /dev/null @@ -1,510 +0,0 @@ -import asyncio -import contextlib -import os -import re -from asyncio import Lock -from ctypes import c_double -from datetime import datetime -from functools import partial -from multiprocessing import Value -from pathlib import Path -from ssl import SSLZeroReturnError -from typing import Any, Dict, Iterable, Iterator, List, Literal, Optional, Tuple - -import ujson as json -from aiofiles import open as async_open -from arkowrapper import ArkoWrapper -from bs4 import BeautifulSoup -from genshin import Client, GenshinException, InvalidCookies -from genshin.models import Character -from httpx import AsyncClient, HTTPError -from pydantic import BaseModel -from telegram import Message, Update, User -from telegram.constants import ChatAction, ParseMode -from telegram.error import RetryAfter, TimedOut -from telegram.ext import CallbackContext - -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 GenshinHelper, PlayerNotFoundError, CookiesNotFoundError, CharacterDetails -from utils.log import logger - -INTERVAL = 1 - -DATA_TYPE = Dict[str, List[List[str]]] -DATA_FILE_PATH = Path(__file__).joinpath("../daily.json").resolve() -DOMAINS = ["忘却之峡", "太山府", "菫色之庭", "昏识塔", "塞西莉亚苗圃", "震雷连山密宫", "砂流之庭", "有顶塔"] -DOMAIN_AREA_MAP = dict(zip(DOMAINS, ["蒙德", "璃月", "稻妻", "须弥"] * 2)) - -WEEK_MAP = ["一", "二", "三", "四", "五", "六", "日"] - - -def sort_item(items: List["ItemData"]) -> Iterable["ItemData"]: - """对武器和角色进行排序 - - 排序规则:持有(星级 > 等级 > 命座/精炼) > 未持有(星级 > 等级 > 命座/精炼) - """ - return ( - ArkoWrapper(items) - .sort(lambda x: x.level or -1, reverse=True) - .groupby(lambda x: x.level is None) # 根据持有与未持有进行分组并排序 - .map( - lambda x: ( - ArkoWrapper(x[1]) - .sort(lambda y: y.rarity, reverse=True) - .groupby(lambda y: y.rarity) # 根据星级分组并排序 - .map( - lambda y: ( - ArkoWrapper(y[1]) - .sort(lambda z: z.refinement or z.constellation or -1, reverse=True) - .groupby(lambda z: z.refinement or z.constellation or -1) # 根据命座/精炼进行分组并排序 - .map(lambda i: ArkoWrapper(i[1]).sort(lambda j: j.id)) - ) - ) - ) - ) - .flat(3) - ) - - -def get_material_serial_name(names: Iterable[str]) -> str: - """获取材料的系列名""" - - 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 DailyMaterial(Plugin): - """每日素材表""" - - data: DATA_TYPE - locks: Tuple[Lock] = (Lock(), Lock()) - - 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): - """插件在初始化时,会检查一下本地是否缓存了每日素材的数据""" - data = None - - async def task_daily(): - async with self.locks[0]: - logger.info("正在开始获取每日素材缓存") - self.data = await self._refresh_data() - - if (not DATA_FILE_PATH.exists()) or ( # 若缓存不存在 - (datetime.today() - datetime.fromtimestamp(os.stat(DATA_FILE_PATH).st_mtime)).days > 3 # 若缓存过期,超过了3天 - ): - asyncio.create_task(task_daily()) # 创建后台任务 - if not data and DATA_FILE_PATH.exists(): # 若存在,则读取至内存中 - async with async_open(DATA_FILE_PATH) as file: - data = json.loads(await file.read()) - self.data = data - - async def _get_skills_data(self, client: Client, character: Character) -> Optional[List[int]]: - detail = await self.character_details.get_character_details(client, character) - 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_data_from_user(self, user: User) -> Tuple[Optional[Client], Dict[str, List[Any]]]: - """获取已经绑定的账号的角色、武器信息""" - user_data = {"avatar": [], "weapon": []} - try: - logger.debug("尝试获取已绑定的原神账号") - client = await self.helper.get_genshin_client(user.id) - logger.debug("获取账号数据成功: UID=%s", client.uid) - characters = await client.get_genshin_characters(client.uid) - for character in characters: - if character.name == "旅行者": # 跳过主角 - continue - cid = AVATAR_DATA[str(character.id)]["id"] - weapon = character.weapon - user_data["avatar"].append( - ItemData( - id=cid, - name=character.name, - rarity=character.rarity, - level=character.level, - constellation=character.constellation, - gid=character.id, - icon=(await self.assets_service.avatar(cid).icon()).as_uri(), - origin=character, - ) - ) - user_data["weapon"].append( - ItemData( - id=str(weapon.id), - name=weapon.name, - level=weapon.level, - rarity=weapon.rarity, - refinement=weapon.refinement, - icon=( - await getattr( # 判定武器的突破次数是否大于 2 ;若是, 则将图标替换为 awakened (觉醒) 的图标 - self.assets_service.weapon(weapon.id), "icon" if weapon.ascension < 2 else "awaken" - )() - ).as_uri(), - c_path=(await self.assets_service.avatar(cid).side()).as_uri(), - ) - ) - except (PlayerNotFoundError, CookiesNotFoundError): - logger.info("未查询到用户 %s[%s] 所绑定的账号信息", user.full_name, user.id) - except InvalidCookies: - logger.info("用户 %s[%s] 所绑定的账号信息已失效", user.full_name, user.id) - else: - # 没有异常返回数据 - return client, user_data - # 有上述异常的, client 会返回 None - return None, user_data - - @handler.command("daily_material", block=False) - async def daily_material(self, update: Update, context: CallbackContext): - user = update.effective_user - 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 - - logger.info("用户 %s[%s}] 每日素材命令请求 || 参数 weekday=%s full=%s", user.full_name, user.id, WEEK_MAP[weekday], full) - - if weekday == 6: - await message.reply_text( - ("今天" if title == "今日" else "这天") + "是星期天, 全部素材都可以刷哦~", parse_mode=ParseMode.HTML - ) - return - - if self.locks[0].locked(): # 若检测到了第一个锁:正在下载每日素材表的数据 - notice = await message.reply_text("派蒙正在摘抄每日素材表,以后再来探索吧~") - self.add_delete_message_job(notice, delay=5) - return - - if self.locks[1].locked(): # 若检测到了第二个锁:正在下载角色、武器、材料的图标 - await message.reply_text("派蒙正在搬运每日素材的图标,以后再来探索吧~") - return - - notice = await message.reply_text("派蒙可能需要找找图标素材,还请耐心等待哦~") - await message.reply_chat_action(ChatAction.TYPING) - - # 获取已经缓存的秘境素材信息 - local_data = {"avatar": [], "weapon": []} - if not self.data: # 若没有缓存每日素材表的数据 - logger.info("正在获取每日素材缓存") - self.data = await self._refresh_data() - for domain, sche in self.data.items(): - area = DOMAIN_AREA_MAP[domain := domain.strip()] # 获取秘境所在的区域 - type_ = "avatar" if DOMAINS.index(domain) < 4 else "weapon" # 获取秘境的培养素材的类型:是天赋书还是武器突破材料 - # 将读取到的数据存入 local_data 中 - local_data[type_].append({"name": area, "materials": sche[weekday][0], "items": sche[weekday][1]}) - - # 尝试获取用户已绑定的原神账号信息 - client, user_data = await self._get_data_from_user(user) - - await message.reply_chat_action(ChatAction.TYPING) - render_data = RenderData(title=title, time=time, uid=client.uid if client else client) - - calculator_sync: bool = True # 默认养成计算器同步为开启 - for type_ in ["avatar", "weapon"]: - areas = [] - for area_data in local_data[type_]: # 遍历每个区域的信息:蒙德、璃月、稻妻、须弥 - items = [] - for id_ in area_data["items"]: # 遍历所有该区域下,当天(weekday)可以培养的角色、武器 - added = False - for i in user_data[type_]: # 从已经获取的角色数据中查找对应角色、武器 - if id_ == str(i.id): - if i.rarity > 3: # 跳过 3 星及以下的武器 - if type_ == "avatar" and client and calculator_sync: # client 不为 None 时给角色添加天赋信息 - try: - skills = await self._get_skills_data(client, i.origin) - i.skills = skills - except InvalidCookies: - calculator_sync = False - except GenshinException as e: - if e.retcode == -502002: - calculator_sync = False # 发现角色养成计算器没启用 设置状态为 False 并防止下次继续获取 - self.add_delete_message_job(notice, delay=5) - await notice.edit_text( - "获取角色天赋信息失败,如果想要显示角色天赋信息,请先在米游社/HoYoLab中使用一次养成计算器后再使用此功能~", - parse_mode=ParseMode.HTML, - ) - else: - raise e - items.append(i) - added = True - if added: - continue - try: - item = HONEY_DATA[type_][id_] - except KeyError: # 跳过不存在或者已忽略的角色、武器 - logger.warning("未在 honey 数据中找到 %s[%s] 的信息", type_, id_) - continue - if item[2] < 4: # 跳过 3 星及以下的武器 - continue - items.append( - ItemData( # 添加角色数据中未找到的 - id=id_, - name=item[1], - rarity=item[2], - icon=(await getattr(self.assets_service, type_)(id_).icon()).as_uri(), - ) - ) - materials = [] - for mid in area_data["materials"]: # 添加这个区域当天(weekday)的培养素材 - try: - path = (await self.assets_service.material(mid).icon()).as_uri() - material = HONEY_DATA["material"][mid] - materials.append(ItemData(id=mid, icon=path, name=material[1], rarity=material[2])) - except AssetsCouldNotFound as exc: - logger.warning("AssetsCouldNotFound message[%s] target[%s]", exc.message, exc.target) - await notice.edit_text("出错了呜呜呜 ~ 派蒙找不到一些素材") - return - areas.append( - AreaData( - name=area_data["name"], - materials=materials, - # template previewer pickle cannot serialize generator - items=list(sort_item(items)), - material_name=get_material_serial_name(map(lambda x: x.name, materials)), - ) - ) - setattr(render_data, {"avatar": "character"}.get(type_, type_), areas) - - 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.html", - {"data": render_data}, - {"width": 1164, "height": 500}, - file_type=file_type, - ttl=30 * 24 * 60 * 60, - ), - self.template_service.render( # 渲染武器素材页 - "genshin/daily_material/weapon.html", - {"data": render_data}, - {"width": 1164, "height": 500}, - file_type=file_type, - ttl=30 * 24 * 60 * 60, - ), - ) - - self.add_delete_message_job(notice, 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", block=False) - async def refresh(self, update: Update, context: CallbackContext): - 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("派蒙还在抄每日素材表呢,我有在好好工作哦~") - self.add_delete_message_job(notice, delay=10) - return - if self.locks[1].locked(): - notice = await message.reply_text("派蒙正在搬运每日素材图标,在努力工作呢!") - self.add_delete_message_job(notice, delay=10) - return - async with self.locks[1]: # 锁住第二把锁 - notice = await message.reply_text("派蒙正在重新摘抄每日素材表,请稍等~", parse_mode=ParseMode.HTML) - async with self.locks[0]: # 锁住第一把锁 - data = await self._refresh_data() - notice = await notice.edit_text( - "每日素材表" + ("摘抄完成!" if data else "坏掉了!等会它再长好了之后我再抄。。。") + "\n正搬运每日素材的图标中。。。", - parse_mode=ParseMode.HTML, - ) - self.data = data or self.data - 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_data(self, retry: int = 5) -> DATA_TYPE: - """刷新来自 honey impact 的每日素材表""" - from bs4 import Tag - - result = {} - for i in range(retry): # 重复尝试 retry 次 - try: - response = await self.client.get("https://genshin.honeyhunterworld.com/?lang=CHS") - soup = BeautifulSoup(response.text, "lxml") - calendar = soup.select(".calendar_day_wrap")[0] - key: str = "" - for tag in calendar: - tag: Tag - if tag.name == "span": # 如果是秘境 - key = tag.find("a").text.strip() - result[key] = [[[], []] for _ in range(7)] - for day, div in enumerate(tag.find_all("div")): - result[key][day][0] = [] - for a in div.find_all("a"): - honey_id = re.findall(r"/(.*)?/", a["href"])[0] - mid: str = [i[0] for i in HONEY_DATA["material"].items() if i[1][0] == honey_id][0] - result[key][day][0].append(mid) - else: # 如果是角色或武器 - id_ = re.findall(r"/(.*)?/", tag["href"])[0] - if tag.text.strip() == "旅行者": # 忽略主角 - continue - id_ = ("" if id_.startswith("i_n") else "10000") + re.findall(r"\d+", id_)[0] - for day in map(int, tag.find("div")["data-days"]): # 获取该角色/武器的可培养天 - result[key][day][1].append(id_) - for stage, schedules in result.items(): - for day, _ in enumerate(schedules): - # noinspection PyUnresolvedReferences - result[stage][day][1] = list(set(result[stage][day][1])) # 去重 - async with async_open(DATA_FILE_PATH, "w", encoding="utf-8") as file: - await file.write(json.dumps(result)) # skipcq: PY-W0079 - logger.info("每日素材刷新成功") - break - except (HTTPError, SSLZeroReturnError): - from asyncio import sleep - - await sleep(1) - if i <= retry - 1: - logger.warning("每日素材刷新失败, 正在重试") - else: - logger.error("每日素材刷新失败, 请稍后重试") - continue - # noinspection PyTypeChecker - return result - - async def _download_icon(self, message: Optional[Message] = None) -> float: - """下载素材图标""" - asset_list = [] - - from time import time as time_ - - 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): - with contextlib.suppress(TimedOut, RetryAfter): - await message.edit_text( - "\n".join(message.text_html.split("\n")[:2] + [text]), parse_mode=ParseMode.HTML - ) - the_time.value = time_() - - async def task(item_id, name, item_type): - 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} 的图标素材。。。成功!") - - 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)) - await asyncio.gather(*task_list) # 等待所有任务执行完成 - try: - await message.edit_text( - "\n".join(message.text_html.split("\n")[:2] + ["图标素材下载完成!"]), parse_mode=ParseMode.HTML - ) - except RetryAfter as e: - await asyncio.sleep(e.retry_after) - await message.edit_text( - "\n".join(message.text_html.split("\n")[:2] + ["图标素材下载完成!"]), parse_mode=ParseMode.HTML - ) - except Exception as e: - logger.debug(e) - - logger.info("图标素材下载完成") - return the_time.value - - -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: Literal["蒙德", "璃月", "稻妻", "须弥"] # 区域名 - material_name: str # 区域的材料系列名 - materials: List[ItemData] = [] # 区域材料 - items: Iterable[ItemData] = [] # 可培养的角色或武器 - - -class RenderData(BaseModel): - title: str # 页面标题,主要用于显示星期几 - time: str # 页面时间 - uid: Optional[int] = None # 用户UID - character: List[AreaData] = [] # 角色数据 - weapon: List[AreaData] = [] # 武器数据 - - def __getitem__(self, item): - return self.__getattribute__(item) diff --git a/plugins/genshin/daily_note.py b/plugins/genshin/daily_note.py deleted file mode 100644 index 021fd1a..0000000 --- a/plugins/genshin/daily_note.py +++ /dev/null @@ -1,130 +0,0 @@ -import datetime -from datetime import datetime -from typing import Optional - -import genshin -from genshin import DataNotPublic -from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update -from telegram.constants import ChatAction -from telegram.ext import ConversationHandler, filters, CallbackContext -from telegram.helpers import create_deep_linked_url - -from core.plugin import Plugin, handler -from core.services.template.models import RenderResult -from core.services.template.services import TemplateService -from plugins.tools.genshin import GenshinHelper, CookiesNotFoundError, PlayerNotFoundError -from utils.log import logger - -__all__ = ("DailyNotePlugin",) - - -class DailyNotePlugin(Plugin): - """每日便签""" - - def __init__( - self, - template: TemplateService, - helper: GenshinHelper, - ): - self.template_service = template - self.helper = helper - - async def _get_daily_note(self, client: genshin.Client) -> RenderResult: - daily_info = await client.get_genshin_notes(client.uid) - - 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 - 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": client.uid, - "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, - } - render_result = await self.template_service.render( - "genshin/daily_note/daily_note.html", - render_data, - {"width": 600, "height": 548}, - full_page=False, - ttl=8 * 60, - ) - return render_result - - @handler.command("dailynote", block=False) - @handler.message(filters.Regex("^当前状态(.*)"), block=False) - async def command_start(self, update: Update, _: CallbackContext) -> Optional[int]: - message = update.effective_message - user = update.effective_user - logger.info("用户 %s[%s] 每日便签命令请求", user.full_name, user.id) - - try: - # 获取当前用户的 genshin.Client - client = await self.helper.get_genshin_client(user.id) - # 渲染 - render_result = await self._get_daily_note(client) - except (CookiesNotFoundError, PlayerNotFoundError): - buttons = [ - [ - InlineKeyboardButton( - "点我绑定账号", url=create_deep_linked_url(self.application.bot.username, "set_cookie") - ) - ] - ] - if filters.ChatType.GROUPS.filter(message): - reply_message = await message.reply_text( - "未查询到您所绑定的账号信息,请先私聊派蒙绑定账号", reply_markup=InlineKeyboardMarkup(buttons) - ) - self.add_delete_message_job(reply_message, delay=30) - self.add_delete_message_job(message, delay=30) - else: - await message.reply_text("未查询到您所绑定的账号信息,请先绑定账号", reply_markup=InlineKeyboardMarkup(buttons)) - return - 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 ConversationHandler.END - - await message.reply_chat_action(ChatAction.UPLOAD_PHOTO) - await render_result.reply_photo(message, filename=f"{client.uid}.png", allow_sending_without_reply=True) diff --git a/plugins/genshin/help_raw.py b/plugins/genshin/help_raw.py deleted file mode 100644 index 9121121..0000000 --- a/plugins/genshin/help_raw.py +++ /dev/null @@ -1,38 +0,0 @@ -import os -from typing import Optional - -import aiofiles -from bs4 import BeautifulSoup -from telegram import Update -from telegram.ext import CallbackContext - -from core.plugin import Plugin, handler -from utils.log import logger - -__all__ = ("HelpRawPlugin",) - - -class HelpRawPlugin(Plugin): - def __init__(self): - self.help_raw: Optional[str] = None - - async def initialize(self): - file_path = os.path.join(os.getcwd(), "resources", "bot", "help", "help.html") # resources/bot/help/help.html - async with aiofiles.open(file_path, mode="r", encoding="utf-8") as f: - html_content = await f.read() - soup = BeautifulSoup(html_content, "lxml") - command_div = soup.find_all("div", _class="command") - for div in command_div: - command_name_div = div.find("div", _class="command_name") - if command_name_div: - command_description_div = div.find("div", _class="command-description") - if command_description_div: - self.help_raw += f"/{command_name_div.text} - {command_description_div}" - - @handler.command(command="help_raw", block=False) - async def start(self, update: Update, _: CallbackContext): - if self.help_raw is not None: - message = update.effective_message - user = update.effective_user - logger.info("用户 %s[%s] 发出 help_raw 命令", user.full_name, user.id) - await message.reply_text(self.help_raw, allow_sending_without_reply=True) diff --git a/plugins/genshin/hilichurls.py b/plugins/genshin/hilichurls.py deleted file mode 100644 index 3ecd73b..0000000 --- a/plugins/genshin/hilichurls.py +++ /dev/null @@ -1,52 +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 - user = update.effective_user - 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 - logger.info("用户 %s[%s] 查询今日角色生日列表 查询丘丘语字典命令请求 || 参数 %s", user.full_name, user.id, 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 68d5a9c..0000000 --- a/plugins/genshin/ledger.py +++ /dev/null @@ -1,151 +0,0 @@ -import os -import re -from datetime import datetime, timedelta - -from genshin import DataNotPublic, InvalidCookies, GenshinException -from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update -from telegram.constants import ChatAction -from telegram.ext import filters, CallbackContext -from telegram.helpers import create_deep_linked_url - -from core.plugin import Plugin, handler -from core.services.cookies import CookiesService -from core.services.template.models import RenderResult -from core.services.template.services import TemplateService -from plugins.tools.genshin import CookiesNotFoundError, GenshinHelper, PlayerNotFoundError -from utils.log import logger - -__all__ = ("LedgerPlugin",) - - -class LedgerPlugin(Plugin): - """旅行札记查询""" - - def __init__( - self, - helper: GenshinHelper, - cookies_service: CookiesService, - template_service: TemplateService, - ): - self.template_service = template_service - self.cookies_service = cookies_service - self.current_dir = os.getcwd() - self.helper = helper - - async def _start_get_ledger(self, client, month=None) -> RenderResult: - diary_info = await client.get_diary(client.uid, month=month) - 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": client.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.html", ledger_data, {"width": 580, "height": 610} - ) - return render_result - - @handler.command(command="ledger", block=False) - @handler.message(filters=filters.Regex("^旅行札记查询(.*)"), block=False) - async def command_start(self, update: Update, context: CallbackContext) -> None: - user = update.effective_user - 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 - month = now_time.month - 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 - logger.info("用户 %s[%s] 查询旅行札记", user.full_name, user.id) - await message.reply_chat_action(ChatAction.TYPING) - try: - client = await self.helper.get_genshin_client(user.id) - try: - render_result = await self._start_get_ledger(client, month) - except InvalidCookies as exc: # 如果抛出InvalidCookies 判断是否真的玄学过期(或权限不足?) - await client.get_genshin_user(client.uid) - logger.warning( - "用户 %s[%s] 无法请求旅行札记数据 API返回信息为 [%s]%s", user.full_name, user.id, exc.retcode, exc.original - ) - reply_message = await message.reply_text("出错了呜呜呜 ~ 当前访问令牌无法请求角色数数据,请尝试重新获取Cookie。") - 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 (PlayerNotFoundError, CookiesNotFoundError): - buttons = [ - [ - InlineKeyboardButton( - "点我绑定账号", url=create_deep_linked_url(self.application.bot.username, "set_cookie") - ) - ] - ] - if filters.ChatType.GROUPS.filter(message): - reply_message = await message.reply_text( - "未查询到您所绑定的账号信息,请先私聊派蒙绑定账号", reply_markup=InlineKeyboardMarkup(buttons) - ) - self.add_delete_message_job(reply_message, delay=30) - self.add_delete_message_job(message, delay=30) - else: - await message.reply_text("未查询到您所绑定的账号信息,请先绑定账号", reply_markup=InlineKeyboardMarkup(buttons)) - return - 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 GenshinException as exc: - if exc.retcode == -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.uid}.png", allow_sending_without_reply=True) diff --git a/plugins/genshin/map.py b/plugins/genshin/map.py deleted file mode 100644 index d01391b..0000000 --- a/plugins/genshin/map.py +++ /dev/null @@ -1,195 +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"派蒙一共找到了 {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): - message = update.effective_message - args = context.args - group_dict = context.match and context.match.groupdict() - user = update.effective_user - 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 - logger.info("用户: %s [%s] 使用 map 命令查询了 %s", user.username, user.id, 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) - 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 1c93dfc..0000000 --- a/plugins/genshin/material.py +++ /dev/null @@ -1,234 +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 ascension_material.icon()).as_uri(), - "name": MATERIAL_DATA[str(ascension_material.id)]["name"], - }, - { - "num": 9, - "rarity": 3, - "icon": (await self.assets_service.material(ascension_material.id - 1).icon()).as_uri(), - "name": MATERIAL_DATA[str(ascension_material.id - 1)]["name"], - }, - { - "num": 9, - "rarity": 4, - "icon": (await self.assets_service.material(str(ascension_material.id - 2)).icon()).as_uri(), - "name": MATERIAL_DATA[str(ascension_material.id - 2)]["name"], - }, - { - "num": 6, - "rarity": 5, - "icon": (await self.assets_service.material(ascension_material.id - 3).icon()).as_uri(), - "name": MATERIAL_DATA[str(ascension_material.id - 3)]["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.html", - 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 - user = update.effective_user - 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) - logger.info("用户 %s[%s] 查询角色培养素材命令请求 || 参数 %s", user.full_name, user.id, 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/pay_log.py b/plugins/genshin/pay_log.py deleted file mode 100644 index 603ee9c..0000000 --- a/plugins/genshin/pay_log.py +++ /dev/null @@ -1,236 +0,0 @@ -import genshin -from telegram import Update, User, InlineKeyboardButton, InlineKeyboardMarkup -from telegram.constants import ChatAction -from telegram.ext import CallbackContext, CommandHandler, MessageHandler, filters, ConversationHandler -from telegram.helpers import create_deep_linked_url - -from core.basemodel import RegionEnum -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 plugins.tools.genshin import GenshinHelper, PlayerNotFoundError -from utils.genshin import get_authkey_by_stoken -from utils.log import logger - -INPUT_URL, CONFIRM_DELETE = range(10100, 10102) - - -class PayLogPlugin(Plugin.Conversation): - """充值记录导入/导出/分析""" - - def __init__( - self, - template_service: TemplateService, - players_service: PlayersService, - cookie_service: CookiesService, - helper: GenshinHelper, - ): - self.template_service = template_service - self.players_service = players_service - self.cookie_service = cookie_service - self.pay_log = PayLog() - self.helper = helper - - async def _refresh_user_data(self, user: User, authkey: str = None) -> str: - """刷新用户数据 - :param user: 用户 - :param authkey: 认证密钥 - :return: 返回信息 - """ - try: - logger.debug("尝试获取已绑定的原神账号") - client = await self.helper.get_genshin_client(user.id, need_cookie=False) - new_num = await self.pay_log.get_log_data(user.id, client, authkey) - return "更新完成,本次没有新增数据" if new_num == 0 else f"更新完成,本次共新增{new_num}条充值记录" - except PayLogNotFound: - return "派蒙没有找到你的充值记录,快去充值吧~" - 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 "派蒙没有找到您所绑定的账号信息,请先私聊派蒙绑定账号" - - @conversation.entry_point - @handler(CommandHandler, command="pay_log_import", filters=filters.ChatType.PRIVATE, block=False) - @handler(MessageHandler, filters=filters.Regex("^导入充值记录$") & filters.ChatType.PRIVATE, block=False) - async def command_start(self, update: Update, context: CallbackContext) -> int: - message = update.effective_message - user = update.effective_user - args = self.get_args(context) - logger.info("用户 %s[%s] 导入充值记录命令请求", user.full_name, user.id) - authkey = from_url_get_authkey(args[0] if args else "") - if not args: - 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 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 - client = genshin.Client( - cookies=cookies.data, - game=genshin.types.Game.GENSHIN, - region=genshin.Region.CHINESE, - lang="zh-cn", - uid=player_info.player_id, - ) - authkey = await get_authkey_by_stoken(client) - if not authkey: - await message.reply_text( - "开始导入充值历史记录:请通过 https://paimon.moe/wish/import 获取抽卡记录链接后发送给我" - "(非 paimon.moe 导出的文件数据)\n\n" - "> 在绑定 Cookie 时添加 stoken 可能有特殊效果哦(仅限国服)\n" - "注意:导入的数据将会与旧数据进行合并。", - parse_mode="html", - ) - return INPUT_URL - text = "小派蒙正在从服务器获取数据,请稍后" - if not args: - text += "\n\n> 由于你绑定的 Cookie 中存在 stoken ,本次通过 stoken 自动刷新数据" - reply = await message.reply_text(text) - 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, _: CallbackContext) -> 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("小派蒙正在从服务器获取数据,请稍后") - 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, _: CallbackContext) -> int: - message = update.effective_message - user = update.effective_user - logger.info("用户 %s[%s] 删除充值记录命令请求", user.full_name, user.id) - try: - client = await self.helper.get_genshin_client(user.id, need_cookie=False) - except PlayerNotFoundError: - logger.info("未查询到用户 %s[%s] 所绑定的账号信息", user.full_name, user.id) - await message.reply_text("未查询到您所绑定的账号信息,请先绑定账号") - return ConversationHandler.END - _, status = await self.pay_log.load_history_info(str(user.id), str(client.uid), 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: CallbackContext) -> 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: CallbackContext): - 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") - client = await self.helper.get_genshin_client(cid, need_cookie=False) - if client is None: - await message.reply_text("该用户暂未绑定账号") - return - _, status = await self.pay_log.load_history_info(str(cid), str(client.uid), only_status=True) - if not status: - await message.reply_text("该用户还没有导入充值记录") - return - status = await self.pay_log.remove_history_info(str(cid), str(client.uid)) - 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: CallbackContext) -> None: - message = update.effective_message - user = update.effective_user - logger.info("用户 %s[%s] 导出充值记录命令请求", user.full_name, user.id) - try: - client = await self.helper.get_genshin_client(user.id, need_cookie=False) - await message.reply_chat_action(ChatAction.TYPING) - path = self.pay_log.get_file_path(str(user.id), str(client.uid)) - 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("派蒙没有找到你的充值记录,快来私聊派蒙导入吧~", 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("未查询到您所绑定的账号信息,请先绑定账号") - - @handler(CommandHandler, command="pay_log", block=False) - @handler(MessageHandler, filters=filters.Regex("^充值记录$"), block=False) - async def command_start_analysis(self, update: Update, context: CallbackContext) -> None: - message = update.effective_message - user = update.effective_user - logger.info("用户 %s[%s] 充值记录统计命令请求", user.full_name, user.id) - try: - client = await self.helper.get_genshin_client(user.id, need_cookie=False) - await message.reply_chat_action(ChatAction.TYPING) - data = await self.pay_log.get_analysis(user.id, client) - await message.reply_chat_action(ChatAction.UPLOAD_PHOTO) - png_data = await self.template_service.render( - "genshin/pay_log/pay_log.html", 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("派蒙没有找到你的充值记录,快来点击按钮私聊派蒙导入吧~", reply_markup=InlineKeyboardMarkup(buttons)) - except PlayerNotFoundError: - logger.info("未查询到用户 %s[%s] 所绑定的账号信息", user.full_name, user.id) - buttons = [[InlineKeyboardButton("点我绑定账号", url=create_deep_linked_url(context.bot.username, "set_uid"))]] - if filters.ChatType.GROUPS.filter(message): - reply_message = await message.reply_text( - "未查询到您所绑定的账号信息,请先私聊派蒙绑定账号", reply_markup=InlineKeyboardMarkup(buttons) - ) - self.add_delete_message_job(reply_message, delay=30) - self.add_delete_message_job(message, delay=30) - else: - await message.reply_text("未查询到您所绑定的账号信息,请先绑定账号", reply_markup=InlineKeyboardMarkup(buttons)) diff --git a/plugins/genshin/player_cards.py b/plugins/genshin/player_cards.py deleted file mode 100644 index 5751cc7..0000000 --- a/plugins/genshin/player_cards.py +++ /dev/null @@ -1,602 +0,0 @@ -import math -from typing import Any, List, Tuple, Union, Optional, TYPE_CHECKING - -from enkanetwork import ( - DigitType, - EnkaNetworkAPI, - EnkaNetworkResponse, - EnkaServerError, - Equipments, - EquipmentsType, - HTTPException, - Stats, - StatsPercentage, - VaildateUIDError, - EnkaServerMaintanance, - EnkaServerUnknown, - EnkaServerRateLimit, - EnkaPlayerNotFound, -) -from pydantic import BaseModel -from telegram import InlineKeyboardButton, InlineKeyboardMarkup -from telegram.constants import ChatAction -from telegram.ext import CommandHandler, MessageHandler, filters -from telegram.helpers import create_deep_linked_url - -from core.config import config -from core.dependence.assets import DEFAULT_EnkaAssets, AssetsService -from core.dependence.redisdb import RedisDB -from core.handler.callbackqueryhandler import CallbackQueryHandler -from core.plugin import Plugin, handler -from core.services.players import PlayersService -from core.services.template.services import TemplateService -from metadata.shortname import roleToName -from modules.playercards.file import PlayerCardsFile -from modules.playercards.helpers import ArtifactStatsTheory -from utils.enkanetwork import RedisCache -from utils.helpers import download_resource -from utils.log import logger -from utils.patch.aiohttp import AioHttpTimeoutException - -if TYPE_CHECKING: - from enkanetwork import CharacterInfo, EquipmentsStats - from telegram.ext import ContextTypes - from telegram import Update - -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.assets_service = assets_service - self.template_service = template_service - self.kitsune: Optional[str] = None - - 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 AioHttpTimeoutException: - 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_history(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) - - @handler(CommandHandler, command="player_card", block=False) - @handler(MessageHandler, filters=filters.Regex("^角色卡片查询(.*)"), block=False) - async def player_cards(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> None: - user = update.effective_user - message = update.effective_message - args = self.get_args(context) - await message.reply_chat_action(ChatAction.TYPING) - player_info = await self.player_service.get_player(user.id) - if player_info is None: - buttons = [ - [ - InlineKeyboardButton( - "点我绑定账号", - url=create_deep_linked_url(context.bot.username, "set_uid"), - ) - ] - ] - if filters.ChatType.GROUPS.filter(message): - reply_message = await message.reply_text( - "未查询到您所绑定的账号信息,请先私聊派蒙绑定账号", - reply_markup=InlineKeyboardMarkup(buttons), - ) - self.add_delete_message_job(reply_message, delay=30) - - self.add_delete_message_job(message, delay=30) - else: - await message.reply_text("未查询到您所绑定的账号信息,请先绑定账号", reply_markup=InlineKeyboardMarkup(buttons)) - return - data = await self._load_history(player_info.player_id) - if data is None: - 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}|{player_info.player_id}", - ) - ] - ] - reply_message = await message.reply_photo( - photo=photo, - caption="角色列表未找到,请尝试点击下方按钮从 EnkaNetwork 更新角色列表", - reply_markup=InlineKeyboardMarkup(buttons), - ) - if reply_message.photo: - self.kitsune = reply_message.photo[-1].file_id - return - if len(args) == 1: - character_name = roleToName(args[0]) - logger.info( - "用户 %s[%s] 角色卡片查询命令请求 || character_name[%s] uid[%s]", - user.full_name, - user.id, - character_name, - player_info.player_id, - ) - else: - logger.info("用户 %s[%s] 角色卡片查询命令请求", user.full_name, user.id) - ttl = await self.cache.ttl(player_info.player_id) - - buttons = self.gen_button(data, user.id, player_info.player_id, 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 - for characters in data.characters: - if characters.name == character_name: - break - else: - await message.reply_text(f"角色展柜中未找到 {character_name} ,请检查角色是否存在于角色展柜中,或者等待角色数据更新后重试") - return - await message.reply_chat_action(ChatAction.UPLOAD_PHOTO) - render_result = await RenderTemplate( - player_info.player_id, characters, self.template_service - ).render() # pylint: disable=W0631 - await render_result.reply_photo( - message, - filename=f"player_card_{player_info.player_id}_{character_name}.png", - ) - - @handler(CallbackQueryHandler, 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) - await callback_query.answer(text="正在从 EnkaNetwork 获取角色列表 请不要重复点击按钮") - 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: - await message.delete() - await callback_query.answer("请先将角色加入到角色展柜并允许查看角色详情后再使用此功能,如果已经添加了角色,请等待角色数据更新后重试", show_alert=True) - return - 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.html", - render_data, - viewport={"width": 750, "height": 580}, - ttl=60 * 10, - caption="更新角色列表成功,请选择你要查询的角色", - ) - await holder.edit_media(message, reply_markup=InlineKeyboardMarkup(buttons)) - - @handler(CallbackQueryHandler, 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, - ) - data = await self._load_history(uid) - if isinstance(data, str): - await message.reply_text(data) - return - if data.characters is None: - await message.delete() - await callback_query.answer("请先将角色加入到角色展柜并允许查看角色详情后再使用此功能,如果已经添加了角色,请等待角色数据更新后重试", show_alert=True) - return - if page: - buttons = self.gen_button(data, user.id, uid, page, not 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 data.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.template_service).render() # pylint: disable=W0631 - render_result.filename = f"player_card_{uid}_{result}.png" - 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": 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", - template_service: TemplateService = None, - ): - self.uid = uid - self.template_service = template_service - # 因为需要替换线上 enka 图片地址为本地地址,先克隆数据,避免修改原数据 - self.character = character.copy(deep=True) - - 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": 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, - } - - # html = await self.template_service.render_async( - # "genshin/player_card/player_card.html", data - # ) - # logger.debug(html) - - return await self.template_service.render( - "genshin/player_card/player_card.html", - data, - {"width": 950, "height": 1080}, - full_page=True, - query_selector=".text-neutral-200", - ttl=7 * 24 * 60 * 60, - ) - - 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) - - 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 594b59b..0000000 --- a/plugins/genshin/quiz.py +++ /dev/null @@ -1,68 +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 - user = update.effective_user - 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): - logger.info("用户 %s[%s] 在群 %s[%s] 发送挑战问题命令请求", user.full_name, user.id, 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 863e66b..0000000 --- a/plugins/genshin/reg_time.py +++ /dev/null @@ -1,122 +0,0 @@ -from datetime import datetime - -from genshin import Client, GenshinException, InvalidCookies -from genshin.client.routes import InternationalRoute # noqa F401 -from genshin.utility import recognize_genshin_server, get_ds_headers -from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup -from telegram.constants import ParseMode -from telegram.ext import CallbackContext -from telegram.ext import filters -from telegram.helpers import create_deep_linked_url - -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, PlayerNotFoundError, CookiesNotFoundError -from utils.genshin import fetch_hk4e_token_by_cookie, recognize_genshin_game_biz -from utils.log import logger - -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 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: Client) -> str: - """获取原神注册时间""" - await fetch_hk4e_token_by_cookie(client) - url = REG_TIME_URL.get_url(client.region) - params = { - "game_biz": recognize_genshin_game_biz(client.uid), - "lang": "zh-cn", - "badge_uid": client.uid, - "badge_region": recognize_genshin_server(client.uid), - } - headers = get_ds_headers( - client.region, - params=params, - lang="zh-cn", - ) - data = await client.cookie_manager.request(url, method="GET", params=params, headers=headers) - if time := jsonlib.loads(data.get("data", "{}")).get("1", 0): - return datetime.fromtimestamp(time).strftime("%Y-%m-%d %H:%M:%S") - raise RegTimePlugin.NotFoundRegTimeError - - async def get_reg_time_from_cache(self, client: Client) -> str: - """从缓存中获取原神注册时间""" - if reg_time := await self.cache.get(f"{self.cache_key}{client.uid}"): - return reg_time.decode("utf-8") - reg_time = await self.get_reg_time(client) - await self.cache.set(f"{self.cache_key}{client.uid}", 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, context: CallbackContext) -> None: - message = update.effective_message - user = update.effective_user - logger.info("用户 %s[%s] 原神注册时间命令请求", user.full_name, user.id) - try: - client = await self.helper.get_genshin_client(user.id) - game_uid = client.uid - try: - reg_time = await self.get_reg_time_from_cache(client) - except InvalidCookies as exc: - await client.get_genshin_user(client.uid) - logger.warning("用户 %s[%s] 无法请求注册时间 API返回信息为 [%s]%s", user.full_name, user.id, exc.retcode, exc.original) - 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 - await message.reply_text(f"你的原神账号 [{game_uid}] 注册时间为:{reg_time}") - except (PlayerNotFoundError, CookiesNotFoundError): - buttons = [[InlineKeyboardButton("点我绑定账号", url=create_deep_linked_url(context.bot.username, "set_cookie"))]] - if filters.ChatType.GROUPS.filter(message): - reply_msg = await message.reply_text( - "此功能需要绑定cookie后使用,请先私聊派蒙绑定账号", - reply_markup=InlineKeyboardMarkup(buttons), - parse_mode=ParseMode.HTML, - ) - self.add_delete_message_job(reply_msg, delay=30) - self.add_delete_message_job(message, delay=30) - else: - await message.reply_text( - "此功能需要绑定cookie后使用,请先私聊派蒙进行绑定", - parse_mode=ParseMode.HTML, - reply_markup=InlineKeyboardMarkup(buttons), - ) - except GenshinException as exc: - if exc.retcode == -501101: - await message.reply_text("当前角色冒险等阶未达到10级,暂时无法获取信息") - else: - raise exc - except RegTimePlugin.NotFoundRegTimeError: - await message.reply_text("未找到你的原神账号 [{game_uid}] 注册时间,仅限 2022 年 10 月 之前注册的账号") - - class NotFoundRegTimeError(Exception): - """未找到注册时间""" diff --git a/plugins/genshin/stats.py b/plugins/genshin/stats.py deleted file mode 100644 index 89a3ae4..0000000 --- a/plugins/genshin/stats.py +++ /dev/null @@ -1,138 +0,0 @@ -import random -from typing import Optional - -from genshin import Client, GenshinException -from genshin.models import GenshinUserStats -from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup -from telegram.constants import ChatAction -from telegram.ext import CallbackContext, filters -from telegram.helpers import create_deep_linked_url - -from core.plugin import Plugin, handler -from core.services.cookies.error import TooManyRequestPublicCookies -from core.services.template.models import RenderResult -from core.services.template.services import TemplateService -from plugins.tools.genshin import GenshinHelper, PlayerNotFoundError, CookiesNotFoundError -from utils.log import logger - -__all__ = ("PlayerStatsPlugins",) - - -class PlayerStatsPlugins(Plugin): - """玩家统计查询""" - - def __init__( - self, - template: TemplateService, - helper: GenshinHelper, - ): - self.template_service = template - self.helper = helper - - @handler.command("stats", block=False) - @handler.message(filters.Regex("^玩家统计查询(.*)"), block=False) - async def command_start(self, update: Update, context: CallbackContext) -> Optional[int]: - user = update.effective_user - message = update.effective_message - logger.info("用户 %s[%s] 查询游戏用户命令请求", user.full_name, user.id) - uid: Optional[int] = None - try: - args = context.args - if args is not None and len(args) >= 1: - uid = int(args[0]) - except ValueError as exc: - logger.warning("获取 uid 发生错误! 错误信息为 %s", str(exc)) - await message.reply_text("输入错误") - return - try: - try: - client = await self.helper.get_genshin_client(user.id) - except CookiesNotFoundError: - client, uid = await self.helper.get_public_genshin_client(user.id) - render_result = await self.render(client, uid) - except PlayerNotFoundError: - buttons = [[InlineKeyboardButton("点我绑定账号", url=create_deep_linked_url(context.bot.username, "set_cookie"))]] - if filters.ChatType.GROUPS.filter(message): - reply_message = await message.reply_text( - "未查询到您所绑定的账号信息,请先私聊派蒙绑定账号", reply_markup=InlineKeyboardMarkup(buttons) - ) - self.add_delete_message_job(reply_message, delay=30) - self.add_delete_message_job(message, delay=30) - else: - await message.reply_text("未查询到您所绑定的账号信息,请先绑定账号", reply_markup=InlineKeyboardMarkup(buttons)) - return - except GenshinException as exc: - if exc.retcode == 1034 and uid: - await message.reply_text("出错了呜呜呜 ~ 请稍后重试") - return - raise exc - except TooManyRequestPublicCookies: - await message.reply_text("用户查询次数过多 请稍后重试") - return - except AttributeError as exc: - logger.error("角色数据有误") - logger.exception(exc) - await message.reply_text("角色数据有误 估计是派蒙晕了") - return - await message.reply_chat_action(ChatAction.UPLOAD_PHOTO) - await render_result.reply_photo(message, filename=f"{client.uid}.png", allow_sending_without_reply=True) - - async def render(self, client: Client, uid: Optional[int] = None) -> RenderResult: - if uid is None: - uid = client.uid - - user_info = await client.get_genshin_user(uid) - logger.debug(user_info) - - # 因为需要替换线上图片地址为本地地址,先克隆数据,避免修改原数据 - user_info = user_info.copy(deep=True) - - data = { - "uid": uid, - "info": user_info.info, - "stats": user_info.stats, - "explorations": user_info.explorations, - "teapot": user_info.teapot, - "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"), - ], - "style": random.choice(["mondstadt", "liyue"]), # nosec - } - - # html = await self.template_service.render_async( - # "genshin/stats/stats.html", data - # ) - # logger.debug(html) - - await self.cache_images(user_info) - - return await self.template_service.render( - "genshin/stats/stats.html", - data, - {"width": 650, "height": 800}, - 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) diff --git a/plugins/genshin/weapon.py b/plugins/genshin/weapon.py deleted file mode 100644 index edfa983..0000000 --- a/plugins/genshin/weapon.py +++ /dev/null @@ -1,143 +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 - user = update.effective_user - 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) - logger.info("用户 %s[%s] 查询角色攻略命令请求 weapon_name[%s]", user.full_name, user.id, 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": await self.download_resource(_weapon_data.weapon_type.icon_url()), - "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": await self.download_resource(_weapon_data.weapon_type.icon_url()), - "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.html", 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", - allow_sending_without_reply=True, - ) - 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 298b3e1..0000000 --- a/plugins/genshin/wish.py +++ /dev/null @@ -1,324 +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.gacha.banner import BannerType, GachaBanner -from modules.gacha.player.info import PlayerGachaInfo -from modules.gacha.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:gacha:" - - 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) - - -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 = BannerType.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 = BannerType.WEAPON - banner.weight4 = ((1, 600), (7, 600), (10, 10000)) - banner.weight5 = ((1, 70), (62, 70), (90, 10000)) - else: - banner.banner_type = BannerType.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: - message = update.effective_message - user = update.effective_user - 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 - logger.info("用户 %s[%s] 抽卡模拟器命令请求 || 参数 %s", user.full_name, user.id, 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 == BannerType.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"{user.full_name}", - "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/gacha/gacha.html", 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 a5458b9..0000000 --- a/plugins/genshin/wish_log.py +++ /dev/null @@ -1,413 +0,0 @@ -from io import BytesIO - -import genshin -from aiofiles import open as async_open -from genshin.models import BannerType -from telegram import Document, InlineKeyboardButton, InlineKeyboardMarkup, Message, Update, User -from telegram.constants import ChatAction -from telegram.ext import CallbackContext, CommandHandler, ConversationHandler, MessageHandler, 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 metadata.scripts.paimon_moe import GACHA_LOG_PAIMON_MOE_PATH, update_paimon_moe_zh -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 plugins.tools.genshin import PlayerNotFoundError, GenshinHelper -from utils.genshin import get_authkey_by_stoken -from utils.log import logger - -try: - import ujson as jsonlib - -except ImportError: - import json as jsonlib - -INPUT_URL, INPUT_FILE, CONFIRM_DELETE = range(10100, 10103) - - -class WishLogPlugin(Plugin.Conversation): - """抽卡记录导入/导出/分析""" - - def __init__( - self, - template_service: TemplateService, - players_service: PlayersService, - assets: AssetsService, - cookie_service: CookiesService, - helper: GenshinHelper, - ): - 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.helper = helper - - 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 _refresh_user_data( - self, user: User, data: dict = None, authkey: str = None, verify_uid: bool = True - ) -> str: - """刷新用户数据 - :param user: 用户 - :param data: 数据 - :param authkey: 认证密钥 - :return: 返回信息 - """ - try: - logger.debug("尝试获取已绑定的原神账号") - client = await self.helper.get_genshin_client(user.id, need_cookie=False) - if authkey: - new_num = await self.gacha_log.get_gacha_log_data(user.id, client, authkey) - return "更新完成,本次没有新增数据" if new_num == 0 else f"更新完成,本次共新增{new_num}条抽卡记录" - if data: - new_num = await self.gacha_log.import_gacha_log_data(user.id, client, data, verify_uid) - return "更新完成,本次没有新增数据" if new_num == 0 else f"更新完成,本次共新增{new_num}条抽卡记录" - except GachaLogNotFound: - return "派蒙没有找到你的抽卡记录,快来私聊派蒙导入吧~" - 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 "派蒙没有找到您所绑定的账号信息,请先私聊派蒙绑定账号" - - async def import_from_file(self, user: User, message: Message, document: 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 > 2 * 1024 * 1024: - await message.reply_text("文件过大,请发送小于 2 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("文件解析失败,请检查文件是否符合 UIGF 标准") - return - except (KeyError, IndexError, ValueError): - await message.reply_text("文件解析失败,请检查文件编码是否正确或符合 UIGF 标准") - return - except Exception as exc: - logger.error("文件解析失败 %s", repr(exc)) - await message.reply_text("文件解析失败,请检查文件是否符合 UIGF 标准") - 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, data=data, verify_uid=file_type == "json") - except Exception as exc: # pylint: disable=W0703 - logger.error("文件解析失败 %s", repr(exc)) - text = "文件解析失败,请检查文件是否符合 UIGF 标准" - await reply.edit_text(text) - - @conversation.entry_point - @handler(CommandHandler, command="gacha_log_import", filters=filters.ChatType.PRIVATE, block=False) - @handler(MessageHandler, filters=filters.Regex("^导入抽卡记录(.*)") & filters.ChatType.PRIVATE, block=False) - async def command_start(self, update: Update, context: CallbackContext) -> int: - message = update.effective_message - user = update.effective_user - args = self.get_args(context) - logger.info("用户 %s[%s] 导入抽卡记录命令请求", user.full_name, user.id) - authkey = from_url_get_authkey(args[0] if args else "") - if not args: - 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 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 - client = genshin.Client( - cookies=cookies.data, - game=genshin.types.Game.GENSHIN, - region=genshin.Region.CHINESE, - lang="zh-cn", - uid=player_info.player_id, - ) - authkey = await get_authkey_by_stoken(client) - if not authkey: - await message.reply_text( - "开始导入祈愿历史记录:请通过 https://paimon.moe/wish/import 获取抽卡记录链接后发送给我" - "(非 paimon.moe 导出的文件数据)\n\n" - "> 你还可以向派蒙发送从其他工具导出的 UIGF 标准的记录文件\n" - "> 或者从 paimon.moe 、非小酋 导出的 xlsx 记录文件\n" - "> 在绑定 Cookie 时添加 stoken 可能有特殊效果哦(仅限国服)\n" - "注意:导入的数据将会与旧数据进行合并。", - parse_mode="html", - ) - return INPUT_URL - text = "小派蒙正在从服务器获取数据,请稍后" - if not args: - text += "\n\n> 由于你绑定的 Cookie 中存在 stoken ,本次通过 stoken 自动刷新数据" - reply = await message.reply_text(text) - 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, _: CallbackContext) -> int: - message = update.effective_message - user = update.effective_user - if message.document: - await self.import_from_file(user, message) - return ConversationHandler.END - if not message.text: - await message.reply_text("请发送文件或链接") - return INPUT_URL - authkey = from_url_get_authkey(message.text) - reply = await message.reply_text("小派蒙正在从服务器获取数据,请稍后") - 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="gacha_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: CallbackContext) -> int: - message = update.effective_message - user = update.effective_user - logger.info("用户 %s[%s] 删除抽卡记录命令请求", user.full_name, user.id) - try: - client = await self.helper.get_genshin_client(user.id, need_cookie=False) - context.chat_data["uid"] = client.uid - except PlayerNotFoundError: - logger.info("未查询到用户 %s[%s] 所绑定的账号信息", user.full_name, user.id) - await message.reply_text("未查询到您所绑定的账号信息,请先绑定账号") - return ConversationHandler.END - _, status = await self.gacha_log.load_history_info(str(user.id), str(client.uid), 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: CallbackContext) -> 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(CommandHandler, command="gacha_log_force_delete", block=False, admin=True) - async def command_gacha_log_force_delete(self, update: Update, context: CallbackContext): - 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") - client = await self.helper.get_genshin_client(cid, need_cookie=False) - _, status = await self.gacha_log.load_history_info(str(cid), str(client.uid), only_status=True) - if not status: - await message.reply_text("该用户还没有导入抽卡记录") - return - status = await self.gacha_log.remove_history_info(str(cid), str(client.uid)) - 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(CommandHandler, command="gacha_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: CallbackContext) -> None: - message = update.effective_message - user = update.effective_user - logger.info("用户 %s[%s] 导出抽卡记录命令请求", user.full_name, user.id) - try: - client = await self.helper.get_genshin_client(user.id, need_cookie=False) - await message.reply_chat_action(ChatAction.TYPING) - path = await self.gacha_log.gacha_log_to_uigf(str(user.id), str(client.uid)) - await message.reply_chat_action(ChatAction.UPLOAD_DOCUMENT) - await message.reply_document(document=open(path, "rb+"), caption="抽卡记录导出文件 - UIGF V2.2") - 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("派蒙没有找到你的抽卡记录,快来私聊派蒙导入吧~", 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("未查询到您所绑定的账号信息,请先绑定账号") - - @handler(CommandHandler, command="gacha_log", block=False) - @handler(MessageHandler, filters=filters.Regex("^抽卡记录?(武器|角色|常驻|)$"), block=False) - async def command_start_analysis(self, update: Update, context: CallbackContext) -> None: - message = update.effective_message - user = update.effective_user - pool_type = BannerType.CHARACTER1 - if args := self.get_args(context): - if "武器" in args: - pool_type = BannerType.WEAPON - elif "常驻" in args: - pool_type = BannerType.STANDARD - logger.info("用户 %s[%s] 抽卡记录命令请求 || 参数 %s", user.full_name, user.id, pool_type.name) - try: - client = await self.helper.get_genshin_client(user.id, need_cookie=False) - await message.reply_chat_action(ChatAction.TYPING) - data = await self.gacha_log.get_analysis(user.id, client, pool_type, self.assets_service) - if isinstance(data, str): - reply_message = await message.reply_text(data) - if filters.ChatType.GROUPS.filter(message): - self.add_delete_message_job(reply_message, delay=300) - self.add_delete_message_job(message, delay=300) - else: - await message.reply_chat_action(ChatAction.UPLOAD_PHOTO) - png_data = await self.template_service.render( - "genshin/gacha_log/gacha_log.html", - data, - full_page=True, - file_type=FileType.DOCUMENT if len(data.get("fiveLog")) > 36 else FileType.PHOTO, - query_selector=".body_box", - ) - if png_data.file_type == FileType.DOCUMENT: - await png_data.reply_document(message, filename="抽卡记录.png") - else: - await png_data.reply_photo(message) - 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("派蒙没有找到你的抽卡记录,快来点击按钮私聊派蒙导入吧~", reply_markup=InlineKeyboardMarkup(buttons)) - except PlayerNotFoundError: - logger.info("未查询到用户 %s[%s] 所绑定的账号信息", user.full_name, user.id) - buttons = [[InlineKeyboardButton("点我绑定账号", url=create_deep_linked_url(context.bot.username, "set_uid"))]] - if filters.ChatType.GROUPS.filter(message): - reply_message = await message.reply_text( - "未查询到您所绑定的账号信息,请先私聊派蒙绑定账号", reply_markup=InlineKeyboardMarkup(buttons) - ) - self.add_delete_message_job(reply_message, delay=30) - self.add_delete_message_job(message, delay=30) - else: - await message.reply_text("未查询到您所绑定的账号信息,请先绑定账号", reply_markup=InlineKeyboardMarkup(buttons)) - - @handler(CommandHandler, command="gacha_count", block=False) - @handler(MessageHandler, filters=filters.Regex("^抽卡统计?(武器|角色|常驻|仅五星|)$"), block=False) - async def command_start_count(self, update: Update, context: CallbackContext) -> None: - message = update.effective_message - user = update.effective_user - pool_type = BannerType.CHARACTER1 - all_five = False - if args := self.get_args(context): - if "武器" in args: - pool_type = BannerType.WEAPON - elif "常驻" in args: - pool_type = BannerType.STANDARD - elif "仅五星" in args: - all_five = True - logger.info("用户 %s[%s] 抽卡统计命令请求 || 参数 %s || 仅五星 %s", user.full_name, user.id, pool_type.name, all_five) - try: - client = await self.helper.get_genshin_client(user.id, need_cookie=False) - group = filters.ChatType.GROUPS.filter(message) - await message.reply_chat_action(ChatAction.TYPING) - if all_five: - data = await self.gacha_log.get_all_five_analysis(user.id, client, self.assets_service) - else: - data = await self.gacha_log.get_pool_analysis(user.id, client, pool_type, self.assets_service, group) - if isinstance(data, str): - reply_message = await message.reply_text(data) - if filters.ChatType.GROUPS.filter(message): - self.add_delete_message_job(reply_message) - self.add_delete_message_job(message) - else: - document = False - if data["hasMore"] and not group: - document = True - data["hasMore"] = False - await message.reply_chat_action(ChatAction.UPLOAD_DOCUMENT if document else ChatAction.UPLOAD_PHOTO) - png_data = await self.template_service.render( - "genshin/gacha_count/gacha_count.html", - data, - full_page=True, - query_selector=".body_box", - file_type=FileType.DOCUMENT if document else FileType.PHOTO, - ) - if document: - await png_data.reply_document(message, filename="抽卡统计.png") - else: - await png_data.reply_photo(message) - 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("派蒙没有找到你的抽卡记录,快来私聊派蒙导入吧~", reply_markup=InlineKeyboardMarkup(buttons)) - except PlayerNotFoundError: - logger.info("未查询到用户 %s[%s] 所绑定的账号信息", user.full_name, user.id) - buttons = [[InlineKeyboardButton("点我绑定账号", url=create_deep_linked_url(context.bot.username, "set_uid"))]] - if filters.ChatType.GROUPS.filter(message): - reply_message = await message.reply_text( - "未查询到您所绑定的账号信息,请先私聊派蒙绑定账号", reply_markup=InlineKeyboardMarkup(buttons) - ) - self.add_delete_message_job(reply_message, delay=30) - - self.add_delete_message_job(message, delay=30) - else: - await message.reply_text("未查询到您所绑定的账号信息,请先绑定账号", reply_markup=InlineKeyboardMarkup(buttons)) diff --git a/plugins/group/README.md b/plugins/group/README.md new file mode 100644 index 0000000..e69de29 diff --git a/plugins/group/captcha.py b/plugins/group/captcha.py deleted file mode 100644 index beec1e9..0000000 --- a/plugins/group/captcha.py +++ /dev/null @@ -1,491 +0,0 @@ -import asyncio -import random -import time -from typing import Tuple, Union, Optional, TYPE_CHECKING, List - -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 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 - -FullChatPermissions = ChatPermissions( - can_send_messages=True, - can_send_media_messages=True, - can_send_polls=True, - can_send_other_messages=True, - can_add_web_page_previews=True, - can_change_info=True, - can_invite_users=True, - can_pin_messages=True, -) - - -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 - - 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 get_chat_administrators( - self, context: "ContextTypes.DEFAULT_TYPE", chat_id: Union[str, int] - ) -> Tuple[ChatMember]: - qname = f"plugin:group_captcha:chat_administrators:{chat_id}" - result: "List[bytes]" = await self.cache.lrange(qname, 0, -1) - if len(result) > 0: - return ChatMember.de_list([jsonlib.loads(str(_data, encoding="utf-8")) for _data in result], context.bot) - chat_administrators = await context.bot.get_chat_administrators(chat_id) - async with self.cache.pipeline(transaction=True) as pipe: - for chat_administrator in chat_administrators: - await pipe.lpush(qname, chat_administrator.to_json()) - await pipe.expire(qname, self.ttl) - await pipe.execute() - return chat_administrators - - @staticmethod - def is_admin(chat_administrators: Tuple[ChatMember], user_id: int) -> bool: - return any(admin.user.id == user_id for admin in chat_administrators) - - 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=FullChatPermissions) - 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 self.get_chat_administrators(context, chat_id=chat.id) - if not self.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" + config.notice.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, "派蒙这边收到了错误的消息!请检查详细日记!") - 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" + config.notice.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 len(config.verify_groups) >= 1: - for verify_group in config.verify_groups: - if verify_group == chat.id: - break - else: - return - else: - 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) - - @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 len(config.verify_groups) >= 1: - for verify_group in config.verify_groups: - if verify_group == chat.id: - break - else: - return - else: - 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 self.get_chat_administrators(context, chat_id=chat.id) - if self.is_admin(chat_administrators, from_user.id): - await chat.send_message("派蒙检测到管理员邀请,自动放行了!") - 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"派蒙无法修改 {user.mention_html()} 的权限!请检查是否给派蒙授权管理了", - parse_mode=ParseMode.HTML, - ) - return - raise exc - new_chat_members_message = await self.get_new_chat_members_message(user, context) - 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}", - ), - ] - ) - if new_chat_members_message: - reply_message = ( - f"*欢迎来到「提瓦特」世界!* \n" - f"问题: {escape_markdown(question.text, version=2)} \n" - f"请在*{self.time_out}*秒内回答问题" - ) - else: - 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: - if new_chat_members_message: - question_message = await new_chat_members_message.reply_markdown_v2( - reply_message, reply_markup=InlineKeyboardMarkup(buttons), allow_sending_without_reply=True - ) - else: - 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("派蒙分心了一下,不小心忘记你了,你只能先退出群再重新进来吧。") - 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"}, - ) - if new_chat_members_message: - context.job_queue.run_once( - callback=self.clean_message_job, - when=self.time_out, - data=new_chat_members_message.message_id, - name=f"{chat.id}|{user.id}|auth_clean_join_message", - chat_id=chat.id, - user_id=user.id, - job_kwargs={"replace_existing": True, "id": f"{chat.id}|{user.id}|auth_clean_join_message"}, - ) - 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: - 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/sign.py b/plugins/jobs/sign.py index e026d05..890412a 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.starrail.sign import SignSystem from plugins.tools.sign import SignJobType from utils.log import logger diff --git a/plugins/genshin/help.py b/plugins/starrail/help.py similarity index 100% rename from plugins/genshin/help.py rename to plugins/starrail/help.py diff --git a/plugins/genshin/sign.py b/plugins/starrail/sign.py similarity index 98% rename from plugins/genshin/sign.py rename to plugins/starrail/sign.py index 4a223ae..3ffae12 100644 --- a/plugins/genshin/sign.py +++ b/plugins/starrail/sign.py @@ -39,7 +39,7 @@ class Sign(Plugin): try: await self.genshin_helper.get_genshin_client(user_id) except (PlayerNotFoundError, CookiesNotFoundError): - return "未查询到账号信息,请先私聊派蒙绑定账号" + return "未查询到账号信息,请先私聊彦卿绑定账号" user: SignUser = await self.sign_service.get_by_user_id(user_id) if user: if method == "关闭": @@ -114,7 +114,7 @@ class Sign(Plugin): buttons = [[InlineKeyboardButton("点我绑定账号", url=create_deep_linked_url(context.bot.username, "set_cookie"))]] if filters.ChatType.GROUPS.filter(message): reply_message = await message.reply_text( - "未查询到您所绑定的账号信息,请先私聊派蒙绑定账号", reply_markup=InlineKeyboardMarkup(buttons) + "未查询到您所绑定的账号信息,请先私聊彦卿绑定账号", reply_markup=InlineKeyboardMarkup(buttons) ) self.add_delete_message_job(reply_message, delay=30) diff --git a/plugins/genshin/strategy.py b/plugins/starrail/strategy.py similarity index 56% rename from plugins/genshin/strategy.py rename to plugins/starrail/strategy.py index 23f7341..944db71 100644 --- a/plugins/genshin/strategy.py +++ b/plugins/starrail/strategy.py @@ -6,6 +6,7 @@ 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 core.services.wiki.services import WikiService from metadata.shortname import roleToName, roleToTag from utils.log import logger @@ -17,10 +18,12 @@ class StrategyPlugin(Plugin): def __init__( self, - game_strategy_service: GameStrategyService = None, + cache_service: GameStrategyService = None, + wiki_service: WikiService = None, search_service: SearchServices = None, ): - self.game_strategy_service = game_strategy_service + self.cache_service = cache_service + self.wiki_service = wiki_service self.search_service = search_service @handler.command(command="strategy", block=False) @@ -38,8 +41,8 @@ class StrategyPlugin(Plugin): 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 == "": + file_path = self.wiki_service.raider.raider_path / f"{character_name}.png" + if not file_path.exists(): reply_message = await message.reply_text( f"没有找到 {character_name} 的攻略", reply_markup=InlineKeyboardMarkup(self.KEYBOARD) ) @@ -49,25 +52,35 @@ class StrategyPlugin(Plugin): return logger.info("用户 %s[%s] 查询角色攻略命令请求 || 参数 %s", user.full_name, user.id, 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", - allow_sending_without_reply=True, - 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 = "From 米游社" + if file_id := await self.cache_service.get_strategy_cache(character_name): + await message.reply_document( + document=file_id, caption=caption, - parse_mode="HTML", - photo_file_id=photo_file_id, + filename=f"{character_name}.png", + allow_sending_without_reply=True, + parse_mode=ParseMode.HTML, ) - await self.search_service.add_entry(entry) + else: + reply_document = await message.reply_document( + document=open(file_path, "rb"), + caption=caption, + filename=f"{character_name}.png", + allow_sending_without_reply=True, + parse_mode=ParseMode.HTML, + ) + if reply_document.document: + tags = roleToTag(character_name) + photo_file_id = reply_document.document.file_id + cid = self.wiki_service.raider.get_item_id(character_name) + await self.cache_service.set_strategy_cache(cid, photo_file_id) + entry = StrategyEntry( + key=f"plugin:strategy:{character_name}", + title=character_name, + description=f"{character_name} 角色攻略", + tags=tags, + caption=caption, + parse_mode="HTML", + document_file_id=photo_file_id, + ) + await self.search_service.add_entry(entry) diff --git a/plugins/genshin/verify.py b/plugins/starrail/verify.py similarity index 100% rename from plugins/genshin/verify.py rename to plugins/starrail/verify.py diff --git a/plugins/system/chat_member.py b/plugins/system/chat_member.py index 44715ae..2a0ce17 100644 --- a/plugins/system/chat_member.py +++ b/plugins/system/chat_member.py @@ -75,7 +75,7 @@ class ChatMember(Plugin): quit_status = True if quit_status: try: - await context.bot.send_message(chat.id, "派蒙不想进去!不是旅行者的邀请!") + await context.bot.send_message(chat.id, "彦卿不想进去!不是开拓者的邀请!") except Forbidden as exc: logger.info("发送消息失败 %s", exc.message) except NetworkError as exc: @@ -85,7 +85,7 @@ class ChatMember(Plugin): await context.bot.leave_chat(chat.id) else: try: - await context.bot.send_message(chat.id, "感谢邀请小派蒙到本群!请使用 /help 查看咱已经学会的功能。") + await context.bot.send_message(chat.id, "感谢邀请小彦卿到本群!请使用 /help 查看咱已经学会的功能。") except Forbidden as exc: logger.info("发送消息失败 %s", exc.message) except NetworkError as exc: diff --git a/plugins/system/errorhandler.py b/plugins/system/errorhandler.py index d08c711..9a5d681 100644 --- a/plugins/system/errorhandler.py +++ b/plugins/system/errorhandler.py @@ -275,7 +275,7 @@ class ErrorHandler(Plugin): chat.id, update.update_id, ) - text = "出错了呜呜呜 ~ 派蒙这边发生了点问题无法处理!" + text = "出错了呜呜呜 ~ 彦卿这边发生了点问题无法处理!" await context.bot.send_message( effective_message.chat_id, text, reply_markup=ReplyKeyboardRemove(), parse_mode=ParseMode.HTML ) diff --git a/plugins/tools/genshin.py b/plugins/tools/genshin.py index d3b09ea..d221f31 100644 --- a/plugins/tools/genshin.py +++ b/plugins/tools/genshin.py @@ -259,7 +259,7 @@ class GenshinHelper(Plugin): client = genshin.Client( cookies, lang="zh-cn", - game=genshin.types.Game.GENSHIN, + game=genshin.types.Game.STARRAIL, region=game_region, uid=uid, hoyolab_id=player.account_id, @@ -286,7 +286,7 @@ class GenshinHelper(Plugin): raise TypeError("Region is not `RegionEnum.NULL`") client = genshin.Client( - cookies.data, region=game_region, uid=uid, game=genshin.types.Game.GENSHIN, lang="zh-cn" + cookies.data, region=game_region, uid=uid, game=genshin.types.Game.STARRAIL, lang="zh-cn" ) if self.genshin_cache is not None: diff --git a/plugins/tools/sign.py b/plugins/tools/sign.py index b6ab306..1678972 100644 --- a/plugins/tools/sign.py +++ b/plugins/tools/sign.py @@ -117,14 +117,14 @@ class SignSystem(Plugin): else: await asyncio.sleep(random.randint(0, 3)) # nosec try: - rewards = await client.get_monthly_rewards(game=Game.GENSHIN, lang="zh-cn") + rewards = await client.get_monthly_rewards(game=Game.STARRAIL, lang="zh-cn") except GenshinException as error: logger.warning("UID[%s] 获取签到信息失败,API返回信息为 %s", client.uid, str(error)) if is_raise: raise error return f"获取签到信息失败,API返回信息为 {str(error)}" try: - daily_reward_info = await client.get_reward_info(game=Game.GENSHIN, lang="zh-cn") # 获取签到信息失败 + daily_reward_info = await client.get_reward_info(game=Game.STARRAIL, lang="zh-cn") # 获取签到信息失败 except GenshinException as error: logger.warning("UID[%s] 获取签到状态失败,API返回信息为 %s", client.uid, str(error)) if is_raise: @@ -137,7 +137,7 @@ class SignSystem(Plugin): request_daily_reward = await client.request_daily_reward( "sign", method="POST", - game=Game.GENSHIN, + game=Game.STARRAIL, lang="zh-cn", challenge=challenge, validate=validate, @@ -158,7 +158,7 @@ class SignSystem(Plugin): request_daily_reward = await client.request_daily_reward( "sign", method="POST", - game=Game.GENSHIN, + game=Game.STARRAIL, lang="zh-cn", challenge=challenge, validate=validate, @@ -178,7 +178,7 @@ class SignSystem(Plugin): _request_daily_reward = await client.request_daily_reward( "sign", method="POST", - game=Game.GENSHIN, + game=Game.STARRAIL, lang="zh-cn", ) logger.debug("request_daily_reward 返回\n%s", _request_daily_reward) @@ -192,7 +192,7 @@ class SignSystem(Plugin): request_daily_reward = await client.request_daily_reward( "sign", method="POST", - game=Game.GENSHIN, + game=Game.STARRAIL, lang="zh-cn", challenge=_challenge, validate=_validate, @@ -210,7 +210,7 @@ class SignSystem(Plugin): logger.success("UID[%s] 通过 recognize 签到成功", client.uid) else: request_daily_reward = await client.request_daily_reward( - "sign", method="POST", game=Game.GENSHIN, lang="zh-cn" + "sign", method="POST", game=Game.STARRAIL, lang="zh-cn" ) gt = request_daily_reward.get("gt", "") challenge = request_daily_reward.get("challenge", "") @@ -218,7 +218,7 @@ class SignSystem(Plugin): raise NeedChallenge(uid=client.uid, gt=gt, challenge=challenge) else: request_daily_reward = await client.request_daily_reward( - "sign", method="POST", game=Game.GENSHIN, lang="zh-cn" + "sign", method="POST", game=Game.STARRAIL, lang="zh-cn" ) gt = request_daily_reward.get("gt", "") challenge = request_daily_reward.get("challenge", "") @@ -235,7 +235,7 @@ class SignSystem(Plugin): logger.warning("UID[%s] 已经签到", client.uid) if is_raise: raise error - result = "今天旅行者已经签到过了~" + result = "今天开拓者已经签到过了~" except GenshinException as error: logger.warning("UID %s 签到失败,API返回信息为 %s", client.uid, str(error)) if is_raise: @@ -245,7 +245,7 @@ class SignSystem(Plugin): result = "OK" else: logger.info("UID[%s] 已经签到", client.uid) - result = "今天旅行者已经签到过了~" + result = "今天开拓者已经签到过了~" logger.info("UID[%s] 签到结果 %s", client.uid, result) reward = rewards[daily_reward_info.claimed_rewards - (1 if daily_reward_info.signed_in else 0)] today = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) @@ -289,7 +289,7 @@ class SignSystem(Plugin): text = "自动签到执行失败,Cookie无效" sign_db.status = SignStatusEnum.INVALID_COOKIES except AlreadyClaimed: - text = "今天旅行者已经签到过了~" + text = "今天开拓者已经签到过了~" sign_db.status = SignStatusEnum.ALREADY_CLAIMED except GenshinException as exc: text = f"自动签到执行失败,API返回信息为 {str(exc)}" diff --git a/resources/bot/help/background/1006.png b/resources/bot/help/background/1006.png deleted file mode 100644 index a7aa184..0000000 Binary files a/resources/bot/help/background/1006.png and /dev/null differ diff --git a/resources/bot/help/background/2015.png b/resources/bot/help/background/2015.png deleted file mode 100644 index c9500c1..0000000 Binary files a/resources/bot/help/background/2015.png and /dev/null differ diff --git a/resources/bot/help/background/2020021114213984258.jpg b/resources/bot/help/background/2020021114213984258.jpg deleted file mode 100644 index 76400a0..0000000 Binary files a/resources/bot/help/background/2020021114213984258.jpg and /dev/null differ diff --git a/resources/bot/help/background/2020021114213984258.png b/resources/bot/help/background/2020021114213984258.png deleted file mode 100644 index c9815ee..0000000 Binary files a/resources/bot/help/background/2020021114213984258.png and /dev/null differ diff --git a/resources/bot/help/background/genshin.png b/resources/bot/help/background/genshin.png deleted file mode 100644 index a8cb922..0000000 Binary files a/resources/bot/help/background/genshin.png and /dev/null differ diff --git a/resources/bot/help/background/header.png b/resources/bot/help/background/header.png new file mode 100644 index 0000000..887308d Binary files /dev/null and b/resources/bot/help/background/header.png differ diff --git a/resources/bot/help/help.css b/resources/bot/help/help.css index c004e92..afd6b8b 100644 --- a/resources/bot/help/help.css +++ b/resources/bot/help/help.css @@ -10,7 +10,7 @@ body { } .header { - background-image: url(background/2020021114213984258.png); + background-image: url(background/header.png); box-shadow: 0 2px 4px rgb(0 0 0 / 10%); } diff --git a/resources/bot/help/help.html b/resources/bot/help/help.html index 69400a4..70484b5 100644 --- a/resources/bot/help/help.html +++ b/resources/bot/help/help.html @@ -11,7 +11,7 @@
-

TGPaimonBot

+

StarRailBot

使用说明

@@ -25,106 +25,106 @@
-
- -
+ + +

查询命令

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

其他命令

-
-
/wish
-
抽卡模拟器(非洲人模拟器)
-
-
-
/set_wish
-
抽卡模拟器定轨
-
-
-
/quiz
-
- 派蒙的十万个为什么 -
-
- -
-
/gacha_log_import
-
导入抽卡记录
-
-
-
/gacha_log_export
-
导出抽卡记录
-
-
-
/gacha_log_delete
-
删除抽卡记录
-
- -
-
/pay_log_import
-
导入充值记录
-
-
-
/pay_log_export
-
导出充值记录
-
-
-
/pay_log_delete
-
删除充值记录
-
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -
-
/setuid
-
添加/重设UID(请私聊BOT)
-
+ + + +
/setcookie
添加/重设Cookie(请私聊BOT)
@@ -239,10 +239,10 @@
@{{bot_username}} 角色名
查询角色攻略
-
-
@{{bot_username}} 武器名
-
查询武器信息
-
+ + + +
diff --git a/tests/unit/test_abyss_team_data.py b/tests/unit/test_abyss_team_data.py deleted file mode 100644 index 7cdc3e7..0000000 --- a/tests/unit/test_abyss_team_data.py +++ /dev/null @@ -1,42 +0,0 @@ -import logging - -import pytest -import pytest_asyncio -from flaky import flaky - -from modules.apihelper.client.components.abyss import AbyssTeam -from modules.apihelper.models.genshin.abyss import TeamRateResult, TeamRate, FullTeamRate - -LOGGER = logging.getLogger(__name__) - - -@pytest_asyncio.fixture -async def abyss_team_data(): - _abyss_team_data = AbyssTeam() - yield _abyss_team_data - await _abyss_team_data.close() - - -# noinspection PyShadowingNames -@pytest.mark.asyncio -@flaky(3, 1) -async def test_abyss_team_data(abyss_team_data: AbyssTeam): - team_data = await abyss_team_data.get_data() - assert isinstance(team_data, TeamRateResult) - assert isinstance(team_data.rate_list_up[0], TeamRate) - assert isinstance(team_data.rate_list_up[-1], TeamRate) - assert isinstance(team_data.rate_list_down[0], TeamRate) - assert isinstance(team_data.rate_list_down[-1], TeamRate) - assert team_data.user_count > 0 - team_data.sort(["迪奥娜", "芭芭拉", "凯亚", "琴"]) - assert isinstance(team_data.rate_list_full[0], FullTeamRate) - assert isinstance(team_data.rate_list_full[-1], FullTeamRate) - random_team = team_data.random_team()[0] - assert isinstance(random_team, FullTeamRate) - member_up = {i.name for i in random_team.up.formation} - member_down = {i.name for i in random_team.down.formation} - assert not member_up & member_down - for i in team_data.rate_list_full[0].down.formation: - LOGGER.info("rate down info:name %s star %s", i.name, i.star) - for i in team_data.rate_list_full[0].up.formation: - LOGGER.info("rate up info:name %s star %s", i.name, i.star) diff --git a/tests/unit/test_hyperion.py b/tests/unit/test_hyperion.py deleted file mode 100644 index bb11851..0000000 --- a/tests/unit/test_hyperion.py +++ /dev/null @@ -1,34 +0,0 @@ -import pytest -import pytest_asyncio -from flaky import flaky - -from modules.apihelper.client.components.hyperion import Hyperion - - -@pytest_asyncio.fixture -async def hyperion(): - _hyperion = Hyperion() - yield _hyperion - await _hyperion.close() - - -# noinspection PyShadowingNames -@pytest.mark.asyncio -@flaky(3, 1) -async def test_get_strategy(hyperion): - test_collection_id_list = [839176, 839179, 839181, 1180811] - test_result = ["温迪", "胡桃", "雷电将军", "柯莱"] - - async def get_post_id(_collection_id: int, character_name: str) -> str: - post_full_in_collection = await hyperion.get_post_full_in_collection(_collection_id) - for post_data in post_full_in_collection["posts"]: - topics = post_data["topics"] - for topic in topics: - if character_name == topic["name"]: - return topic["name"] - return "" - - for index, _ in enumerate(test_collection_id_list): - second = test_result[index] - first = await get_post_id(test_collection_id_list[index], second) - assert first == second diff --git a/tests/unit/test_hyperion_bbs.py b/tests/unit/test_hyperion_bbs.py deleted file mode 100644 index 54eb586..0000000 --- a/tests/unit/test_hyperion_bbs.py +++ /dev/null @@ -1,72 +0,0 @@ -"""Test Url -https://bbs.mihoyo.com/ys/article/29023709 -""" -import logging - -import pytest -import pytest_asyncio -from bs4 import BeautifulSoup -from flaky import flaky - -from modules.apihelper.client.components.hyperion import Hyperion -from modules.apihelper.models.genshin.hyperion import PostInfo - -LOGGER = logging.getLogger(__name__) - - -@pytest_asyncio.fixture -async def hyperion(): - _hyperion = Hyperion() - yield _hyperion - await _hyperion.close() - - -# noinspection PyShadowingNames -@pytest.mark.asyncio -@flaky(3, 1) -async def test_get_post_info(hyperion): - post_info = await hyperion.get_post_info(2, 29023709) - assert post_info - assert isinstance(post_info, PostInfo) - assert post_info["post"]["post"]["post_id"] == "29023709" - assert post_info.post_id == 29023709 - assert post_info["post"]["post"]["subject"] == "《原神》长期项目启动·概念PV" - assert post_info.subject == "《原神》长期项目启动·概念PV" - assert len(post_info["post"]["post"]["images"]) == 1 - post_soup = BeautifulSoup(post_info["post"]["post"]["content"], features="html.parser") - assert post_soup.find_all("p") - - -# noinspection PyShadowingNames -@pytest.mark.asyncio -@flaky(3, 1) -async def test_get_video_post_info(hyperion): - post_info = await hyperion.get_post_info(2, 33846648) - assert post_info - assert isinstance(post_info, PostInfo) - assert post_info["post"]["post"]["post_id"] == "33846648" - assert post_info.post_id == 33846648 - assert post_info["post"]["post"]["subject"] == "当然是原神了" - assert post_info.subject == "当然是原神了" - assert len(post_info.video_urls) == 1 - - -# noinspection PyShadowingNames -@pytest.mark.asyncio -@flaky(3, 1) -async def test_get_images_by_post_id(hyperion): - post_images = await hyperion.get_images_by_post_id(2, 29023709) - assert len(post_images) == 1 - - -# noinspection PyShadowingNames -@pytest.mark.asyncio -@flaky(3, 1) -async def test_official_recommended_posts(hyperion): - official_recommended_posts = await hyperion.get_official_recommended_posts(2) - assert len(official_recommended_posts["list"]) > 0 - for data_list in official_recommended_posts["list"]: - post_info = await hyperion.get_post_info(2, data_list["post_id"]) - assert post_info.post_id - assert post_info.subject - LOGGER.info("official_recommended_posts: post_id[%s] subject[%s]", post_info.post_id, post_info.subject) diff --git a/utils/patch/genshin.py b/utils/patch/genshin.py index 301b0b1..27a68fc 100644 --- a/utils/patch/genshin.py +++ b/utils/patch/genshin.py @@ -304,10 +304,10 @@ class DailyRewardClient: elif self.region == types.Region.CHINESE: # TODO: Support cn honkai - player_id = await self._get_uid(types.Game.GENSHIN) + player_id = await self._get_uid(types.Game.STARRAIL) params["uid"] = player_id - params["region"] = utility.recognize_genshin_server(player_id) + params["region"] = utility.recognize_server(player_id, types.Game.STARRAIL) account_id = self.cookie_manager.user_id if account_id: