Update /play_card placeholder image

Update current player showcase after updating character list.
Use asynchronous locks to make file read/write thread-safe.
Update `EnkaNetworkCache`.

---------

Co-authored-by: 洛水居室 <luoshuijs@outlook.com>
This commit is contained in:
LittleMengBot 2023-03-17 13:33:29 +08:00 committed by GitHub
parent b60094eef0
commit 72154924be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 463 additions and 58 deletions

View File

@ -63,7 +63,7 @@ class PlayerInfoService(BaseService):
self.cache = redis.client self.cache = redis.client
self._players_info_repository = players_info_repository self._players_info_repository = players_info_repository
self.enka_client = EnkaNetworkAPI(lang="chs", user_agent=config.enka_network_api_agent) self.enka_client = EnkaNetworkAPI(lang="chs", user_agent=config.enka_network_api_agent)
self.enka_client.set_cache(RedisCache(redis.client, key="players_info:enka_network", ttl=60)) self.enka_client.set_cache(RedisCache(redis.client, key="players_info:enka_network", ex=60))
self.qname = "players_info" self.qname = "players_info"
async def get_form_cache(self, player: Player): async def get_form_cache(self, player: Player):

View File

@ -1,3 +1,4 @@
import asyncio
from pathlib import Path from pathlib import Path
from typing import Optional, Dict, Union from typing import Optional, Dict, Union
@ -5,13 +6,19 @@ import aiofiles
from utils.const import PROJECT_ROOT from utils.const import PROJECT_ROOT
import ujson as jsonlib try:
import ujson as jsonlib
except ImportError:
import json as jsonlib
PLAYER_CARDS_PATH = PROJECT_ROOT.joinpath("data", "apihelper", "player_cards") PLAYER_CARDS_PATH = PROJECT_ROOT.joinpath("data", "apihelper", "player_cards")
PLAYER_CARDS_PATH.mkdir(parents=True, exist_ok=True) PLAYER_CARDS_PATH.mkdir(parents=True, exist_ok=True)
class PlayerCardsFile: class PlayerCardsFile:
_lock = asyncio.Lock()
def __init__(self, player_cards_path: Path = PLAYER_CARDS_PATH): def __init__(self, player_cards_path: Path = PLAYER_CARDS_PATH):
self.player_cards_path = player_cards_path self.player_cards_path = player_cards_path
@ -53,6 +60,7 @@ class PlayerCardsFile:
uid: Union[str, int], uid: Union[str, int],
data: Dict, data: Dict,
) -> Dict: ) -> Dict:
async with self._lock:
old_data = await self.load_history_info(uid) old_data = await self.load_history_info(uid)
if old_data is None: if old_data is None:
await self.save_json(self.get_file_path(uid), data) await self.save_json(self.get_file_path(uid), data)

View File

@ -67,7 +67,7 @@ class AvatarListPlugin(Plugin):
self.assets_service = assets_service self.assets_service = assets_service
self.template_service = template_service self.template_service = template_service
self.enka_client = EnkaNetworkAPI(lang="chs", user_agent=config.enka_network_api_agent) self.enka_client = EnkaNetworkAPI(lang="chs", user_agent=config.enka_network_api_agent)
self.enka_client.set_cache(RedisCache(redis.client, key="plugin:avatar_list:enka_network", ttl=60 * 60 * 3)) self.enka_client.set_cache(RedisCache(redis.client, key="plugin:avatar_list:enka_network", ex=60 * 60 * 3))
self.enka_assets = EnkaAssets(lang="chs") self.enka_assets = EnkaAssets(lang="chs")
self.helper = helper self.helper = helper
self.character_details = character_details self.character_details = character_details

View File

