👷 Update to HonkaiStarRailGram

This commit is contained in:
xtaodada 2023-04-26 16:48:05 +08:00
parent 0a63b8250d
commit a72be627da
Signed by: xtaodada
GPG Key ID: 4CBB3F4FA8C85659
76 changed files with 942 additions and 6429 deletions

View File

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

View File

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

View File

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

View 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:

View File

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

View File

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

View File

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

View File

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

View File

@ -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 aiofiles
import ujson as jsonlib
except ImportError:
import json as jsonlib
__all__ = ["Model", "WikiModel", "HONEY_HOST"]
HONEY_HOST = URL("https://genshin.honeyhunterworld.com/")
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()

View File

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

View 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())

View File

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

View 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]
"""角色星魂数据"""

View 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 = "其他-能量充能效率"

View 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]
"""晋阶信息"""

View 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
"""类型"""

View 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
"""发现地点"""

View 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
View 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
View 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
View 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())

View File

@ -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`
必要的函数必须捕获异常后通知用户或者直接抛出异常
**部分修饰器为带参修饰器,必须带括号,否则会出现调用错误**

View File

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

View File

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

View File

@ -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 绑定"

View File

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

View File

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

View File

@ -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完成!")

View File

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

View File

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

View File

@ -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}
)
]

View File

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

View File

@ -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},
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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}`")

View File

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

View File

@ -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刷新成功")

View File

@ -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`的格式并保证每个数字都在[110]
"""
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)

View File

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

View File

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

View File

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

View File

@ -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):
"""未找到注册时间"""

View File

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

View File

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

View File

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

View File

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

View 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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

View File

@ -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%);
}

View File

@ -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>-->
<!-- &lt;!&ndash; 最高查询类 &ndash;&gt;-->
<!-- <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>-->
<!-- &lt;!&ndash;! gacha_log 相关 &ndash;&gt;-->
<!-- <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>-->
<!-- &lt;!&ndash;! 其他 &ndash;&gt;-->
<!-- <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>-->
<!-- &lt;!&ndash;! gacha_log 相关 &ndash;&gt;-->
<!-- <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>-->
<!-- &lt;!&ndash;! pay_log 相关 &ndash;&gt;-->
<!-- <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>

View File

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

View File

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

View File

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

View File

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