From e038046365bff362c7192d2f257ef4858f39ef98 Mon Sep 17 00:00:00 2001 From: Karako Date: Sun, 18 Sep 2022 12:19:29 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20=E6=94=B9=E8=BF=9B=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=20=F0=9F=92=A1=20=E7=BB=99=20`daily=5Fmaterial`=20?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E6=B7=BB=E5=8A=A0=E6=B3=A8=E9=87=8A=20?= =?UTF-8?q?=F0=9F=8E=A8=20=E4=BD=BF=E7=94=A8=20`AssetsService`=20=E7=AE=A1?= =?UTF-8?q?=E7=90=86=20`weapon`=20=E6=A8=A1=E5=9D=97=E7=9A=84=E6=AD=A6?= =?UTF-8?q?=E5=99=A8=E5=92=8C=E6=9D=90=E6=96=99=E5=9B=BE=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/genshin/daily/material.py | 170 +++++++++++++++--------------- plugins/genshin/weapon.py | 26 +++-- utils/helpers.py | 16 +++ 3 files changed, 118 insertions(+), 94 deletions(-) diff --git a/plugins/genshin/daily/material.py b/plugins/genshin/daily/material.py index 2a7fb69..381a643 100644 --- a/plugins/genshin/daily/material.py +++ b/plugins/genshin/daily/material.py @@ -1,13 +1,13 @@ import asyncio import itertools -import os import re from asyncio import Lock from ctypes import c_double from datetime import datetime from multiprocessing import Value from pathlib import Path -from typing import Any, Dict, Iterable, List, Literal, Optional, Tuple, Union +from ssl import SSLZeroReturnError +from typing import Any, Dict, Iterable, List, Literal, Optional, Tuple import ujson as json from aiofiles import open as async_open @@ -28,27 +28,26 @@ from core.template import TemplateService from core.user.error import UserNotFoundError from metadata.honey import HONEY_ID_MAP, HONEY_ROLE_NAME_MAP from utils.bot import get_all_args -from utils.const import RESOURCE_DIR from utils.decorators.admins import bot_admins_rights_check from utils.decorators.error import error_callable from utils.decorators.restricts import restricts -from utils.helpers import get_genshin_client +from utils.helpers import get_genshin_client, is_number from utils.log import logger DATA_TYPE = Dict[str, List[List[str]]] DATA_FILE_PATH = Path(__file__).joinpath('../daily.json').resolve() -AREA = ['蒙德', '璃月', '稻妻', '须弥'] DOMAINS = ['忘却之峡', '太山府', '菫色之庭', '昏识塔', '塞西莉亚苗圃', '震雷连山密宫', '砂流之庭', '有顶塔'] -DOMAIN_AREA_MAP = dict(zip(DOMAINS, AREA * 2)) +DOMAIN_AREA_MAP = dict(zip(DOMAINS, ['蒙德', '璃月', '稻妻', '须弥'] * 2)) WEEK_MAP = ['一', '二', '三', '四', '五', '六', '日'] -def convert_path(path: Union[str, Path]) -> str: - return f"..{os.sep}..{os.sep}{str(path.relative_to(RESOURCE_DIR))}" - - def sort_item(items: List['ItemData']) -> Iterable['ItemData']: + """对武器和角色进行排序 + + 角色排序规则:星级>等级>命座 + 武器排序规则:星级>等级>精炼度 + """ result_a = [] for _, group_a in itertools.groupby(sorted(items, key=lambda x: x.rarity, reverse=True), lambda x: x.rarity): result_b = [] @@ -71,20 +70,23 @@ class DailyMaterial(Plugin, BasePlugin): self.client = AsyncClient() async def __async_init__(self): + """插件在初始化时,会检查一下本地是否缓存了每日素材的数据""" data = None - if not DATA_FILE_PATH.exists(): - async def task_daily(): - async with self.locks[0]: - logger.info("正在开始获取每日素材缓存") - self.data = await self._refresh_data() - self.refresh_task = asyncio.create_task(task_daily()) - if not data and DATA_FILE_PATH.exists(): + async def task_daily(): + async with self.locks[0]: + logger.info("正在开始获取每日素材缓存") + self.data = await self._refresh_data() + + if not DATA_FILE_PATH.exists(): # 若缓存不存在 + self.refresh_task = 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 + self.data = data async def _get_data_from_user(self, user: User) -> Tuple[Optional[Client], Dict[str, List[Any]]]: + """获取已经绑定的账号的角色、武器信息""" client = None user_data = {'character': [], 'weapon': []} try: @@ -99,19 +101,17 @@ class DailyMaterial(Plugin, BasePlugin): ItemData( id=cid, name=character.name, rarity=character.rarity, level=character.level, constellation=character.constellation, - icon=convert_path(await self.assets_service.character(cid).icon()) + icon=(await self.assets_service.character(cid).icon()).as_uri() ) ) user_data['weapon'].append( ItemData( id=(wid := f"i_n{weapon.id}"), name=weapon.name, level=weapon.level, rarity=weapon.rarity, refinement=weapon.refinement, - icon=convert_path( - await getattr( - self.assets_service.weapon(wid), 'icon' if weapon.ascension < 2 else 'awakened' - )() - ), - c_path=convert_path(await self.assets_service.character(cid).side()) + icon=(await getattr( # 判定武器的突破次数是否大于 2 ;若是, 则将图标替换为 awakened (觉醒) 的图标 + self.assets_service.weapon(wid), 'icon' if weapon.ascension < 2 else 'awakened' + )()).as_uri(), + c_path=(await self.assets_service.character(cid).side()).as_uri() ) ) except (UserNotFoundError, CookiesNotFoundError): @@ -126,21 +126,20 @@ class DailyMaterial(Plugin, BasePlugin): args = get_all_args(context) now = datetime.now() - if args and str(args[0]).isdigit(): - weekday = int(args[0]) - 1 - if weekday < 0: - weekday = 0 - elif weekday > 6: - weekday = 6 + if args and is_number(args[0]): + # 适配出现 负数、小数和数字大于 7 的状况 + weekday = (_ := int(args[0])) - (_ > 0) + weekday = (weekday % 7 + 7) % 7 time = title = f"星期{WEEK_MAP[weekday]}" else: # 获取今日是星期几,判定了是否过了凌晨4点 title = "今日" weekday = now.weekday() - (1 if now.hour < 4 else 0) weekday = 6 if weekday < 0 else weekday time = now.strftime("%m-%d %H:%M") + " 星期" + WEEK_MAP[weekday] - full = args and args[-1] == 'full' + full = args and args[-1] == 'full' # 判定最后一个参数是不是 full - logger.info(f"用户 {user.full_name}[{user.id}] 每日素材命令请求 || 参数 weekday={weekday} full={full}") + logger.info( + f"用户 {user.full_name}[{user.id}] 每日素材命令请求 || 参数 weekday={WEEK_MAP[weekday]} full={full}") if weekday == 6: await update.message.reply_text( @@ -149,26 +148,27 @@ class DailyMaterial(Plugin, BasePlugin): ) return - if self.locks[0].locked(): + if self.locks[0].locked(): # 若检测到了第一个锁:正在下载每日素材表的数据 notice = await update.message.reply_text("派蒙正在摘抄每日素材表,以后再来探索吧~") self._add_delete_message_job(context, notice.chat_id, notice.message_id, 5) return - if self.locks[1].locked(): + if self.locks[1].locked(): # 若检测到了第二个锁:正在下载角色、武器、材料的图标 await update.message.reply_text("派蒙正在搬运每日素材的图标,以后再来探索吧~") return notice = await update.message.reply_text("派蒙可能需要找找图标素材,还请耐心等待哦~") await update.message.reply_chat_action(ChatAction.TYPING) - # 获取已经缓存至本地的秘境素材信息 + # 获取已经缓存的秘境素材信息 local_data = {'character': [], 'weapon': []} - if not self.data: + if not self.data: # 若没有缓存每日素材表的数据 logger.info("正在获取每日素材缓存") await self._refresh_data() for domain, sche in self.data.items(): - area = DOMAIN_AREA_MAP[domain] - type_ = 'character' if DOMAINS.index(domain) < 4 else 'weapon' + area = DOMAIN_AREA_MAP[domain] # 获取秘境所在的区域 + type_ = 'character' if DOMAINS.index(domain) < 4 else 'weapon' # 获取秘境的培养素材的类型:是天赋书还是武器突破材料 + # 将读取到的数据存入 local_data 中 local_data[type_].append({'name': area, 'materials': sche[weekday][0], 'items': sche[weekday][1]}) # 尝试获取用户已绑定的原神账号信息 @@ -178,11 +178,11 @@ class DailyMaterial(Plugin, BasePlugin): render_data = RenderData(title=title, time=time, uid=client.uid if client else client) for type_ in ['character', 'weapon']: areas = [] - for area_data in local_data[type_]: + for area_data in local_data[type_]: # 遍历每个区域的信息:蒙德、璃月、稻妻、须弥 items = [] - for id_ in area_data['items']: + for id_ in area_data['items']: # 遍历所有该区域下,当天(weekday)可以培养的角色、武器 added = False - for i in user_data[type_]: + for i in user_data[type_]: # 从已经获取的角色数据中查找对应角色、武器 if id_ == i.id: if i.rarity > 3: # 跳过 3 星及以下的武器 items.append(i) @@ -193,26 +193,28 @@ class DailyMaterial(Plugin, BasePlugin): item = HONEY_ID_MAP[type_][id_] if item[1] < 4: # 跳过 3 星及以下的武器 continue - items.append(ItemData( + items.append(ItemData( # 添加角色数据中未找到的 id=id_, name=item[0], rarity=item[1], - icon=convert_path(await getattr(self.assets_service, f'{type_}')(id_).icon()) + icon=(await getattr(self.assets_service, f'{type_}')(id_).icon()).as_uri() )) materials = [] - for mid in area_data['materials']: - path = convert_path(await self.assets_service.material(mid).icon()) + for mid in area_data['materials']: # 添加这个区域当天(weekday)的培养素材 + path = (await self.assets_service.material(mid).icon()).as_uri() material = HONEY_ID_MAP['material'][mid] materials.append(ItemData(id=mid, icon=path, name=material[0], rarity=material[1])) areas.append(AreaData(name=area_data['name'], materials=materials, items=sort_item(items))) setattr(render_data, type_, areas) + await update.message.reply_chat_action(ChatAction.TYPING) - character_img_data = await self.template_service.render( + character_img_data = await self.template_service.render( # 渲染角色素材页 'genshin/daily_material', 'character.html', {'data': render_data}, {'width': 1164, 'height': 500} ) - weapon_img_data = await self.template_service.render( + weapon_img_data = await self.template_service.render( # 渲染武器素材页 'genshin/daily_material', 'weapon.html', {'data': render_data}, {'width': 1164, 'height': 500} ) + await update.message.reply_chat_action(ChatAction.UPLOAD_PHOTO) - if full: + if full: # 是否发送原图 await update.message.reply_media_group([ InputMediaDocument(character_img_data, filename="可培养角色.png"), InputMediaDocument(weapon_img_data, filename="可培养武器.png") @@ -235,9 +237,9 @@ class DailyMaterial(Plugin, BasePlugin): notice = await message.reply_text("派蒙正在搬运每日素材图标,在努力工作呢!") self._add_delete_message_job(context, notice.chat_id, notice.message_id, 10) return - async with self.locks[1]: + async with self.locks[1]: # 锁住第二把锁 notice = await message.reply_text("派蒙正在重新摘抄每日素材表,请稍等~", parse_mode=ParseMode.HTML) - async with self.locks[0]: + async with self.locks[0]: # 锁住第一把锁 data = await self._refresh_data() notice = await notice.edit_text( "每日素材表" + @@ -254,10 +256,10 @@ class DailyMaterial(Plugin, BasePlugin): self._add_delete_message_job(context, notice.chat_id, notice.message_id, 10) async def _refresh_data(self, retry: int = 5) -> DATA_TYPE: + """刷新来自 honey impact 的每日素材表""" from bs4 import Tag - from asyncio import sleep result = {} - for i in range(retry): + for i in range(retry): # 重复尝试 retry 次 try: response = await self.client.get("https://genshin.honeyhunterworld.com/?lang=CHS") soup = BeautifulSoup(response.text, 'lxml') @@ -265,26 +267,27 @@ class DailyMaterial(Plugin, BasePlugin): key: str = '' for tag in calendar: tag: Tag - if tag.name == 'span': + if tag.name == 'span': # 如果是秘境 key = tag.find('a').text result[key] = [[[], []] for _ in range(7)] for day, div in enumerate(tag.find_all('div')): result[key][day][0] = [re.findall(r"/(.*)?/", a['href'])[0] for a in div.find_all('a')] - else: + else: # 如果是角色或武器 id_ = re.findall(r"/(.*)?/", tag['href'])[0] - if tag.text.strip() == '旅行者': + if tag.text.strip() == '旅行者': # 忽略主角 continue - for day in map(int, tag.find('div')['data-days']): + 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])) + 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)) # pylint: disable=PY-W0079 logger.info("每日素材刷新成功") break - except HTTPError: + except (HTTPError, SSLZeroReturnError): + from asyncio import sleep await sleep(1) if i <= retry - 1: logger.warning("每日素材刷新失败, 正在重试") @@ -295,6 +298,7 @@ class DailyMaterial(Plugin, BasePlugin): return result async def _download_icon(self, message: Optional[Message] = None): + """下载素材图标""" from time import time as time_ lock = asyncio.Lock() interval = 2 @@ -303,21 +307,21 @@ class DailyMaterial(Plugin, BasePlugin): async def task(_id, _item, _type): logger.debug(f"正在开始下载 \"{_item[0]}\" 的图标素材") async with lock: - if message is not None and time_() >= the_time.value + interval: + if message is not None and time_() >= the_time.value + interval: # 判定现在是否距离上次修改消息已经有了足够的时间 text = '\n'.join(message.text_html.split('\n')[:2]) + f"\n正在搬运 {_item[0]} 的图标素材。。。" try: await message.edit_text(text, parse_mode=ParseMode.HTML) the_time.value = time_() - except (TimedOut, RetryAfter): + except (TimedOut, RetryAfter): # 修改消息失败 pass - asset = getattr(self.assets_service, _type)(_id) - icon_types = list(filter( + asset = getattr(self.assets_service, _type)(_id) # 获取素材对象 + icon_types = list(filter( # 找到该素材对象的所有图标类型 lambda x: not x.startswith('_') and x not in ['path'] and callable(getattr(asset, x)), dir(asset) )) - icon_coroutines = map(lambda x: getattr(asset, x), icon_types) + icon_coroutines = map(lambda x: getattr(asset, x), icon_types) # 根据图标类型找到下载对应图标的函数 for coroutine in icon_coroutines: - await coroutine() + await coroutine() # 执行下载函数 logger.debug(f"\"{_item[0]}\" 的图标素材下载成功") async with lock: if message is not None and time_() >= the_time.value + interval: @@ -331,38 +335,38 @@ class DailyMaterial(Plugin, BasePlugin): except (TimedOut, RetryAfter): pass - for type_, items in HONEY_ID_MAP.items(): + for type_, items in HONEY_ID_MAP.items(): # 遍历每个对象 task_list = [] for id_, item in items.items(): task_list.append(asyncio.create_task(task(id_, item, type_))) - await asyncio.gather(*task_list) + await asyncio.gather(*task_list) # 等待所有任务执行完成 logger.info("图标素材下载完成") class ItemData(BaseModel): - id: str - name: str - rarity: int - icon: str - level: Optional[int] = None - constellation: Optional[int] = None - refinement: Optional[int] = None - c_path: Optional[str] = None + id: str # ID + name: str # 名称 + rarity: int # 星级 + icon: str # 图标 + level: Optional[int] = None # 等级 + constellation: Optional[int] = None # 命座 + refinement: Optional[int] = None # 精炼度 + c_path: Optional[str] = None # 武器使用者图标 class AreaData(BaseModel): - name: Literal['蒙德', '璃月', '稻妻', '须弥'] - materials: List[ItemData] = [] - items: Iterable[ItemData] = [] + name: Literal['蒙德', '璃月', '稻妻', '须弥'] # 区域名 + materials: List[ItemData] = [] # 区域材料 + items: Iterable[ItemData] = [] # 可培养的角色或武器 class RenderData(BaseModel): - title: str - time: str - uid: Optional[int] = None - character: List[AreaData] = [] - weapon: List[AreaData] = [] + title: str # 页面标题,主要用于显示星期几 + time: str # 页面时间 + uid: Optional[int] = None # 用户UID + character: List[AreaData] = [] # 角色数据 + weapon: List[AreaData] = [] # 武器数据 def __getitem__(self, item): return self.__getattribute__(item) diff --git a/plugins/genshin/weapon.py b/plugins/genshin/weapon.py index 3a1d27c..b702449 100644 --- a/plugins/genshin/weapon.py +++ b/plugins/genshin/weapon.py @@ -1,15 +1,13 @@ -from telegram import InlineKeyboardButton, InlineKeyboardMarkup -from telegram import Update +from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update from telegram.constants import ChatAction -from telegram.ext import CommandHandler, CallbackContext -from telegram.ext import MessageHandler, filters +from telegram.ext import CallbackContext, CommandHandler, MessageHandler, filters +from core.assets import AssetsService from core.baseplugin import BasePlugin from core.plugin import Plugin, handler from core.template import TemplateService from core.wiki.services import WikiService from metadata.shortname import weaponToName -from modules.wiki.base import SCRAPE_HOST from modules.wiki.weapon import Weapon from utils.bot import get_all_args from utils.decorators.error import error_callable @@ -25,9 +23,15 @@ class WeaponPlugin(Plugin, BasePlugin): InlineKeyboardButton(text="查看武器列表并查询", switch_inline_query_current_chat="查看武器列表并查询") ]] - def __init__(self, template_service: TemplateService = None, wiki_service: WikiService = None): + def __init__( + self, + template_service: TemplateService = None, + wiki_service: WikiService = None, + assert_service: AssetsService = None + ): self.wiki_service = wiki_service self.template_service = template_service + self.assert_service = assert_service @handler(CommandHandler, command="weapon", block=False) @handler(MessageHandler, filters=filters.Regex("^武器查询(.*)"), block=False) @@ -47,6 +51,7 @@ class WeaponPlugin(Plugin, BasePlugin): self._add_delete_message_job(context, reply_message.chat_id, reply_message.message_id) return weapon_name = weaponToName(weapon_name) + logger.info(f"用户 {user.full_name}[{user.id}] 查询武器命令请求 || 参数 weapon_name={weapon_name}") weapons_list = await self.wiki_service.get_weapons_list() for weapon in weapons_list: if weapon.name == weapon_name: @@ -59,7 +64,6 @@ class WeaponPlugin(Plugin, BasePlugin): self._add_delete_message_job(context, message.chat_id, message.message_id) self._add_delete_message_job(context, reply_message.chat_id, reply_message.message_id) return - logger.info(f"用户 {user.full_name}[{user.id}] 查询武器命令请求 || 参数 {weapon_name}") await message.reply_chat_action(ChatAction.TYPING) async def input_template_data(_weapon_data: Weapon): @@ -74,11 +78,11 @@ class WeaponPlugin(Plugin, BasePlugin): "weapon_info_type_img": await url_to_file(_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 url_to_file(_weapon_data.icon.icon), + "weapon_info_source_img": (await self.assert_service.weapon(_weapon_data.id).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 url_to_file(str(SCRAPE_HOST.join(f'/img/{mid}.png'))) + (await self.assert_service.material(mid).icon()).as_uri() for mid in _weapon_data.ascension[-3:] ], "special_ability_name": _weapon_data.affix.name, @@ -90,11 +94,11 @@ class WeaponPlugin(Plugin, BasePlugin): "weapon_info_type_img": await url_to_file(_weapon_data.weapon_type.icon_url()), "progression_secondary_stat_value": ' ', "progression_secondary_stat_name": '无其它属性加成', - "weapon_info_source_img": await url_to_file(_weapon_data.icon.icon), + "weapon_info_source_img": (await self.assert_service.weapon(_weapon_data.id).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 url_to_file(str(SCRAPE_HOST.join(f'/img/{mid}.png'))) + (await self.assert_service.material(mid).icon()).as_uri() for mid in _weapon_data.ascension[-3:] ], "special_ability_name": '', diff --git a/utils/helpers.py b/utils/helpers.py index 13c57a0..6b022a0 100644 --- a/utils/helpers.py +++ b/utils/helpers.py @@ -150,3 +150,19 @@ def mkdir(path: Path) -> Path: path_list.pop().mkdir(exist_ok=True) return path + + +def is_number(target: str) -> bool: + """判断字符串是否是数字""" + try: # 尝试将字符串转为浮点数 + float(target) + return True + except ValueError: + pass + try: + import unicodedata # 处理ASCii码的包 + unicodedata.numeric(target) # 把一个表示数字的字符串转换为浮点数返回的函数 + return True + except (TypeError, ValueError): + pass + return False