mirror of
https://github.com/PaiGramTeam/PamGram.git
synced 2024-11-23 06:49:35 +00:00
👷 Update to HonkaiStarRailGram
This commit is contained in:
parent
0a63b8250d
commit
a72be627da
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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:
|
||||
|
@ -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()
|
||||
|
@ -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())
|
||||
|
46
modules/wiki/light_cone.py
Normal file
46
modules/wiki/light_cone.py
Normal file
@ -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())
|
@ -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("<br/>"))
|
||||
)
|
||||
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("<br/>"))
|
||||
)
|
||||
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())
|
||||
|
60
modules/wiki/models/avatar.py
Normal file
60
modules/wiki/models/avatar.py
Normal file
@ -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]
|
||||
"""角色星魂数据"""
|
84
modules/wiki/models/enums.py
Normal file
84
modules/wiki/models/enums.py
Normal file
@ -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 = "其他-能量充能效率"
|
45
modules/wiki/models/light_cone.py
Normal file
45
modules/wiki/models/light_cone.py
Normal file
@ -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]
|
||||
"""晋阶信息"""
|
19
modules/wiki/models/material.py
Normal file
19
modules/wiki/models/material.py
Normal file
@ -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
|
||||
"""类型"""
|
26
modules/wiki/models/monster.py
Normal file
26
modules/wiki/models/monster.py
Normal file
@ -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
|
||||
"""发现地点"""
|
||||
|
13
modules/wiki/models/relic.py
Normal file
13
modules/wiki/models/relic.py
Normal file
@ -0,0 +1,13 @@
|
||||
# 遗器套装
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class Relic(BaseModel):
|
||||
id: int
|
||||
"""遗器套装ID"""
|
||||
name: str
|
||||
"""套装名称"""
|
||||
icon: str
|
||||
"""套装图标"""
|
||||
affect: str
|
||||
"""套装效果"""
|
46
modules/wiki/monster.py
Normal file
46
modules/wiki/monster.py
Normal file
@ -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())
|
40
modules/wiki/raider.py
Normal file
40
modules/wiki/raider.py
Normal file
@ -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)
|
46
modules/wiki/relic.py
Normal file
46
modules/wiki/relic.py
Normal file
@ -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())
|
@ -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`
|
||||
|
||||
必要的函数必须捕获异常后通知用户或者直接抛出异常
|
||||
|
||||
**部分修饰器为带参修饰器,必须带括号,否则会出现调用错误**
|
@ -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,8 +279,6 @@ 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,
|
||||
|
@ -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,8 +361,6 @@ 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,
|
||||
|
@ -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 绑定"
|
||||
|
@ -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]
|
||||
|
@ -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
|
@ -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完成!")
|
@ -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,17 +97,17 @@ 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:
|
||||
if simple_search_result.photo_file_id:
|
||||
description = simple_search_result.description
|
||||
if len(description) >= 10:
|
||||
description = description[:10]
|
||||
results_list.append(
|
||||
InlineQueryResultCachedPhoto(
|
||||
item = None
|
||||
if simple_search_result.photo_file_id:
|
||||
item = InlineQueryResultCachedPhoto(
|
||||
id=str(uuid4()),
|
||||
title=simple_search_result.title,
|
||||
photo_file_id=simple_search_result.photo_file_id,
|
||||
@ -167,15 +115,24 @@ class Inline(Plugin):
|
||||
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:
|
||||
|
@ -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! ヾ(✿゚▽゚)ノ")
|
||||
|
42
plugins/app/title.py
Normal file
42
plugins/app/title.py
Normal file
@ -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)
|
@ -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(
|
||||
"<b>深渊挑战数据</b>功能使用帮助(中括号表示可选参数)\n\n"
|
||||
"指令格式:\n<code>/abyss + [层数/all] + [pre]</code>\n(<code>pre</code>表示上期)\n\n"
|
||||
"文本格式:\n<code>深渊数据 + 查询/总览 + [上期] + [层数]</code> \n\n"
|
||||
"例如以下指令都正确:\n"
|
||||
"<code>/abyss</code>\n<code>/abyss 12 pre</code>\n<code>/abyss all pre</code>\n"
|
||||
"<code>深渊数据查询</code>\n<code>深渊数据查询上期第12层</code>\n<code>深渊数据总览上期</code>",
|
||||
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}(<code>{uid}</code>) {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}
|
||||
)
|
||||
]
|
@ -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)
|
@ -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(
|
||||
"此功能需要绑定<code>cookie</code>后使用,请先私聊派蒙绑定账号",
|
||||
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(
|
||||
"此功能需要绑定<code>cookie</code>后使用,请先私聊派蒙进行绑定",
|
||||
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("请先在米游社中使用一次<b>养成计算器</b>后再使用此功能~")
|
||||
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},
|
||||
)
|
@ -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(
|
||||
"此功能需要绑定<code>cookie</code>后使用,请先私聊派蒙绑定账号",
|
||||
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(
|
||||
"此功能需要绑定<code>cookie</code>后使用,请先私聊派蒙进行绑定",
|
||||
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)
|
@ -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)
|
@ -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 "这天") + "是星期天, <b>全部素材都可以</b>刷哦~", 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中使用一次<b>养成计算器</b>后再使用此功能~",
|
||||
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(
|
||||
"每日素材表" + ("摘抄<b>完成!</b>" 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每日素材图标搬运<b>完成!</b>", 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"正在搬运 <b>{name}</b> 的图标素材。。。")
|
||||
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"正在搬运 <b>{name}</b> 的图标素材。。。<b>成功!</b>")
|
||||
|
||||
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)
|
@ -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)
|
@ -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)
|
@ -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}`")
|
@ -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)
|
@ -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<name>.*)(在哪里|在哪|哪里有|哪儿有|哪有|在哪儿)$"), block=False)
|
||||
@handler(MessageHandler, filters=filters.Regex("^(哪里有|哪儿有|哪有)(?P<name>.*)$"), 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刷新成功")
|
@ -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)
|
@ -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(
|
||||
"<b>开始导入充值历史记录:请通过 https://paimon.moe/wish/import 获取抽卡记录链接后发送给我"
|
||||
"(非 paimon.moe 导出的文件数据)</b>\n\n"
|
||||
"> 在绑定 Cookie 时添加 stoken 可能有特殊效果哦(仅限国服)\n"
|
||||
"<b>注意:导入的数据将会与旧数据进行合并。</b>",
|
||||
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))
|
@ -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
|
||||
]
|
@ -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)
|
@ -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(
|
||||
"此功能需要绑定<code>cookie</code>后使用,请先私聊派蒙绑定账号",
|
||||
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(
|
||||
"此功能需要绑定<code>cookie</code>后使用,请先私聊派蒙进行绑定",
|
||||
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):
|
||||
"""未找到注册时间"""
|
@ -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)
|
@ -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)
|
@ -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"<color=#(.*?)>", 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)
|
@ -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(
|
||||
"<b>开始导入祈愿历史记录:请通过 https://paimon.moe/wish/import 获取抽卡记录链接后发送给我"
|
||||
"(非 paimon.moe 导出的文件数据)</b>\n\n"
|
||||
"> 你还可以向派蒙发送从其他工具导出的 UIGF 标准的记录文件\n"
|
||||
"> 或者从 paimon.moe 、非小酋 导出的 xlsx 记录文件\n"
|
||||
"> 在绑定 Cookie 时添加 stoken 可能有特殊效果哦(仅限国服)\n"
|
||||
"<b>注意:导入的数据将会与旧数据进行合并。</b>",
|
||||
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))
|
0
plugins/group/README.md
Normal file
0
plugins/group/README.md
Normal file
@ -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)
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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,18 +52,28 @@ 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 米游社 西风驿站 查看<a href='{url}'>原图</a>"
|
||||
reply_photo = await message.reply_photo(
|
||||
photo=open(file_path, "rb"),
|
||||
caption = "From 米游社"
|
||||
if file_id := await self.cache_service.get_strategy_cache(character_name):
|
||||
await message.reply_document(
|
||||
document=file_id,
|
||||
caption=caption,
|
||||
filename=f"{character_name}.png",
|
||||
allow_sending_without_reply=True,
|
||||
parse_mode=ParseMode.HTML,
|
||||
)
|
||||
if reply_photo.photo:
|
||||
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_photo.photo[0].file_id
|
||||
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,
|
||||
@ -68,6 +81,6 @@ class StrategyPlugin(Plugin):
|
||||
tags=tags,
|
||||
caption=caption,
|
||||
parse_mode="HTML",
|
||||
photo_file_id=photo_file_id,
|
||||
document_file_id=photo_file_id,
|
||||
)
|
||||
await self.search_service.add_entry(entry)
|
@ -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:
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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:
|
||||
|
@ -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)}"
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 40 KiB |
Binary file not shown.
Before Width: | Height: | Size: 70 KiB |
Binary file not shown.
Before Width: | Height: | Size: 82 KiB |
Binary file not shown.
Before Width: | Height: | Size: 427 KiB |
Binary file not shown.
Before Width: | Height: | Size: 3.6 KiB |
BIN
resources/bot/help/background/header.png
Normal file
BIN
resources/bot/help/background/header.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 295 KiB |
@ -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%);
|
||||
}
|
||||
|
||||
|
@ -11,7 +11,7 @@
|
||||
<div class="container mx-auto px-5 py-10 max-w-7xl">
|
||||
<div class="header p-6 flex mb-8 rounded-xl bg-cover justify-between">
|
||||
<div>
|
||||
<h1 class="text-4xl italic">TGPaimonBot</h1>
|
||||
<h1 class="text-4xl italic">StarRailBot</h1>
|
||||
<h1 class="text-2xl">使用说明</h1>
|
||||
</div>
|
||||
<div>
|
||||
@ -25,106 +25,106 @@
|
||||
</div>
|
||||
<div class="box pt-4 rounded-xl space-y-4 overflow-hidden">
|
||||
<div>
|
||||
<div class="command-background pointer-events-none">
|
||||
<img src="background/2015.png" alt=""/>
|
||||
</div>
|
||||
<!-- <div class="command-background pointer-events-none">-->
|
||||
<!-- <img src="background/2015.png" alt=""/>-->
|
||||
<!-- </div>-->
|
||||
<div class="command-title text-2xl mx-1">
|
||||
<h1>查询命令</h1>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 py-4 px-2">
|
||||
<!-- WIKI类 -->
|
||||
<div class="command">
|
||||
<div class="command-name">/weapon</div>
|
||||
<div class="command-description">查询武器</div>
|
||||
</div>
|
||||
<!-- <div class="command">-->
|
||||
<!-- <div class="command-name">/weapon</div>-->
|
||||
<!-- <div class="command-description">查询武器</div>-->
|
||||
<!-- </div>-->
|
||||
<div class="command rounded-xl flex-1">
|
||||
<div class="command-name">/strategy</div>
|
||||
<div class="command-description">查询角色攻略</div>
|
||||
</div>
|
||||
<div class="command">
|
||||
<div class="command-name">/material</div>
|
||||
<div class="command-description">角色培养素材查询</div>
|
||||
</div>
|
||||
<!-- <div class="command">-->
|
||||
<!-- <div class="command-name">/material</div>-->
|
||||
<!-- <div class="command-description">角色培养素材查询</div>-->
|
||||
<!-- </div>-->
|
||||
<!-- UID 查询类 -->
|
||||
<div class="command">
|
||||
<div class="command-name">
|
||||
/stats
|
||||
<i class="fa fa-user-circle-o ml-2"></i>
|
||||
</div>
|
||||
<div class="command-description">玩家统计查询</div>
|
||||
</div>
|
||||
<div class="command">
|
||||
<div class="command-name">
|
||||
/player_card
|
||||
<i class="fa fa-user-circle-o ml-2"></i>
|
||||
</div>
|
||||
<div class="command-description">角色卡片</div>
|
||||
</div>
|
||||
<!-- 最高查询类 -->
|
||||
<div class="command">
|
||||
<div class="command-name">
|
||||
/dailynote
|
||||
<i class="fa fa-id-card-o ml-2"></i>
|
||||
</div>
|
||||
<div class="command-description">查询实时便笺</div>
|
||||
</div>
|
||||
<div class="command">
|
||||
<div class="command-name">
|
||||
/ledger
|
||||
<i class="fa fa-id-card-o ml-2"></i>
|
||||
</div>
|
||||
<div class="command-description">查询当月旅行札记</div>
|
||||
</div>
|
||||
<div class="command">
|
||||
<div class="command-name">
|
||||
/abyss
|
||||
<i class="fa fa-id-card-o ml-2"></i>
|
||||
</div>
|
||||
<div class="command-description">查询当期深渊战绩</div>
|
||||
</div>
|
||||
<div class="command">
|
||||
<div class="command-name">
|
||||
/abyss_team
|
||||
<i class="fa fa-id-card-o ml-2"></i>
|
||||
</div>
|
||||
<div class="command-description">查询深渊推荐配队</div>
|
||||
</div>
|
||||
<div class="command">
|
||||
<div class="command-name">
|
||||
/avatars
|
||||
<i class="fa fa-id-card-o ml-2"></i>
|
||||
</div>
|
||||
<div class="command-description">查询角色练度</div>
|
||||
</div>
|
||||
<div class="command">
|
||||
<div class="command-name">
|
||||
/reg_time
|
||||
<i class="fa fa-id-card-o ml-2"></i>
|
||||
</div>
|
||||
<div class="command-description">原神账号注册时间</div>
|
||||
</div>
|
||||
<!--! gacha_log 相关 -->
|
||||
<div class="command">
|
||||
<div class="command-name">
|
||||
/gacha_log
|
||||
<i class="fa fa-user-circle-o ml-2"></i>
|
||||
</div>
|
||||
<div class="command-description">抽卡记录</div>
|
||||
</div>
|
||||
<div class="command">
|
||||
<div class="command-name">
|
||||
/gacha_count
|
||||
<i class="fa fa-user-circle-o ml-2"></i>
|
||||
</div>
|
||||
<div class="command-description">抽卡统计</div>
|
||||
</div>
|
||||
<div class="command">
|
||||
<div class="command-name">
|
||||
/pay_log
|
||||
<i class="fa fa-user-circle-o ml-2"></i>
|
||||
</div>
|
||||
<div class="command-description">充值记录</div>
|
||||
</div>
|
||||
<!-- <div class="command">-->
|
||||
<!-- <div class="command-name">-->
|
||||
<!-- /stats-->
|
||||
<!-- <i class="fa fa-user-circle-o ml-2"></i>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="command-description">玩家统计查询</div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="command">-->
|
||||
<!-- <div class="command-name">-->
|
||||
<!-- /player_card-->
|
||||
<!-- <i class="fa fa-user-circle-o ml-2"></i>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="command-description">角色卡片</div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <!– 最高查询类 –>-->
|
||||
<!-- <div class="command">-->
|
||||
<!-- <div class="command-name">-->
|
||||
<!-- /dailynote-->
|
||||
<!-- <i class="fa fa-id-card-o ml-2"></i>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="command-description">查询实时便笺</div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="command">-->
|
||||
<!-- <div class="command-name">-->
|
||||
<!-- /ledger-->
|
||||
<!-- <i class="fa fa-id-card-o ml-2"></i>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="command-description">查询当月旅行札记</div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="command">-->
|
||||
<!-- <div class="command-name">-->
|
||||
<!-- /abyss-->
|
||||
<!-- <i class="fa fa-id-card-o ml-2"></i>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="command-description">查询当期深渊战绩</div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="command">-->
|
||||
<!-- <div class="command-name">-->
|
||||
<!-- /abyss_team-->
|
||||
<!-- <i class="fa fa-id-card-o ml-2"></i>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="command-description">查询深渊推荐配队</div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="command">-->
|
||||
<!-- <div class="command-name">-->
|
||||
<!-- /avatars-->
|
||||
<!-- <i class="fa fa-id-card-o ml-2"></i>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="command-description">查询角色练度</div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="command">-->
|
||||
<!-- <div class="command-name">-->
|
||||
<!-- /reg_time-->
|
||||
<!-- <i class="fa fa-id-card-o ml-2"></i>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="command-description">原神账号注册时间</div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <!–! gacha_log 相关 –>-->
|
||||
<!-- <div class="command">-->
|
||||
<!-- <div class="command-name">-->
|
||||
<!-- /gacha_log-->
|
||||
<!-- <i class="fa fa-user-circle-o ml-2"></i>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="command-description">抽卡记录</div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="command">-->
|
||||
<!-- <div class="command-name">-->
|
||||
<!-- /gacha_count-->
|
||||
<!-- <i class="fa fa-user-circle-o ml-2"></i>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="command-description">抽卡统计</div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="command">-->
|
||||
<!-- <div class="command-name">-->
|
||||
<!-- /pay_log-->
|
||||
<!-- <i class="fa fa-user-circle-o ml-2"></i>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="command-description">充值记录</div>-->
|
||||
<!-- </div>-->
|
||||
<div class="command">
|
||||
<div class="command-name">
|
||||
/sign
|
||||
@ -132,86 +132,86 @@
|
||||
</div>
|
||||
<div class="command-description">每日签到 | 查询</div>
|
||||
</div>
|
||||
<div class="command">
|
||||
<div class="command-name">/daily_material</div>
|
||||
<div class="command-description">每日素材</div>
|
||||
</div>
|
||||
<!--! 其他 -->
|
||||
<div class="command">
|
||||
<div class="command-name">/hilichurls</div>
|
||||
<div class="command-description">丘丘语字典</div>
|
||||
</div>
|
||||
<div class="command">
|
||||
<div class="command-name">/birthday</div>
|
||||
<div class="command-description">角色生日</div>
|
||||
</div>
|
||||
<div class="command">
|
||||
<div class="command-name">
|
||||
/birthday_card
|
||||
<i class="fa fa-id-card-o ml-2"></i>
|
||||
</div>
|
||||
<div class="command-description">领取角色生日画片</div>
|
||||
</div>
|
||||
<div class="command">
|
||||
<div class="command-name">/calendar</div>
|
||||
<div class="command-description">活动日历</div>
|
||||
</div>
|
||||
<!-- <div class="command">-->
|
||||
<!-- <div class="command-name">/daily_material</div>-->
|
||||
<!-- <div class="command-description">每日素材</div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <!–! 其他 –>-->
|
||||
<!-- <div class="command">-->
|
||||
<!-- <div class="command-name">/hilichurls</div>-->
|
||||
<!-- <div class="command-description">丘丘语字典</div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="command">-->
|
||||
<!-- <div class="command-name">/birthday</div>-->
|
||||
<!-- <div class="command-description">角色生日</div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="command">-->
|
||||
<!-- <div class="command-name">-->
|
||||
<!-- /birthday_card-->
|
||||
<!-- <i class="fa fa-id-card-o ml-2"></i>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="command-description">领取角色生日画片</div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="command">-->
|
||||
<!-- <div class="command-name">/calendar</div>-->
|
||||
<!-- <div class="command-description">活动日历</div>-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="command-background pointer-events-none">
|
||||
<img src="background/1006.png" />
|
||||
</div>
|
||||
<!-- <div class="command-background pointer-events-none">-->
|
||||
<!-- <img src="background/1006.png" />-->
|
||||
<!-- </div>-->
|
||||
<div class="command-title text-2xl mx-1">
|
||||
<h1>其他命令</h1>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 py-4 px-2">
|
||||
<div class="command">
|
||||
<div class="command-name">/wish</div>
|
||||
<div class="command-description">抽卡模拟器(非洲人模拟器)</div>
|
||||
</div>
|
||||
<div class="command">
|
||||
<div class="command-name">/set_wish</div>
|
||||
<div class="command-description">抽卡模拟器定轨</div>
|
||||
</div>
|
||||
<div class="command">
|
||||
<div class="command-name">/quiz</div>
|
||||
<div class="command-description">
|
||||
派蒙的十万个为什么
|
||||
</div>
|
||||
</div>
|
||||
<!--! gacha_log 相关 -->
|
||||
<div class="command">
|
||||
<div class="command-name">/gacha_log_import</div>
|
||||
<div class="command-description">导入抽卡记录</div>
|
||||
</div>
|
||||
<div class="command">
|
||||
<div class="command-name">/gacha_log_export</div>
|
||||
<div class="command-description">导出抽卡记录</div>
|
||||
</div>
|
||||
<div class="command">
|
||||
<div class="command-name">/gacha_log_delete</div>
|
||||
<div class="command-description">删除抽卡记录</div>
|
||||
</div>
|
||||
<!--! pay_log 相关 -->
|
||||
<div class="command">
|
||||
<div class="command-name">/pay_log_import</div>
|
||||
<div class="command-description">导入充值记录</div>
|
||||
</div>
|
||||
<div class="command">
|
||||
<div class="command-name">/pay_log_export</div>
|
||||
<div class="command-description">导出充值记录</div>
|
||||
</div>
|
||||
<div class="command">
|
||||
<div class="command-name">/pay_log_delete</div>
|
||||
<div class="command-description">删除充值记录</div>
|
||||
</div>
|
||||
<!-- <div class="command">-->
|
||||
<!-- <div class="command-name">/wish</div>-->
|
||||
<!-- <div class="command-description">抽卡模拟器(非洲人模拟器)</div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="command">-->
|
||||
<!-- <div class="command-name">/set_wish</div>-->
|
||||
<!-- <div class="command-description">抽卡模拟器定轨</div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="command">-->
|
||||
<!-- <div class="command-name">/quiz</div>-->
|
||||
<!-- <div class="command-description">-->
|
||||
<!-- 派蒙的十万个为什么-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <!–! gacha_log 相关 –>-->
|
||||
<!-- <div class="command">-->
|
||||
<!-- <div class="command-name">/gacha_log_import</div>-->
|
||||
<!-- <div class="command-description">导入抽卡记录</div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="command">-->
|
||||
<!-- <div class="command-name">/gacha_log_export</div>-->
|
||||
<!-- <div class="command-description">导出抽卡记录</div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="command">-->
|
||||
<!-- <div class="command-name">/gacha_log_delete</div>-->
|
||||
<!-- <div class="command-description">删除抽卡记录</div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <!–! pay_log 相关 –>-->
|
||||
<!-- <div class="command">-->
|
||||
<!-- <div class="command-name">/pay_log_import</div>-->
|
||||
<!-- <div class="command-description">导入充值记录</div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="command">-->
|
||||
<!-- <div class="command-name">/pay_log_export</div>-->
|
||||
<!-- <div class="command-description">导出充值记录</div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="command">-->
|
||||
<!-- <div class="command-name">/pay_log_delete</div>-->
|
||||
<!-- <div class="command-description">删除充值记录</div>-->
|
||||
<!-- </div>-->
|
||||
<!--! user 相关 -->
|
||||
<div class="command">
|
||||
<div class="command-name">/setuid</div>
|
||||
<div class="command-description">添加/重设UID(请私聊BOT)</div>
|
||||
</div>
|
||||
<!-- <div class="command">-->
|
||||
<!-- <div class="command-name">/setuid</div>-->
|
||||
<!-- <div class="command-description">添加/重设UID(请私聊BOT)</div>-->
|
||||
<!-- </div>-->
|
||||
<div class="command">
|
||||
<div class="command-name">/setcookie</div>
|
||||
<div class="command-description">添加/重设Cookie(请私聊BOT)</div>
|
||||
@ -239,10 +239,10 @@
|
||||
<div class="command-name">@{{bot_username}} 角色名</div>
|
||||
<div class="command-description">查询角色攻略</div>
|
||||
</div>
|
||||
<div class="command">
|
||||
<div class="command-name">@{{bot_username}} 武器名</div>
|
||||
<div class="command-description">查询武器信息</div>
|
||||
</div>
|
||||
<!-- <div class="command">-->
|
||||
<!-- <div class="command-name">@{{bot_username}} 武器名</div>-->
|
||||
<!-- <div class="command-description">查询武器信息</div>-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -1,42 +0,0 @@
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from flaky import flaky
|
||||
|
||||
from modules.apihelper.client.components.abyss import AbyssTeam
|
||||
from modules.apihelper.models.genshin.abyss import TeamRateResult, TeamRate, FullTeamRate
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def abyss_team_data():
|
||||
_abyss_team_data = AbyssTeam()
|
||||
yield _abyss_team_data
|
||||
await _abyss_team_data.close()
|
||||
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
@pytest.mark.asyncio
|
||||
@flaky(3, 1)
|
||||
async def test_abyss_team_data(abyss_team_data: AbyssTeam):
|
||||
team_data = await abyss_team_data.get_data()
|
||||
assert isinstance(team_data, TeamRateResult)
|
||||
assert isinstance(team_data.rate_list_up[0], TeamRate)
|
||||
assert isinstance(team_data.rate_list_up[-1], TeamRate)
|
||||
assert isinstance(team_data.rate_list_down[0], TeamRate)
|
||||
assert isinstance(team_data.rate_list_down[-1], TeamRate)
|
||||
assert team_data.user_count > 0
|
||||
team_data.sort(["迪奥娜", "芭芭拉", "凯亚", "琴"])
|
||||
assert isinstance(team_data.rate_list_full[0], FullTeamRate)
|
||||
assert isinstance(team_data.rate_list_full[-1], FullTeamRate)
|
||||
random_team = team_data.random_team()[0]
|
||||
assert isinstance(random_team, FullTeamRate)
|
||||
member_up = {i.name for i in random_team.up.formation}
|
||||
member_down = {i.name for i in random_team.down.formation}
|
||||
assert not member_up & member_down
|
||||
for i in team_data.rate_list_full[0].down.formation:
|
||||
LOGGER.info("rate down info:name %s star %s", i.name, i.star)
|
||||
for i in team_data.rate_list_full[0].up.formation:
|
||||
LOGGER.info("rate up info:name %s star %s", i.name, i.star)
|
@ -1,34 +0,0 @@
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from flaky import flaky
|
||||
|
||||
from modules.apihelper.client.components.hyperion import Hyperion
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def hyperion():
|
||||
_hyperion = Hyperion()
|
||||
yield _hyperion
|
||||
await _hyperion.close()
|
||||
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
@pytest.mark.asyncio
|
||||
@flaky(3, 1)
|
||||
async def test_get_strategy(hyperion):
|
||||
test_collection_id_list = [839176, 839179, 839181, 1180811]
|
||||
test_result = ["温迪", "胡桃", "雷电将军", "柯莱"]
|
||||
|
||||
async def get_post_id(_collection_id: int, character_name: str) -> str:
|
||||
post_full_in_collection = await hyperion.get_post_full_in_collection(_collection_id)
|
||||
for post_data in post_full_in_collection["posts"]:
|
||||
topics = post_data["topics"]
|
||||
for topic in topics:
|
||||
if character_name == topic["name"]:
|
||||
return topic["name"]
|
||||
return ""
|
||||
|
||||
for index, _ in enumerate(test_collection_id_list):
|
||||
second = test_result[index]
|
||||
first = await get_post_id(test_collection_id_list[index], second)
|
||||
assert first == second
|
@ -1,72 +0,0 @@
|
||||
"""Test Url
|
||||
https://bbs.mihoyo.com/ys/article/29023709
|
||||
"""
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from bs4 import BeautifulSoup
|
||||
from flaky import flaky
|
||||
|
||||
from modules.apihelper.client.components.hyperion import Hyperion
|
||||
from modules.apihelper.models.genshin.hyperion import PostInfo
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def hyperion():
|
||||
_hyperion = Hyperion()
|
||||
yield _hyperion
|
||||
await _hyperion.close()
|
||||
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
@pytest.mark.asyncio
|
||||
@flaky(3, 1)
|
||||
async def test_get_post_info(hyperion):
|
||||
post_info = await hyperion.get_post_info(2, 29023709)
|
||||
assert post_info
|
||||
assert isinstance(post_info, PostInfo)
|
||||
assert post_info["post"]["post"]["post_id"] == "29023709"
|
||||
assert post_info.post_id == 29023709
|
||||
assert post_info["post"]["post"]["subject"] == "《原神》长期项目启动·概念PV"
|
||||
assert post_info.subject == "《原神》长期项目启动·概念PV"
|
||||
assert len(post_info["post"]["post"]["images"]) == 1
|
||||
post_soup = BeautifulSoup(post_info["post"]["post"]["content"], features="html.parser")
|
||||
assert post_soup.find_all("p")
|
||||
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
@pytest.mark.asyncio
|
||||
@flaky(3, 1)
|
||||
async def test_get_video_post_info(hyperion):
|
||||
post_info = await hyperion.get_post_info(2, 33846648)
|
||||
assert post_info
|
||||
assert isinstance(post_info, PostInfo)
|
||||
assert post_info["post"]["post"]["post_id"] == "33846648"
|
||||
assert post_info.post_id == 33846648
|
||||
assert post_info["post"]["post"]["subject"] == "当然是原神了"
|
||||
assert post_info.subject == "当然是原神了"
|
||||
assert len(post_info.video_urls) == 1
|
||||
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
@pytest.mark.asyncio
|
||||
@flaky(3, 1)
|
||||
async def test_get_images_by_post_id(hyperion):
|
||||
post_images = await hyperion.get_images_by_post_id(2, 29023709)
|
||||
assert len(post_images) == 1
|
||||
|
||||
|
||||
# noinspection PyShadowingNames
|
||||
@pytest.mark.asyncio
|
||||
@flaky(3, 1)
|
||||
async def test_official_recommended_posts(hyperion):
|
||||
official_recommended_posts = await hyperion.get_official_recommended_posts(2)
|
||||
assert len(official_recommended_posts["list"]) > 0
|
||||
for data_list in official_recommended_posts["list"]:
|
||||
post_info = await hyperion.get_post_info(2, data_list["post_id"])
|
||||
assert post_info.post_id
|
||||
assert post_info.subject
|
||||
LOGGER.info("official_recommended_posts: post_id[%s] subject[%s]", post_info.post_id, post_info.subject)
|
@ -304,10 +304,10 @@ class DailyRewardClient:
|
||||
|
||||
elif self.region == types.Region.CHINESE:
|
||||
# TODO: Support cn honkai
|
||||
player_id = await self._get_uid(types.Game.GENSHIN)
|
||||
player_id = await self._get_uid(types.Game.STARRAIL)
|
||||
|
||||
params["uid"] = player_id
|
||||
params["region"] = utility.recognize_genshin_server(player_id)
|
||||
params["region"] = utility.recognize_server(player_id, types.Game.STARRAIL)
|
||||
|
||||
account_id = self.cookie_manager.user_id
|
||||
if account_id:
|
||||
|
Loading…
Reference in New Issue
Block a user