@ -1,14 +1,12 @@
import math import math
from typing import Any, List, Tuple, Union, Optional from typing import Any, List, Tuple, Union, Optional, TYPE_CHECKING
from enkanetwork import ( from enkanetwork import (
CharacterInfo,
DigitType, DigitType,
EnkaNetworkAPI, EnkaNetworkAPI,
EnkaNetworkResponse, EnkaNetworkResponse,
EnkaServerError, EnkaServerError,
Equipments, Equipments,
EquipmentsStats,
EquipmentsType, EquipmentsType,
HTTPException, HTTPException,
Stats, Stats,
@ -20,13 +18,13 @@ from enkanetwork import (
EnkaPlayerNotFound, EnkaPlayerNotFound,
) )
from pydantic import BaseModel from pydantic import BaseModel
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update from telegram import InlineKeyboardButton, InlineKeyboardMarkup
from telegram.constants import ChatAction from telegram.constants import ChatAction
from telegram.ext import CallbackContext, CommandHandler, MessageHandler, filters from telegram.ext import CommandHandler, MessageHandler, filters
from telegram.helpers import create_deep_linked_url from telegram.helpers import create_deep_linked_url
from core.config import config from core.config import config
from core.dependence.assets import DEFAULT_EnkaAssets from core.dependence.assets import DEFAULT_EnkaAssets, AssetsService
from core.dependence.redisdb import RedisDB from core.dependence.redisdb import RedisDB
from core.handler.callbackqueryhandler import CallbackQueryHandler from core.handler.callbackqueryhandler import CallbackQueryHandler
from core.plugin import Plugin, handler from core.plugin import Plugin, handler
@ -40,6 +38,11 @@ from utils.helpers import download_resource
from utils.log import logger from utils.log import logger
from utils.patch.aiohttp import AioHttpTimeoutException from utils.patch.aiohttp import AioHttpTimeoutException
if TYPE_CHECKING:
from enkanetwork import CharacterInfo, EquipmentsStats
from telegram.ext import ContextTypes
from telegram import Update
try: try:
import ujson as jsonlib import ujson as jsonlib
except ImportError: except ImportError:
@ -47,15 +50,22 @@ except ImportError:
class PlayerCards(Plugin): class PlayerCards(Plugin):
def __init__(self, player_service: PlayersService, template_service: TemplateService, redis: RedisDB): def __init__(
self,
player_service: PlayersService,
template_service: TemplateService,
assets_service: AssetsService,
redis: RedisDB,
):
self.player_service = player_service self.player_service = player_service
self.client = EnkaNetworkAPI(lang="chs", user_agent=config.enka_network_api_agent, cache=False) self.client = EnkaNetworkAPI(lang="chs", user_agent=config.enka_network_api_agent, cache=False)
self.cache = RedisCache(redis.client, key="plugin:player_cards:enka_network") self.cache = RedisCache(redis.client, key="plugin:player_cards:enka_network", ex=60)
self.player_cards_file = PlayerCardsFile() self.player_cards_file = PlayerCardsFile()
self.assets_service = assets_service
self.template_service = template_service self.template_service = template_service
self.temp_photo: Optional[str] = None self.kitsune: Optional[str] = None
async def _fetch_user(self, uid) -> Union[EnkaNetworkResponse, str]: async def _update_enka_data(self, uid) -> Union[EnkaNetworkResponse, str]:
try: try:
data = await self.cache.get(uid) data = await self.cache.get(uid)
if data is not None: if data is not None:
@ -82,25 +92,35 @@ class PlayerCards(Plugin):
error = "未找到玩家请检查您的UID/用户名" error = "未找到玩家请检查您的UID/用户名"
except HTTPException: except HTTPException:
error = "Enka.Network HTTP 服务请求错误,请稍后重试" error = "Enka.Network HTTP 服务请求错误,请稍后重试"
old_data = await self.player_cards_file.load_history_info(uid)
if old_data is not None:
logger.warning("UID %s | 角色卡片使用历史数据 | %s", uid, error)
return EnkaNetworkResponse.parse_obj(old_data)
return error return error
async def _load_history(self, uid) -> Optional[EnkaNetworkResponse]:
data = await self.player_cards_file.load_history_info(uid)
if data is None:
return None
return EnkaNetworkResponse.parse_obj(data)
@handler(CommandHandler, command="player_card", block=False) @handler(CommandHandler, command="player_card", block=False)
@handler(MessageHandler, filters=filters.Regex("^角色卡片查询(.*)"), block=False) @handler(MessageHandler, filters=filters.Regex("^角色卡片查询(.*)"), block=False)
async def player_cards(self, update: Update, context: CallbackContext) -> None: async def player_cards(self, update: "Update", context: "ContextTypes.DEFAULT_TYPE") -> None:
user = update.effective_user user = update.effective_user
message = update.effective_message message = update.effective_message
args = self.get_args(context) args = self.get_args(context)
await message.reply_chat_action(ChatAction.TYPING) await message.reply_chat_action(ChatAction.TYPING)
player_info = await self.player_service.get_player(user.id) player_info = await self.player_service.get_player(user.id)
if player_info is None: if player_info is None:
buttons = [[InlineKeyboardButton("点我绑定账号", url=create_deep_linked_url(context.bot.username, "set_uid"))]] buttons = [
[
InlineKeyboardButton(
"点我绑定账号",
url=create_deep_linked_url(context.bot.username, "set_uid"),
)
]
]
if filters.ChatType.GROUPS.filter(message): if filters.ChatType.GROUPS.filter(message):
reply_message = await message.reply_text( reply_message = await message.reply_text(
"未查询到您所绑定的账号信息,请先私聊派蒙绑定账号", reply_markup=InlineKeyboardMarkup(buttons) "未查询到您所绑定的账号信息,请先私聊派蒙绑定账号",
reply_markup=InlineKeyboardMarkup(buttons),
) )
self.add_delete_message_job(reply_message, delay=30) self.add_delete_message_job(reply_message, delay=30)
@ -108,12 +128,27 @@ class PlayerCards(Plugin):
else: else:
await message.reply_text("未查询到您所绑定的账号信息,请先绑定账号", reply_markup=InlineKeyboardMarkup(buttons)) await message.reply_text("未查询到您所绑定的账号信息,请先绑定账号", reply_markup=InlineKeyboardMarkup(buttons))
return return
data = await self._fetch_user(player_info.player_id) data = await self._load_history(player_info.player_id)
if isinstance(data, str): if data is None:
await message.reply_text(data) if isinstance(self.kitsune, str):
return photo = self.kitsune
if data.characters is None: else:
await message.reply_text("请在游戏中的角色展柜中添加角色再开启显示角色详情再使用此功能,如果已经添加了角色,请等待角色数据更新后重试") photo = open("resources/img/kitsune.png", "rb")
buttons = [
[
InlineKeyboardButton(
"更新面板",
callback_data=f"update_player_card|{user.id}|{player_info.player_id}",
)
]
]
reply_message = await message.reply_photo(
photo=photo,
caption="角色列表未找到,请尝试点击下方按钮从 EnkaNetwork 更新角色列表",
reply_markup=InlineKeyboardMarkup(buttons),
)
if reply_message.photo:
self.kitsune = reply_message.photo[-1].file_id
return return
if len(args) == 1: if len(args) == 1:
character_name = roleToName(args[0]) character_name = roleToName(args[0])
@ -126,16 +161,20 @@ class PlayerCards(Plugin):
) )
else: else:
logger.info("用户 %s[%s] 角色卡片查询命令请求", user.full_name, user.id) logger.info("用户 %s[%s] 角色卡片查询命令请求", user.full_name, user.id)
buttons = self.gen_button(data, user.id, player_info.player_id) ttl = await self.cache.ttl(player_info.player_id)
if isinstance(self.temp_photo, str):
photo = self.temp_photo buttons = self.gen_button(data, user.id, player_info.player_id, update_button=ttl < 0)
if isinstance(self.kitsune, str):
photo = self.kitsune
else: else:
photo = open("resources/img/kitsune.png", "rb") photo = open("resources/img/kitsune.png", "rb")
reply_message = await message.reply_photo( reply_message = await message.reply_photo(
photo=photo, caption="请选择你要查询的角色,部分角色数据存在缓存,更新可能不及时", reply_markup=InlineKeyboardMarkup(buttons) photo=photo,
caption="请选择你要查询的角色",
reply_markup=InlineKeyboardMarkup(buttons),
) )
if reply_message.photo: if reply_message.photo:
self.temp_photo = reply_message.photo[-1].file_id self.kitsune = reply_message.photo[-1].file_id
return return
for characters in data.characters: for characters in data.characters:
if characters.name == character_name: if characters.name == character_name:
@ -147,20 +186,71 @@ class PlayerCards(Plugin):
render_result = await RenderTemplate( render_result = await RenderTemplate(
player_info.player_id, characters, self.template_service player_info.player_id, characters, self.template_service
).render() # pylint: disable=W0631 ).render() # pylint: disable=W0631
await render_result.reply_photo(message, filename=f"player_card_{player_info.player_id}_{character_name}.png") await render_result.reply_photo(
message,
filename=f"player_card_{player_info.player_id}_{character_name}.png",
)
@handler(CallbackQueryHandler, pattern=r"^update_player_card\|", block=False)
async def update_player_card(self, update: "Update", _: "ContextTypes.DEFAULT_TYPE") -> None:
user = update.effective_user
message = update.effective_message
callback_query = update.callback_query
async def get_player_card_callback(callback_query_data: str) -> Tuple[int, int]:
_data = callback_query_data.split("|")
_user_id = int(_data[1])
_uid = int(_data[2])
logger.debug("callback_query_data函数返回 user_id[%s] uid[%s]", _user_id, _uid)
return _user_id, _uid
user_id, uid = await get_player_card_callback(callback_query.data)
if user.id != user_id:
await callback_query.answer(text="这不是你的按钮!\n" + config.notice.user_mismatch, show_alert=True)
return
ttl = await self.cache.ttl(uid)
if ttl > 0:
await callback_query.answer(text=f"请等待 {ttl} 秒后再更新", show_alert=True)
return
await message.reply_chat_action(ChatAction.TYPING)
await callback_query.answer(text="正在从 EnkaNetwork 获取角色列表 请不要重复点击按钮")
data = await self._update_enka_data(uid)
if isinstance(data, str):
await callback_query.answer(text=data, show_alert=True)
return
buttons = self.gen_button(data, user.id, uid, update_button=False)
render_data = await self.parse_holder_data(data)
holder = await self.template_service.render(
"genshin/player_card/holder.html",
render_data,
viewport={"width": 750, "height": 580},
ttl=60 * 10,
caption="更新角色列表成功,请选择你要查询的角色",
)
await holder.edit_media(message, reply_markup=InlineKeyboardMarkup(buttons))
@handler(CallbackQueryHandler, pattern=r"^get_player_card\|", block=False) @handler(CallbackQueryHandler, pattern=r"^get_player_card\|", block=False)
async def get_player_cards(self, update: Update, _: CallbackContext) -> None: async def get_player_cards(self, update: "Update", _: "ContextTypes.DEFAULT_TYPE") -> None:
callback_query = update.callback_query callback_query = update.callback_query
user = callback_query.from_user user = callback_query.from_user
message = callback_query.message message = callback_query.message
async def get_player_card_callback(callback_query_data: str) -> Tuple[str, int, int]: async def get_player_card_callback(
callback_query_data: str,
) -> Tuple[str, int, int]:
_data = callback_query_data.split("|") _data = callback_query_data.split("|")
_user_id = int(_data[1]) _user_id = int(_data[1])
_uid = int(_data[2]) _uid = int(_data[2])
_result = _data[3] _result = _data[3]
logger.debug("callback_query_data函数返回 result[%s] user_id[%s] uid[%s]", _result, _user_id, _uid) logger.debug(
"callback_query_data函数返回 result[%s] user_id[%s] uid[%s]",
_result,
_user_id,
_uid,
)
return _result, _user_id, _uid return _result, _user_id, _uid
result, user_id, uid = await get_player_card_callback(callback_query.data) result, user_id, uid = await get_player_card_callback(callback_query.data)
@ -173,10 +263,22 @@ class PlayerCards(Plugin):
page = 0 page = 0
if result.isdigit(): if result.isdigit():
page = int(result) page = int(result)
logger.info("用户 %s[%s] 角色卡片查询命令请求 || page[%s] uid[%s]", user.full_name, user.id, page, uid) logger.info(
"用户 %s[%s] 角色卡片查询命令请求 || page[%s] uid[%s]",
user.full_name,
user.id,
page,
uid,
)
else: else:
logger.info("用户 %s[%s] 角色卡片查询命令请求 || character_name[%s] uid[%s]", user.full_name, user.id, result, uid) logger.info(
data = await self._fetch_user(uid) "用户 %s[%s] 角色卡片查询命令请求 || character_name[%s] uid[%s]",
user.full_name,
user.id,
result,
uid,
)
data = await self._load_history(uid)
if isinstance(data, str): if isinstance(data, str):
await message.reply_text(data) await message.reply_text(data)
return return
@ -185,7 +287,7 @@ class PlayerCards(Plugin):
await callback_query.answer("请先将角色加入到角色展柜并允许查看角色详情后再使用此功能,如果已经添加了角色,请等待角色数据更新后重试", show_alert=True) await callback_query.answer("请先将角色加入到角色展柜并允许查看角色详情后再使用此功能,如果已经添加了角色,请等待角色数据更新后重试", show_alert=True)
return return
if page: if page:
buttons = self.gen_button(data, user.id, uid, page) buttons = self.gen_button(data, user.id, uid, page, not await self.cache.ttl(uid) > 0)
await message.edit_reply_markup(reply_markup=InlineKeyboardMarkup(buttons)) await message.edit_reply_markup(reply_markup=InlineKeyboardMarkup(buttons))
await callback_query.answer(f"已切换到第 {page}", show_alert=False) await callback_query.answer(f"已切换到第 {page}", show_alert=False)
return return
@ -208,6 +310,7 @@ class PlayerCards(Plugin):
user_id: Union[str, int], user_id: Union[str, int],
uid: int, uid: int,
page: int = 1, page: int = 1,
update_button: bool = True,
) -> List[List[InlineKeyboardButton]]: ) -> List[List[InlineKeyboardButton]]:
"""生成按钮""" """生成按钮"""
buttons = [ buttons = [
@ -238,6 +341,13 @@ class PlayerCards(Plugin):
callback_data=f"get_player_card|{user_id}|{uid}|empty_data", callback_data=f"get_player_card|{user_id}|{uid}|empty_data",
) )
) )
if update_button:
last_button.append(
InlineKeyboardButton(
"更新面板",
callback_data=f"update_player_card|{user_id}|{uid}",
)
)
if next_page: if next_page:
last_button.append( last_button.append(
InlineKeyboardButton( InlineKeyboardButton(
@ -249,6 +359,30 @@ class PlayerCards(Plugin):
send_buttons.append(last_button) send_buttons.append(last_button)
return send_buttons return send_buttons
async def parse_holder_data(self, data: EnkaNetworkResponse) -> dict:
"""
生成渲染所需数据
"""
characters_data = []
for idx, character in enumerate(data.characters):
characters_data.append(
{
"level": character.level,
"element": character.element.name,
"constellation": character.constellations_unlocked,
"rarity": character.rarity,
"icon": (await self.assets_service.avatar(character.id).icon()).as_uri(),
}
)
if idx > 6:
break
return {
"uid": data.uid,
"level": data.player.level,
"signature": data.player.signature,
"characters": characters_data,
}
class Artifact(BaseModel): class Artifact(BaseModel):
"""在 enka Equipments model 基础上扩展了圣遗物评分数据""" """在 enka Equipments model 基础上扩展了圣遗物评分数据"""
@ -301,7 +435,12 @@ class Artifact(BaseModel):
class RenderTemplate: class RenderTemplate:
def __init__(self, uid: Union[int, str], character: CharacterInfo, template_service: TemplateService = None): def __init__(
self,
uid: Union[int, str],
character: "CharacterInfo",
template_service: TemplateService = None,
):
self.uid = uid self.uid = uid
self.template_service = template_service self.template_service = template_service
# 因为需要替换线上 enka 图片地址为本地地址,先克隆数据,避免修改原数据 # 因为需要替换线上 enka 图片地址为本地地址,先克隆数据,避免修改原数据
@ -443,7 +582,7 @@ class RenderTemplate:
stats = ArtifactStatsTheory(self.character.name) stats = ArtifactStatsTheory(self.character.name)
def substat_score(s: EquipmentsStats) -> float: def substat_score(s: "EquipmentsStats") -> float:
return stats.theory(s) return stats.theory(s)
return [ return [

View File

@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="zh-ch">
<head>
<meta charset="UTF-8">
<title>holder</title>
<link type="text/css" href="./style.css" rel="stylesheet"/>
<link type="text/css" href="../../styles/public.css" rel="stylesheet"/>
</head>
<body>
<div class="overview">
<div class="title">角色展柜</div>
<div class="summarize">
<div>
<div>UID: {{ uid }}</div>
<div>冒险等阶: {{ level }} 级</div>
</div>
<div>
<div>签名: {{ signature }}</div>
</div>
</div>
<div class="characters">
{% for character in characters %}
<div class="character">
{% if character.constellation > 0 %}
{% set bg = ['blue','blue', 'green','green', 'red', 'red'][character.constellation - 1] %}
<div style="background-color: var(--{{ bg }})">{{ character.constellation }} 命</div>
{% endif %}
<div class="element" style="background-image: url('../../img/element/{{ character.element }}.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 %}
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,58 @@
<!DOCTYPE html>
<html lang="zh-ch">
<head>
<meta charset="UTF-8">
<title>holder_example</title>
<link type="text/css" href="./style.css" rel="stylesheet"/>
<link type="text/css" href="../../styles/public.css" rel="stylesheet"/>
</head>
<body>
<div class="overview">
<div class="title">角色展柜</div>
<div class="summarize">
<div>
<div>UID: 123456789</div>
<div>冒险等阶: 55</div>
</div>
<div>
<div>签名: 貴方の運命は、すでに我が手中の糸が絡めとった!填充</div>
</div>
</div>
<div class="characters">
<div class="character">
<div style="background-color: var(--green)">4命</div>
<div class="element" style="background-image: url('../../img/element/Cryo.png')"></div>
<div class="icon" style="background-image: url('../../background/rarity/half/5.png')">
<img src="../../assets/avatar/10000007/icon.png" alt="荧"/>
</div>
<div class="caption">Lv.90</div>
</div>
<div class="character">
<div class="icon" style="background-image: url('../../background/rarity/half/5.png')">
<img src="../../assets/avatar/10000007/icon.png" alt="荧"/>
</div>
</div>
<div class="character">
<div class="icon" style="background-image: url('../../background/rarity/half/5.png')">
<img src="../../assets/avatar/10000007/icon.png" alt="荧"/>
</div>
</div>
<div class="character">
<div class="icon" style="background-image: url('../../background/rarity/half/5.png')">
<img src="../../assets/avatar/10000007/icon.png" alt="荧"/>
</div>
</div>
<div class="character">
<div class="icon" style="background-image: url('../../background/rarity/half/5.png')">
<img src="../../assets/avatar/10000007/icon.png" alt="荧"/>
</div>
</div>
<div class="character">
<div class="icon" style="background-image: url('../../background/rarity/half/5.png')">
<img src="../../assets/avatar/10000007/icon.png" alt="荧"/>
</div>
</div>
</div>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

View File

@ -0,0 +1,146 @@
: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: 5px;
}
.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%));
}
.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: 540px;
padding: 20px 30px;
background-size: cover;
background-repeat: no-repeat;
background-position: center;
background-image: linear-gradient(to top, rgb(0 0 0 / 10%), rgb(0 0 0 / 10%)), url("./img/holder_bg.png");
background-attachment: local;
border-radius: 15px;
overflow: hidden;
}
.summarize {
font-size: 20px;
margin: 10px;
padding: 20px;
border-radius: 5px;
border: 2px solid rgb(118 121 120 / 80%);
outline: 4px solid rgb(70, 80, 100);
background-color: rgb(70 80 100 / 60%);
background-image: url("../abyss/background/banner 01.png"), url("../abyss/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 > div {
width: 100%;
height: 50%;
padding: 5px;
color: var(--white);
display: flex;
align-items: center;
}
.summarize > div > div {
flex: 1;
}
.characters {
margin-left: 47px;
margin-top: 15px;
display: flex;
flex-wrap: wrap;
}
.character {
width: 120px;
height: 150px;
margin: 15px 12px;
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: 120px;
background-size: cover;
background-repeat: no-repeat;
background-position: center;
overflow: hidden;
border-radius: 0 0 20px 0;
}
.character > .caption {
font-size: 16px;
margin: 4px 0 0;
padding: 0;
height: min-content;
text-align: center;
color: black;
}
.character > div:first-child:not(.icon, .element) {
position: absolute;
top: 0;
right: 0;
padding: 3px;
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);
}
.icon > img {
width: inherit;
height: inherit;
}

View File

@ -1,16 +1,23 @@
import json from typing import Dict, Any, Optional, TYPE_CHECKING
from typing import Dict, Any, Optional
from enkanetwork import Cache from enkanetwork import Cache
from redis import asyncio as aioredis
try:
import ujson as jsonlib
except ImportError:
import json as jsonlib
if TYPE_CHECKING:
from redis import asyncio as aioredis
__all__ = ("RedisCache",) __all__ = ("RedisCache",)
class RedisCache(Cache): class RedisCache(Cache):
def __init__(self, redis: aioredis.Redis, key: Optional[str] = None, ttl: int = 60 * 3) -> None: def __init__(self, redis: "aioredis.Redis", key: Optional[str] = None, ex: int = 60 * 3) -> None:
self.redis = redis self.redis = redis
self.ttl = ttl self.ex = ex
self.key = key self.key = key
def get_qname(self, key): def get_qname(self, key):
@ -21,10 +28,18 @@ class RedisCache(Cache):
data = await self.redis.get(qname) data = await self.redis.get(qname)
if data: if data:
json_data = str(data, encoding="utf-8") json_data = str(data, encoding="utf-8")
return json.loads(json_data) return jsonlib.loads(json_data)
return None return None
async def set(self, key, value) -> None: async def set(self, key, value) -> None:
qname = self.get_qname(key) qname = self.get_qname(key)
data = json.dumps(value) data = jsonlib.dumps(value)
await self.redis.set(qname, data, ex=self.ttl) await self.redis.set(qname, data, ex=self.ex)
async def exists(self, key) -> int:
qname = self.get_qname(key)
return await self.redis.exists(qname)
async def ttl(self, key) -> int:
qname = self.get_qname(key)
return await self.redis.ttl(qname)