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 @@