diff --git a/core/services/history_data/models.py b/core/services/history_data/models.py index c18e8b1..139720d 100644 --- a/core/services/history_data/models.py +++ b/core/services/history_data/models.py @@ -1,9 +1,8 @@ import enum -from typing import Dict from pydantic import BaseModel -from simnet.models.genshin.chronicle.abyss import SpiralAbyss -from simnet.models.genshin.diary import Diary +from simnet.models.starrail.diary import StarRailDiary +from simnet.models.zzz.chronicle.challenge import ZZZChallenge from gram_core.services.history_data.models import HistoryData @@ -16,13 +15,14 @@ __all__ = ( class HistoryDataTypeEnum(int, enum.Enum): - ABYSS = 0 # 深境螺旋 + ABYSS = 0 # 混沌回忆 + CHALLENGE_STORY = 1 # 虚构叙事 LEDGER = 2 # 开拓月历 + CHALLENGE_BOSS = 3 # 末日幻影 class HistoryDataAbyss(BaseModel): - abyss_data: SpiralAbyss - character_data: Dict[int, int] + abyss_data: ZZZChallenge @classmethod def from_data(cls, data: HistoryData) -> "HistoryDataAbyss": @@ -30,7 +30,7 @@ class HistoryDataAbyss(BaseModel): class HistoryDataLedger(BaseModel): - diary_data: Diary + diary_data: StarRailDiary @classmethod def from_data(cls, data: HistoryData) -> "HistoryDataLedger": diff --git a/core/services/history_data/services.py b/core/services/history_data/services.py index 95d9022..e4660e2 100644 --- a/core/services/history_data/services.py +++ b/core/services/history_data/services.py @@ -1,11 +1,16 @@ import datetime -from typing import Dict, List +from typing import List from pytz import timezone -from simnet.models.genshin.chronicle.abyss import SpiralAbyss -from simnet.models.genshin.diary import Diary +from simnet.models.starrail.diary import StarRailDiary +from simnet.models.zzz.chronicle.challenge import ZZZChallenge -from core.services.history_data.models import HistoryData, HistoryDataTypeEnum, HistoryDataAbyss, HistoryDataLedger +from core.services.history_data.models import ( + HistoryData, + HistoryDataTypeEnum, + HistoryDataAbyss, + HistoryDataLedger, +) from gram_core.base_service import BaseService from gram_core.services.history_data.services import HistoryDataBaseServices @@ -35,12 +40,12 @@ class HistoryDataAbyssServices(BaseService, HistoryDataBaseServices): @staticmethod def exists_data(data: HistoryData, old_data: List[HistoryData]) -> bool: - floor = data.data.get("floors") - return any(d.data.get("floors") == floor for d in old_data) + floors = data.data.get("abyss_data", {}).get("all_floor_detail") + return any(d.data.get("abyss_data", {}).get("all_floor_detail") == floors for d in old_data) @staticmethod - def create(user_id: int, abyss_data: SpiralAbyss, character_data: Dict[int, int]): - data = HistoryDataAbyss(abyss_data=abyss_data, character_data=character_data) + def create(user_id: int, abyss_data: ZZZChallenge): + data = HistoryDataAbyss(abyss_data=abyss_data) json_data = data.json(by_alias=True, encoder=json_encoder) return HistoryData( user_id=user_id, @@ -55,7 +60,7 @@ class HistoryDataLedgerServices(BaseService, HistoryDataBaseServices): DATA_TYPE = HistoryDataTypeEnum.LEDGER.value @staticmethod - def create(user_id: int, diary_data: Diary): + def create(user_id: int, diary_data: StarRailDiary): data = HistoryDataLedger(diary_data=diary_data) json_data = data.json(by_alias=True, encoder=json_encoder) return HistoryData( diff --git a/modules/gacha_log/log.py b/modules/gacha_log/log.py index dcf523a..4eb717c 100644 --- a/modules/gacha_log/log.py +++ b/modules/gacha_log/log.py @@ -448,7 +448,7 @@ class GachaLog: {"num": no_four_star, "unit": "抽", "lable": "未出四星"}, {"num": five_star_const, "unit": "个", "lable": "五星常驻"}, {"num": up_avg, "unit": "抽", "lable": "UP平均"}, - {"num": up_cost, "unit": "", "lable": "UP花费星琼"}, + {"num": up_cost, "unit": "", "lable": "UP花费菲林"}, ], ] diff --git a/plugins/jobs/refresh_history.py b/plugins/jobs/refresh_history.py new file mode 100644 index 0000000..181f7cc --- /dev/null +++ b/plugins/jobs/refresh_history.py @@ -0,0 +1,122 @@ +import datetime +from asyncio import sleep +from typing import TYPE_CHECKING + +from simnet.errors import ( + TimedOut as SimnetTimedOut, + BadRequest as SimnetBadRequest, + InvalidCookies, +) +from telegram.constants import ParseMode +from telegram.error import BadRequest, Forbidden + +from core.plugin import Plugin, job +from core.services.history_data.services import ( + HistoryDataAbyssServices, + HistoryDataLedgerServices, +) +from gram_core.basemodel import RegionEnum +from gram_core.plugin import handler +from gram_core.services.cookies import CookiesService +from gram_core.services.cookies.models import CookiesStatusEnum +from plugins.zzz.challenge import ChallengePlugin +from plugins.tools.genshin import GenshinHelper, PlayerNotFoundError, CookiesNotFoundError +from utils.log import logger + +if TYPE_CHECKING: + from telegram import Update + from telegram.ext import ContextTypes + + from simnet import ZZZClient + +REGION = [RegionEnum.HYPERION, RegionEnum.HOYOLAB] +NOTICE_TEXT = """#### %s更新 #### +时间:%s (UTC+8) +UID: %s +结果: 新的%s已保存,可通过命令回顾""" + + +class RefreshHistoryJob(Plugin): + """历史记录定时刷新""" + + def __init__( + self, + cookies: CookiesService, + genshin_helper: GenshinHelper, + history_abyss: HistoryDataAbyssServices, + history_ledger: HistoryDataLedgerServices, + ): + self.cookies = cookies + self.genshin_helper = genshin_helper + self.history_data_abyss = history_abyss + self.history_data_ledger = history_ledger + + @staticmethod + async def send_notice(context: "ContextTypes.DEFAULT_TYPE", user_id: int, notice_text: str): + try: + await context.bot.send_message(user_id, notice_text, parse_mode=ParseMode.HTML) + except (BadRequest, Forbidden) as exc: + logger.error("执行自动刷新历史记录时发生错误 user_id[%s] Message[%s]", user_id, exc.message) + except Exception as exc: + logger.error("执行自动刷新历史记录时发生错误 user_id[%s]", user_id, exc_info=exc) + + async def save_abyss_data(self, client: "ZZZClient") -> bool: + uid = client.player_id + abyss_data = await client.get_zzz_challenge(uid, previous=False, lang="zh-cn") + if abyss_data.has_data: + return await ChallengePlugin.save_abyss_data(self.history_data_abyss, uid, abyss_data) + return False + + async def send_abyss_notice(self, context: "ContextTypes.DEFAULT_TYPE", user_id: int, uid: int): + now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + notice_text = NOTICE_TEXT % ("防卫战历史记录", now, uid, "挑战记录") + await self.send_notice(context, user_id, notice_text) + + @handler.command(command="remove_same_history", block=False, admin=True) + async def remove_same_history(self, update: "Update", _: "ContextTypes.DEFAULT_TYPE"): + user = update.effective_user + logger.info("用户 %s[%s] remove_same_history 命令请求", user.full_name, user.id) + message = update.effective_message + reply = await message.reply_text("正在执行移除相同数据历史记录任务,请稍后...") + text = "移除相同数据历史记录任务完成\n" + num1 = await self.history_data_abyss.remove_same_data() + text += f"防卫战数据移除数量:{num1}\n" + await reply.edit_text(text) + + @handler.command(command="refresh_all_history", block=False, admin=True) + async def refresh_all_history(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE"): + user = update.effective_user + logger.info("用户 %s[%s] refresh_all_history 命令请求", user.full_name, user.id) + message = update.effective_message + reply = await message.reply_text("正在执行刷新历史记录任务,请稍后...") + await self.daily_refresh_history(context) + await reply.edit_text("全部账号刷新历史记录任务完成") + + @job.run_daily(time=datetime.time(hour=6, minute=1, second=0), name="RefreshHistoryJob") + async def daily_refresh_history(self, context: "ContextTypes.DEFAULT_TYPE"): + logger.info("正在执行每日刷新历史记录任务") + for database_region in REGION: + for cookie_model in await self.cookies.get_all( + region=database_region, status=CookiesStatusEnum.STATUS_SUCCESS + ): + user_id = cookie_model.user_id + try: + async with self.genshin_helper.genshin(user_id) as client: + if await self.save_abyss_data(client): + await self.send_abyss_notice(context, user_id, client.player_id) + except (InvalidCookies, PlayerNotFoundError, CookiesNotFoundError): + continue + except SimnetBadRequest as exc: + logger.warning( + "用户 user_id[%s] 请求历史记录失败 [%s]%s", user_id, exc.ret_code, exc.original or exc.message + ) + continue + except SimnetTimedOut: + logger.info("用户 user_id[%s] 请求历史记录超时", user_id) + continue + except Exception as exc: + logger.error("执行自动刷新历史记录时发生错误 user_id[%s]", user_id, exc_info=exc) + continue + await sleep(1) + + logger.success("执行每日刷新历史记录任务完成") diff --git a/plugins/zzz/challenge.py b/plugins/zzz/challenge.py new file mode 100644 index 0000000..96ce028 --- /dev/null +++ b/plugins/zzz/challenge.py @@ -0,0 +1,632 @@ +"""防卫战数据查询""" + +import asyncio +import math +import re +from functools import lru_cache, partial +from typing import Any, List, Optional, Tuple, Union, TYPE_CHECKING + +from arkowrapper import ArkoWrapper +from pytz import timezone +from simnet.models.zzz.chronicle.challenge import ZZZChallenge +from telegram import Message, Update, InlineKeyboardButton, InlineKeyboardMarkup +from telegram.constants import ChatAction, ParseMode +from telegram.ext import CallbackContext, filters, ContextTypes + +from core.dependence.assets import AssetsService +from core.plugin import Plugin, handler +from core.services.cookies.error import TooManyRequestPublicCookies +from core.services.history_data.models import HistoryDataAbyss +from core.services.history_data.services import HistoryDataAbyssServices +from core.services.template.models import RenderGroupResult, RenderResult +from core.services.template.services import TemplateService +from gram_core.config import config +from gram_core.dependence.redisdb import RedisDB +from gram_core.plugin.methods.inline_use_data import IInlineUseData +from plugins.tools.genshin import GenshinHelper +from utils.enkanetwork import RedisCache +from utils.log import logger +from utils.uid import mask_number + +try: + import ujson as jsonlib + +except ImportError: + import json as jsonlib + +if TYPE_CHECKING: + from simnet import ZZZClient + + +TZ = timezone("Asia/Shanghai") +cmd_pattern = r"(?i)^/challenge(?:@[\w]+)?\s*((?:\d+)|(?:all))?\s*(pre)?" +msg_pattern = r"^防卫战数据((?:查询)|(?:总览))(上期)?\D?(\d*)?.*?$" +MAX_FLOOR = 7 +MAX_STARS = MAX_FLOOR * 3 + + +@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) + if floor > 100: + floor = 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 AbyssFastPassed(Exception): + """快速通过,无数据""" + + +class ChallengePlugin(Plugin): + """防卫战数据查询""" + + def __init__( + self, + template: TemplateService, + helper: GenshinHelper, + assets_service: AssetsService, + history_data_abyss: HistoryDataAbyssServices, + redis: RedisDB, + ): + self.template_service = template + self.helper = helper + self.assets_service = assets_service + self.history_data_abyss = history_data_abyss + self.cache = RedisCache(redis.client, key="plugin:challenge:history") + + async def get_uid(self, user_id: int, reply: Optional[Message], player_id: int, offset: int) -> int: + """通过消息获取 uid,优先级:args > reply > self""" + uid, user_id_ = player_id, user_id + if reply: + try: + user_id_ = reply.from_user.id + except AttributeError: + pass + if not uid: + player_info = await self.helper.players_service.get_player(user_id_, offset=offset) + if player_info is not None: + uid = player_info.player_id + if (not uid) and (user_id_ != user_id): + player_info = await self.helper.players_service.get_player(user_id, offset=offset) + if player_info is not None: + uid = player_info.player_id + return uid + + @handler.command("challenge", block=False) + @handler.message(filters.Regex(msg_pattern), block=False) + async def command_start(self, update: Update, _: CallbackContext) -> None: + user_id = await self.get_real_user_id(update) + message = update.effective_message + uid, offset = self.get_real_uid_or_offset(update) + uid: int = await self.get_uid(user_id, message.reply_to_message, uid, offset) + + # 若查询帮助 + if (message.text.startswith("/") and "help" in message.text) or "帮助" in message.text: + await message.reply_text( + "防卫战数据功能使用帮助(中括号表示可选参数)\n\n" + "指令格式:\n/challenge + [层数/all] + [pre]\n(pre表示上期)\n\n" + "文本格式:\n防卫战数据 + 查询/总览 + [上期] + [层数] \n\n" + "例如以下指令都正确:\n" + "/challenge\n/challenge 1 pre\n/challenge all pre\n" + "防卫战数据查询\n防卫战数据查询上期第1层\n防卫战数据总览上期", + parse_mode=ParseMode.HTML, + ) + self.log_user(update, logger.info, "查询[bold]防卫战数据[/bold]帮助", extra={"markup": True}) + return + + # 解析参数 + floor, total, previous = get_args(message.text) + + if floor > MAX_FLOOR or floor < 0: + reply_msg = await message.reply_text(f"防卫战层数输入错误,请重新输入。支持的参数为: 1-{MAX_FLOOR} 或 all") + if filters.ChatType.GROUPS.filter(message): + self.add_delete_message_job(reply_msg) + self.add_delete_message_job(message) + return + + self.log_user( + update, + logger.info, + "[bold]防卫战挑战数据[/bold]请求: uid=%s floor=%s total=%s previous=%s", + uid, + floor, + total, + previous, + extra={"markup": True}, + ) + + async def reply_message_func(content: str) -> None: + _reply_msg = await message.reply_text(f"绳匠 ({uid}) {content}", parse_mode=ParseMode.HTML) + + reply_text: Optional[Message] = None + + try: + async with self.helper.genshin_or_public(user_id, uid=uid) as client: + if total: + reply_text = await message.reply_text( + f"{config.notice.bot_name} 需要时间整理防卫战数据,还请耐心等待哦~" + ) + await message.reply_chat_action(ChatAction.TYPING) + abyss_data = await self.get_rendered_pic_data(client, uid, previous) + images = await self.get_rendered_pic(abyss_data, uid, floor, total) + 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 + except AbyssUnlocked: # 若防卫战未解锁 + await reply_message_func("还未解锁防卫战哦~") + return + except AbyssFastPassed: # 若防卫战已快速通过 + await reply_message_func("本层已被快速通过,无详细数据~") + return + except IndexError: # 若防卫战为挑战此层 + await reply_message_func("还没有挑战本层呢,咕咕咕~") + return + except ValueError as e: + if uid: + await reply_message_func("UID 输入错误,请重新输入") + return + raise e + 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, write_timeout=60) + + if reply_text is not None: + await reply_text.delete() + + self.log_user(update, logger.info, "[bold]防卫战挑战数据[/bold]: 成功发送图片", extra={"markup": True}) + + @staticmethod + def get_floor_data(abyss_data: "ZZZChallenge", floor: int): + try: + floor_data = abyss_data.floors[-floor] + except IndexError: + floor_data = None + if not floor_data: + raise AbyssUnlocked() + render_data = { + "floor": floor_data, + "floor_time": floor_data.floor_challenge_time.datetime.astimezone(TZ).strftime("%Y-%m-%d %H:%M:%S"), + "floor_nodes": [floor_data.node_1, floor_data.node_2], + "floor_num": floor, + } + return render_data + + async def get_rendered_pic_data(self, client: "ZZZClient", uid: int, previous: bool) -> "ZZZChallenge": + abyss_data = await client.get_zzz_challenge(uid, previous=previous, lang="zh-cn") + if abyss_data.has_data: + await self.save_abyss_data(self.history_data_abyss, uid, abyss_data) + return abyss_data + + @staticmethod + def from_seconds_to_hours(seconds: int) -> str: + hours = seconds / 3600 + minutes = (seconds % 3600) / 60 + sec = seconds % 60 + return f"{int(hours)}时{int(minutes)}分{int(sec)}秒" + + async def get_rendered_pic( # skipcq: PY-R1000 # + self, abyss_data: "ZZZChallenge", uid: int, floor: int, total: bool + ) -> Union[ + Tuple[ + Union[BaseException, Any], + Union[BaseException, Any], + Union[BaseException, Any], + Union[BaseException, Any], + Union[BaseException, Any], + ], + List[RenderResult], + None, + ]: + """ + 获取渲染后的图片 + + Args: + abyss_data (ZZZChallenge): 防卫战数据 + uid (int): 需要查询的 uid + floor (int): 层数 + total (bool): 是否为总览 + + Returns: + bytes格式的图片 + """ + + if not abyss_data.has_data: + raise AbyssUnlocked() + start_time = abyss_data.begin_time.datetime.astimezone(TZ).strftime("%m月%d日 %H:%M") + end_time = abyss_data.end_time.datetime.astimezone(TZ).strftime("%m月%d日 %H:%M") + dura = self.from_seconds_to_hours(abyss_data.fast_layer_time) + max_floor_map = {1: "一", 2: "二", 3: "三", 4: "四", 5: "五", 6: "六", 7: "七"} + max_floor = f"第{max_floor_map.get(abyss_data.max_layer, abyss_data.max_layer)}防线" + + render_data = { + "title": "防卫战", + "start_time": start_time, + "end_time": end_time, + "stars": abyss_data.rating_list, + "uid": mask_number(uid), + "max_floor": max_floor, + "max_dura": dura, + "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", + }, + } + + overview = await self.template_service.render( + "zzz/abyss/overview.html", render_data, viewport={"width": 750, "height": 250} + ) + + if total: + + def floor_task(floor_index: int): + _abyss_data = self.get_floor_data(abyss_data, floor_index) + return ( + floor_index, + self.template_service.render( + "zzz/abyss/floor.html", + { + **render_data, + **_abyss_data, + }, + viewport={"width": 690, "height": 500}, + full_page=True, + ttl=15 * 24 * 60 * 60, + ), + ) + + render_inputs = [] + floors = abyss_data.floors[::-1] + for i, f in enumerate(floors): + try: + render_inputs.append(floor_task(i + 1)) + except AbyssFastPassed: + pass + + render_group_inputs = list(map(lambda x: x[1], sorted(render_inputs, key=lambda x: x[0]))) + + render_group_outputs = await asyncio.gather(*render_group_inputs) + render_group_outputs.insert(0, overview) + return render_group_outputs + + if floor < 1: + return [overview] + try: + floor_data = abyss_data.floors[-floor] + except IndexError: + return None + if not floor_data: + return None + render_data.update(self.get_floor_data(abyss_data, floor)) + return [ + await self.template_service.render( + "zzz/abyss/floor.html", render_data, viewport={"width": 690, "height": 500} + ) + ] + + @staticmethod + async def save_abyss_data( + history_data_abyss: "HistoryDataAbyssServices", uid: int, abyss_data: "ZZZChallenge" + ) -> bool: + model = history_data_abyss.create(uid, abyss_data) + old_data = await history_data_abyss.get_by_user_id_data_id(uid, model.data_id) + exists = history_data_abyss.exists_data(model, old_data) + if not exists: + await history_data_abyss.add(model) + return True + return False + + async def get_abyss_data(self, uid: int): + return await self.history_data_abyss.get_by_user_id(uid) + + @staticmethod + def get_season_data_name(data: "HistoryDataAbyss"): + last_battles = data.abyss_data.floors[0] + start_time = last_battles.floor_challenge_time.datetime.astimezone(TZ) + time = start_time.strftime("%Y.%m.%d") + name = "" + if "第" in last_battles.zone_name: + name = last_battles.zone_name.split("第")[0] + honor = "" + if data.abyss_data.total_stars == MAX_STARS: + honor = "👑" + num_of_characters = max( + len(last_battles.node_1.avatars), + len(last_battles.node_2.avatars), + ) + if num_of_characters == 2: + honor = "双通" + elif num_of_characters == 1: + honor = "单通" + + return f"{name} {time} {data.abyss_data.total_stars} ★ {honor}".strip() + + async def get_session_button_data(self, user_id: int, uid: int, force: bool = False): + redis = await self.cache.get(str(uid)) + if redis and not force: + return redis["buttons"] + data = await self.get_abyss_data(uid) + data.sort(key=lambda x: x.id, reverse=True) + abyss_data = [HistoryDataAbyss.from_data(i) for i in data] + buttons = [ + { + "name": self.get_season_data_name(abyss_data[idx]), + "value": f"get_abyss_history|{user_id}|{uid}|{value.id}", + } + for idx, value in enumerate(data) + ] + await self.cache.set(str(uid), {"buttons": buttons}) + return buttons + + async def gen_season_button( + self, + user_id: int, + uid: int, + page: int = 1, + ) -> List[List[InlineKeyboardButton]]: + """生成按钮""" + data = await self.get_session_button_data(user_id, uid) + if not data: + return [] + buttons = [ + InlineKeyboardButton( + value["name"], + callback_data=value["value"], + ) + for value in data + ] + all_buttons = [buttons[i : i + 2] for i in range(0, len(buttons), 2)] + send_buttons = all_buttons[(page - 1) * 7 : page * 7] + last_page = page - 1 if page > 1 else 0 + all_page = math.ceil(len(all_buttons) / 7) + 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_abyss_history|{user_id}|{uid}|p_{last_page}", + ) + ) + if last_page or next_page: + last_button.append( + InlineKeyboardButton( + f"{page}/{all_page}", + callback_data=f"get_abyss_history|{user_id}|{uid}|empty_data", + ) + ) + if next_page: + last_button.append( + InlineKeyboardButton( + "下一页 >>", + callback_data=f"get_abyss_history|{user_id}|{uid}|p_{next_page}", + ) + ) + if last_button: + send_buttons.append(last_button) + return send_buttons + + @staticmethod + async def gen_floor_button( + data_id: int, + abyss_data: "HistoryDataAbyss", + user_id: int, + uid: int, + ) -> List[List[InlineKeyboardButton]]: + max_floors = len(abyss_data.abyss_data.floors) + buttons = [ + InlineKeyboardButton( + f"第 {i + 1} 层", + callback_data=f"get_abyss_history|{user_id}|{uid}|{data_id}|{i + 1}", + ) + for i in range(max_floors) + ] + send_buttons = [buttons[i : i + 4] for i in range(0, len(buttons), 4)] + all_buttons = [ + InlineKeyboardButton( + "<< 返回", + callback_data=f"get_abyss_history|{user_id}|{uid}|p_1", + ), + InlineKeyboardButton( + "总览", + callback_data=f"get_abyss_history|{user_id}|{uid}|{data_id}|total", + ), + InlineKeyboardButton( + "所有", + callback_data=f"get_abyss_history|{user_id}|{uid}|{data_id}|all", + ), + ] + send_buttons.append(all_buttons) + return send_buttons + + @handler.command("challenge_history", block=False) + @handler.message(filters.Regex(r"^防卫战历史数据"), block=False) + async def abyss_history_command_start(self, update: Update, _: CallbackContext) -> None: + user_id = await self.get_real_user_id(update) + message = update.effective_message + uid, offset = self.get_real_uid_or_offset(update) + uid: int = await self.get_uid(user_id, message.reply_to_message, uid, offset) + self.log_user(update, logger.info, "查询防卫战历史数据 uid[%s]", uid) + + async with self.helper.genshin_or_public(user_id, uid=uid) as _: + await self.get_session_button_data(user_id, uid, force=True) + buttons = await self.gen_season_button(user_id, uid) + if not buttons: + await message.reply_text("还没有防卫战历史数据哦~") + return + await message.reply_text("请选择要查询的防卫战历史数据", reply_markup=InlineKeyboardMarkup(buttons)) + + async def get_abyss_history_page(self, update: "Update", user_id: int, uid: int, result: str): + """翻页处理""" + callback_query = update.callback_query + + self.log_user(update, logger.info, "切换防卫战历史数据页 page[%s]", result) + page = int(result.split("_")[1]) + async with self.helper.genshin_or_public(user_id) as _: + buttons = await self.gen_season_button(user_id, uid, page) + if not buttons: + await callback_query.answer("还没有防卫战历史数据哦~", show_alert=True) + await callback_query.edit_message_text("还没有防卫战历史数据哦~") + return + await callback_query.edit_message_reply_markup(reply_markup=InlineKeyboardMarkup(buttons)) + await callback_query.answer(f"已切换到第 {page} 页", show_alert=False) + + async def get_abyss_history_season(self, update: "Update", data_id: int): + """进入选择层数""" + callback_query = update.callback_query + user = callback_query.from_user + + self.log_user(update, logger.info, "切换防卫战历史数据到层数页 data_id[%s]", data_id) + data = await self.history_data_abyss.get_by_id(data_id) + if not data: + await callback_query.answer("数据不存在,请尝试重新发送命令~", show_alert=True) + await callback_query.edit_message_text("数据不存在,请尝试重新发送命令~") + return + abyss_data = HistoryDataAbyss.from_data(data) + buttons = await self.gen_floor_button(data_id, abyss_data, user.id, data.user_id) + await callback_query.edit_message_reply_markup(reply_markup=InlineKeyboardMarkup(buttons)) + await callback_query.answer("已切换到层数页", show_alert=False) + + async def get_abyss_history_floor(self, update: "Update", data_id: int, detail: str): + """渲染层数数据""" + callback_query = update.callback_query + message = callback_query.message + reply = None + if message.reply_to_message: + reply = message.reply_to_message + + floor = 0 + total = False + if detail == "total": + floor = 0 + elif detail == "all": + total = True + else: + floor = int(detail) + data = await self.history_data_abyss.get_by_id(data_id) + if not data: + await callback_query.answer("数据不存在,请尝试重新发送命令", show_alert=True) + await callback_query.edit_message_text("数据不存在,请尝试重新发送命令~") + return + abyss_data = HistoryDataAbyss.from_data(data) + + images = await self.get_rendered_pic(abyss_data.abyss_data, data.user_id, floor, total) + if images is None: + await callback_query.answer(f"还没有第 {floor} 层的挑战数据", show_alert=True) + return + await callback_query.answer("正在渲染图片中 请稍等 请不要重复点击按钮", show_alert=False) + + await message.reply_chat_action(ChatAction.UPLOAD_PHOTO) + + for group in ArkoWrapper(images).group(10): # 每 10 张图片分一个组 + await RenderGroupResult(results=group).reply_media_group(reply or message, write_timeout=60) + self.log_user(update, logger.info, "[bold]防卫战挑战数据[/bold]: 成功发送图片", extra={"markup": True}) + self.add_delete_message_job(message, delay=1) + + @handler.callback_query(pattern=r"^get_abyss_history\|", block=False) + async def get_abyss_history(self, update: "Update", _: "ContextTypes.DEFAULT_TYPE") -> None: + callback_query = update.callback_query + user = callback_query.from_user + + async def get_abyss_history_callback( + callback_query_data: str, + ) -> Tuple[str, str, int, int]: + _data = callback_query_data.split("|") + _user_id = int(_data[1]) + _uid = int(_data[2]) + _result = _data[3] + _detail = _data[4] if len(_data) > 4 else None + logger.debug( + "callback_query_data函数返回 detail[%s] result[%s] user_id[%s] uid[%s]", + _detail, + _result, + _user_id, + _uid, + ) + return _detail, _result, _user_id, _uid + + detail, result, user_id, uid = await get_abyss_history_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 + if result.startswith("p_"): + await self.get_abyss_history_page(update, user_id, uid, result) + return + data_id = int(result) + if detail: + await self.get_abyss_history_floor(update, data_id, detail) + return + await self.get_abyss_history_season(update, data_id) + + async def abyss_use_by_inline(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE", previous: bool): + callback_query = update.callback_query + user = update.effective_user + user_id = user.id + uid = IInlineUseData.get_uid_from_context(context) + + self.log_user(update, logger.info, "查询防卫战挑战总览数据 previous[%s]", previous) + notice = None + try: + async with self.helper.genshin_or_public(user_id, uid=uid) as client: + if not client.public: + await client.get_record_cards() + abyss_data = await self.get_rendered_pic_data(client, uid, previous) + images = await self.get_rendered_pic(abyss_data, uid, 0, False) + image = images[0] + except AbyssUnlocked: # 若深渊未解锁 + notice = "还未解锁防卫战哦~" + except TooManyRequestPublicCookies: + notice = "查询次数太多,请您稍后重试" + + if notice: + await callback_query.answer(notice, show_alert=True) + return + + await image.edit_inline_media(callback_query) + + async def get_inline_use_data(self) -> List[Optional[IInlineUseData]]: + return [ + IInlineUseData( + text="本期防卫战总览", + hash="challenge_current", + callback=partial(self.abyss_use_by_inline, previous=False), + player=True, + ), + IInlineUseData( + text="上期防卫战总览", + hash="challenge_previous", + callback=partial(self.abyss_use_by_inline, previous=True), + player=True, + ), + ] diff --git a/resources/background/rarity/half/A.png b/resources/background/rarity/half/A.png new file mode 100644 index 0000000..8079547 Binary files /dev/null and b/resources/background/rarity/half/A.png differ diff --git a/resources/background/rarity/half/B.png b/resources/background/rarity/half/B.png new file mode 100644 index 0000000..44aa8c3 Binary files /dev/null and b/resources/background/rarity/half/B.png differ diff --git a/resources/background/rarity/half/C.png b/resources/background/rarity/half/C.png new file mode 100644 index 0000000..c1d5363 Binary files /dev/null and b/resources/background/rarity/half/C.png differ diff --git a/resources/background/rarity/half/D.png b/resources/background/rarity/half/D.png new file mode 100644 index 0000000..5811598 Binary files /dev/null and b/resources/background/rarity/half/D.png differ diff --git a/resources/background/rarity/half/S.png b/resources/background/rarity/half/S.png new file mode 100644 index 0000000..2630db5 Binary files /dev/null and b/resources/background/rarity/half/S.png differ diff --git a/resources/zzz/abyss/background/abyss-bg-grad.png b/resources/zzz/abyss/background/abyss-bg-grad.png new file mode 100644 index 0000000..281dea6 Binary files /dev/null and b/resources/zzz/abyss/background/abyss-bg-grad.png differ diff --git a/resources/zzz/abyss/background/abyss.png b/resources/zzz/abyss/background/abyss.png new file mode 100644 index 0000000..3addb52 Binary files /dev/null and b/resources/zzz/abyss/background/abyss.png differ diff --git a/resources/zzz/abyss/background/banner 01.png b/resources/zzz/abyss/background/banner 01.png new file mode 100644 index 0000000..d6d1ae9 Binary files /dev/null and b/resources/zzz/abyss/background/banner 01.png differ diff --git a/resources/zzz/abyss/background/banner 02.png b/resources/zzz/abyss/background/banner 02.png new file mode 100644 index 0000000..f948a1f Binary files /dev/null and b/resources/zzz/abyss/background/banner 02.png differ diff --git a/resources/zzz/abyss/background/roleStarBg4.png b/resources/zzz/abyss/background/roleStarBg4.png new file mode 100644 index 0000000..8079547 Binary files /dev/null and b/resources/zzz/abyss/background/roleStarBg4.png differ diff --git a/resources/zzz/abyss/background/roleStarBg5.png b/resources/zzz/abyss/background/roleStarBg5.png new file mode 100644 index 0000000..2630db5 Binary files /dev/null and b/resources/zzz/abyss/background/roleStarBg5.png differ diff --git a/resources/zzz/abyss/floor.html b/resources/zzz/abyss/floor.html new file mode 100644 index 0000000..a9cedcf --- /dev/null +++ b/resources/zzz/abyss/floor.html @@ -0,0 +1,82 @@ + + + + + floor + + + + + +
+
+
+
+
+
+
+
UID: {{ uid }}
+
{{ title }}•{{ floor.zone_name }}
+
+
+
+
+
+
+
+
+ {{ floor_time }} +
+
+
+ {% for node in floor_nodes %} +
+ {% for character in node.avatars %} +
+
+
+ +
+
Lv.{{ character.level }}
+
+ {% endfor %} + {% if node.buddy %} + {% set character = node.buddy %} +
+
+ +
+
Lv.{{ character.level }}
+
+ {% endif %} + {% if loop.length > 1 %} +
{{ ['上', '下'][loop.index - 1] }}半
+ {% endif %} +
+ {% endfor %} +
+
+
+
+
+ + \ No newline at end of file diff --git a/resources/zzz/abyss/overview.html b/resources/zzz/abyss/overview.html new file mode 100644 index 0000000..6c7b49a --- /dev/null +++ b/resources/zzz/abyss/overview.html @@ -0,0 +1,47 @@ + + + + + overview + + + + + +
+ {% set summarize_class = '' %} + {% set overview_class = 'overview-abyss' %} +
+
{{ title }}挑战回顾
+
+
+
UID: {{ uid }}
+
+ {% for star in stars %} +
+ *{{ star.times }} + {% endfor %} +
+
+
+
本期开始: {{ start_time }}
+
最深抵达: {{ max_floor }}
+
+
+
本期结束: {{ end_time }}
+
最快通关: {{ max_dura }}
+
+
+
+
+ + \ No newline at end of file diff --git a/resources/zzz/abyss/style.css b/resources/zzz/abyss/style.css new file mode 100644 index 0000000..6effe2c --- /dev/null +++ b/resources/zzz/abyss/style.css @@ -0,0 +1,436 @@ +:root { + --white: rgb(246 248 249); + --bg-color: rgb(233 229 220); + --h-color: rgb(203 189 162); + --red: rgb(255 86 33/ 80%); + --blue: rgb(98 168 233/ 80%); + --green: rgb(67 185 124/ 80%); +} + +body { + margin: 0; + padding: 10px; +} + +.hr { + width: 100%; + height: 3px; + background-color: rgb(246 248 249 / 50%); +} + +.container { + width: 750px; + position: relative; + filter: drop-shadow(2px 2px 5px rgb(0 0 0 /70%)); +} + +.container2 { + width: 850px; + position: relative; + filter: drop-shadow(2px 2px 5px rgb(0 0 0 /70%)); +} + +.title { + text-align: center; + font-size: 27px; + font-weight: bold; + color: var(--h-color); +} + +.caption { + margin: 10px 0; + color: var(--h-color); + font-size: 20px; +} + +/* 概览 */ + +.overview { + /*height: 250px;*/ + padding: 20px 40px; + background-size: cover; + background-repeat: no-repeat; + background-position: center; + background-attachment: local; + border-radius: 15px; + overflow: hidden; +} + +.overview-abyss { + background-image: linear-gradient(to top, rgb(0 0 0 / 10%), rgb(0 0 0 / 10%)), url("./background/abyss-bg-grad.png"); +} + +.summarize { + font-size: 20px; + width: calc(100% - 90px); + height: calc(150px - 20px); + margin: 20px 0; + padding: 10px 0 10px 80px; + border-radius: 5px; + border: 2px solid rgb(118 121 120 / 80%); + outline: 4px solid rgb(74, 82, 101); + background-color: rgb(74 82 101 / 60%); + background-image: url("./background/banner 01.png"), url("./background/banner 02.png"); + background-repeat: no-repeat, no-repeat; + background-position: right, left; + background-size: auto 100%, auto 100%; + backdrop-filter: blur(5px); +} + +.summarize-boss { + height: 200px; +} + +.summarize-boss > div { + height: 25%!important; +} + +.summarize > div { + width: 100%; + height: 33.3%; + color: var(--white); + display: flex; + align-items: center; +} + +.summarize > div > div { + flex: 1; +} + +.star { + display: inline-flex; + align-items: center; + position: relative; +} + +.star::before { + content: ""; + width: 40px; + height: 40px; + background-size: cover; +} + +.star1S, .starS::before { + background-image: url("../../img/rarity/S.png"); +} + +.star1A, .starA::before { + background-image: url("../../img/rarity/A.png"); +} + +.star1B, .starB::before { + background-image: url("../../img/rarity/B.png"); +} + +.star > span { + margin-left: 10px; +} + +.star > div { + margin-left: 10px; +} + +.star > div > div { + text-align: center; +} + +.most-played { + width: 100%; + margin: 30px 0; +} + +.characters { + width: calc(100% - 60px); + padding: 0 30px; + display: flex; + justify-content: center; + align-items: center; +} + +.character { + width: 100px; + height: 125px; + margin: 0 20px; + background-color: rgb(233 229 220); + overflow: hidden; + border-radius: 10px; + position: relative; +} + +.characters > .character > .element { + position: absolute; + top: 3px; + left: 3px; + width: 25px; + height: 25px; + background-size: contain; + background-position: center; + background-repeat: no-repeat; +} + +.icon { + width: 100%; + height: 100px; + background-size: cover; + background-repeat: no-repeat; + background-position: center; + overflow: hidden; + border-radius: 0 0 20px 0; + display: flex; + flex-direction: column; + flex-wrap: wrap; + align-items: center; + justify-content: space-around; +} + +.icon2 { + width: 20px; + height: 20px; +} + +.icon > img { + width: inherit; + height: inherit; +} + +.character > .caption { + font-size: 18px; + margin: 4px 0 0; + padding: 0; + height: min-content; + text-align: center; + color: black; +} + +.four-star { + width: 15px; + height: 15px; + position: relative; +} + +.four-star::after { + content: ''; + position: absolute; + top: 0; + left: 0; + z-index: 1; + width: inherit; + height: inherit; + background-position: center; + background-image: url("./background/four-star.png"); + background-blend-mode: lighten; + background-size: contain; +} + +.ranks { + width: calc(100% + 80px); + transform: translateX(-40px); + color: var(--white); + position: relative; + display: flex; + flex-flow: column; + flex-wrap: wrap; +} + +.ranks > div { + font-size: 18px; + width: 100%; + height: 43px; + display: flex; + align-items: center; +} + +.rank { + flex: 1; + position: relative; + display: flex; + margin-left: 80px; + margin-right: 60px; +} + +.ranks > div > .rank:first-child { + margin-right: 10px; +} + +.rank > img { + width: 50px; + position: absolute; + right: 0; + bottom: -5px; +} + +/* 层数 */ + +.floors { + margin-top: 20px; +} + +.floor { + margin: 20px 0; + padding: 0 30px; + width: calc(100% - 60px); + border-radius: 20px; + overflow: hidden; + outline: rgb(233 229 220 / 30%) solid 3px; + outline-offset: -7px; + background-position: top center; + background-repeat: no-repeat; +} + +.floor-abyss { + background-image: url('./background/abyss-bg-grad.png'); +} + +.head { + width: 100%; + height: 160px; + display: flex; +} + +.floor-name { + font-size: 28px; + display: flex; + align-items: center; + font-weight: bold; + color: var(--white); +} + +.floor-name > div:not(.floor-num) { + margin-left: 20px; + text-shadow: 1px 1px 5px rgb(0 0 0/50%); +} + +.floor-num { + color: black; + width: 100px; + height: 100px; + background-image: url("./background/abyss.png"); + background-size: contain; + background-repeat: no-repeat; + filter: drop-shadow(0 0 5px rgb(0 0 0/80%)) grayscale(10%); + position: relative; +} + +.head > .star { + margin-left: auto; + float: right; + color: var(--white); + font-size: 25px; + text-shadow: 1px 1px 5px rgb(0 0 0/50%); +} + +.head > .star::before { + width: 50px; + height: 50px; +} + +.chamber { + margin: 10px 0; + display: flex; + flex-flow: column; +} + +.chamber-info { + height: 70px; + display: flex; + align-items: center; + color: var(--white); + font-size: 22px; +} + +.stars { + margin-left: auto; +} + +.dim-star::before { + filter: brightness(50%); +} + +.battle { + margin: 10px 0 20px 0; + display: flex; + justify-content: center; + align-items: center; +} + +.battle > .character { + flex: 2; + max-width: 120px; + height: 170px; +} + +.battle > .battle-info { + flex: 1; + color: var(--white); + font-size: 22px; + font-weight: bold; +} + +.battle > .character > div:first-child:not(.icon, .element) { + position: absolute; + top: 0; + right: 0; + padding: 5px; + min-width: 27px; + text-align: center; + border-radius: 0 0 0 10px; + filter: drop-shadow(1px 1px 5px rgb(0 0 0/50%)); + font-weight: 500; + color: var(--white); +} + +.battle > .character > .element { + position: absolute; + top: 3px; + left: 3px; + width: 30px; + height: 30px; + background-size: contain; + background-position: center; + background-repeat: no-repeat; +} + +.battle > .character > .icon { + width: 100%; + height: fit-content; + border-radius: 0 0 28px 0; +} + +.battle > .character > .caption { + font-size: 20px; + margin-top: 2px; +} + +.score { + font-size: medium; +} + +.buffs { + margin-top: 10px; + margin-bottom: 10px; +} + +.buffs > .buff-item { + background-color: hsla(0,0%,100%,.08); + display: flex; + color: #d9d9d9; +} + +.buffs > .buff-item > .buff-item-icon { + display: flex; + align-items: center; + width: 35px; + height: 35px; + -ms-flex-negative: 0; + flex-shrink: 0; + margin-top: 7px; + margin-right: 10px; + margin-left: 20px; + background-color: rgba(0,0,0,.7); + border-radius: 50%; + border: 1px solid hsla(0,0%,100%,.7); +} + +.buffs > .buff-item > p > .buff-item-name { + color: #f9c87e; +}