Support zzz challenge

This commit is contained in:
xtaodada 2024-07-15 16:32:25 +08:00
parent 3dbc17b2a2
commit 4ba98e650e
Signed by: xtaodada
GPG Key ID: 4CBB3F4FA8C85659
19 changed files with 1341 additions and 17 deletions

View File

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

View File

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

View File

@ -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花费菲林"},
],
]

View File

@ -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("执行每日刷新历史记录任务完成")

632
plugins/zzz/challenge.py Normal file
View File

@ -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(
"<b>防卫战数据</b>功能使用帮助(中括号表示可选参数)\n\n"
"指令格式:\n<code>/challenge + [层数/all] + [pre]</code>\n<code>pre</code>表示上期)\n\n"
"文本格式:\n<code>防卫战数据 + 查询/总览 + [上期] + [层数]</code> \n\n"
"例如以下指令都正确:\n"
"<code>/challenge</code>\n<code>/challenge 1 pre</code>\n<code>/challenge all pre</code>\n"
"<code>防卫战数据查询</code>\n<code>防卫战数据查询上期第1层</code>\n<code>防卫战数据总览上期</code>",
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"绳匠 (<code>{uid}</code>) {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,
),
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,82 @@
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8">
<title>floor</title>
<link type="text/css" href="./style.css" rel="stylesheet"/>
<link type="text/css" href="../../styles/public.css" rel="stylesheet"/>
<style>
body {
margin: 0;
padding: 0;
}
.floors, .floor {
border-radius: unset;
margin: 0;
}
.floor-num > div:last-child {
display: flex;
flex-flow: column;
justify-content: center;
align-content: center;
}
</style>
</head>
<body>
<div class="container">
<div class="floors">
<div class="floor floor-abyss">
<div class="head">
<div class="floor-name">
<div class="floor-num"></div>
<div>
<div>UID: {{ uid }}</div>
<div>{{ title }}•{{ floor.zone_name }}</div>
</div>
</div>
<div class="star star{{ floor.rating }}"></div>
</div>
<div class="hr"></div>
<div class="chamber">
<div class="chamber-info">
<div>
<span style="color: white;">{{ floor_time }}</span>
</div>
</div>
<div class="battles">
{% for node in floor_nodes %}
<div class="battle">
{% for character in node.avatars %}
<div class="character">
<div class="element" style="background-image: url('../../img/attribute/{{ character.element_type }}.png')"></div>
<div class="icon"
style="background-image: url('../../background/rarity/half/{{ character.rarity }}.png')">
<img src="{{ character.icon }}" alt=""/>
</div>
<div class="caption">Lv.{{ character.level }}</div>
</div>
{% endfor %}
{% if node.buddy %}
{% set character = node.buddy %}
<div class="character">
<div class="icon"
style="background-image: url('../../background/rarity/half/{{ character.rarity }}.png')">
<img src="{{ character.icon }}" alt=""/>
</div>
<div class="caption">Lv.{{ character.level }}</div>
</div>
{% endif %}
{% if loop.length > 1 %}
<div class="battle-info">{{ ['上', '下'][loop.index - 1] }}半</div>
{% endif %}
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="zh-ch">
<head>
<meta charset="UTF-8">
<title>overview</title>
<link type="text/css" href="./style.css" rel="stylesheet"/>
<link type="text/css" href="../../styles/public.css" rel="stylesheet"/>
<style>
body {
margin: 0;
padding: 0;
}
.overview {
border-radius: unset;
}
</style>
</head>
<body>
<div class="container">
{% set summarize_class = '' %}
{% set overview_class = 'overview-abyss' %}
<div class="overview {{ overview_class }}">
<div class="title">{{ title }}挑战回顾</div>
<div class="summarize {{ summarize_class }}">
<div>
<div>UID: {{ uid }}</div>
<div class="star">
{% for star in stars %}
<div class="icon icon2 star1{{ star.rating }}"></div>
<span>*{{ star.times }}</span>
{% endfor %}
</div>
</div>
<div>
<div>本期开始: {{ start_time }}</div>
<div>最深抵达: {{ max_floor }}</div>
</div>
<div>
<div>本期结束: {{ end_time }}</div>
<div>最快通关: {{ max_dura }}</div>
</div>
</div>
</div>
</div>
</body>
</html>

View File

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