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