增加 html to image 的缓存

* 增加 html to image 缓存
* 对 template_service.render 进行封装,管理缓存逻辑
* cache key 为 html 的 sha256
* cache value 为 reply_photo 后 telegram 返回的 file_id
* 存入 redis,并设置合理的 ttl

Co-authored-by: 洛水居室 <luoshuijs@outlook.com>
Co-authored-by: xtaodada <xtao@xtaolink.cn>
This commit is contained in:
Chuangbo Li 2022-10-22 15:03:59 +08:00 committed by GitHub
parent f55077e713
commit f122e21092
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 384 additions and 176 deletions

View File

@ -2,10 +2,12 @@ from core.base.aiobrowser import AioBrowser
from core.service import init_service
from core.base.redisdb import RedisDB
from core.template.services import TemplateService
from core.template.cache import TemplatePreviewCache
from core.template.cache import TemplatePreviewCache, HtmlToFileIdCache
@init_service
def create_template_service(browser: AioBrowser, redis: RedisDB):
_cache = TemplatePreviewCache(redis)
_service = TemplateService(browser, _cache)
_preview_cache = TemplatePreviewCache(redis)
_html_to_file_id_cache = HtmlToFileIdCache(redis)
_service = TemplateService(browser, _html_to_file_id_cache, _preview_cache)
return _service

View File

@ -1,12 +1,13 @@
from typing import Any
import pickle # nosec B403
import gzip
import pickle # nosec B403
from hashlib import sha256
from typing import Any, Optional
from core.base.redisdb import RedisDB
class TemplatePreviewCache:
'''暂存渲染模板的数据用于预览'''
"""暂存渲染模板的数据用于预览"""
def __init__(self, redis: RedisDB):
self.client = redis.client
@ -16,7 +17,7 @@ class TemplatePreviewCache:
data = await self.client.get(self.cache_key(key))
if data:
# skipcq: BAN-B301
return pickle.loads(gzip.decompress(data)) # nosec B301
return pickle.loads(gzip.decompress(data)) # nosec B301
async def set_data(self, key: str, data: Any, ttl: int = 8 * 60 * 60):
ck = self.cache_key(key)
@ -26,3 +27,26 @@ class TemplatePreviewCache:
def cache_key(self, key: str) -> str:
return f"{self.qname}:{key}"
class HtmlToFileIdCache:
"""html to file_id 的缓存"""
def __init__(self, redis: RedisDB):
self.client = redis.client
self.qname = "bot:template:html-to-file-id"
async def get_data(self, html: str, file_type: str) -> Optional[str]:
data = await self.client.get(self.cache_key(html, file_type))
if data:
return data.decode()
async def set_data(self, html: str, file_type: str, file_id: str, ttl: int = 24 * 60 * 60):
ck = self.cache_key(html, file_type)
await self.client.set(ck, file_id)
if ttl != -1:
await self.client.expire(ck, ttl)
def cache_key(self, html: str, file_type: str) -> str:
key = sha256(html.encode()).hexdigest()
return f"{self.qname}:{file_type}:{key}"

14
core/template/error.py Normal file
View File

@ -0,0 +1,14 @@
class TemplateException(Exception):
pass
class QuerySelectorNotFound(TemplateException):
pass
class ErrorFileType(TemplateException):
pass
class FileIdNotFound(TemplateException):
pass

126
core/template/models.py Normal file
View File

@ -0,0 +1,126 @@
from enum import Enum
from typing import Optional, Union, List
from telegram import Message, InputMediaPhoto, InputMediaDocument
from core.template.cache import HtmlToFileIdCache
from core.template.error import ErrorFileType, FileIdNotFound
class FileType(Enum):
PHOTO = 1
DOCUMENT = 2
@staticmethod
def media_type(file_type: "FileType"):
"""对应的 Telegram media 类型"""
if file_type == FileType.PHOTO:
return InputMediaPhoto
elif file_type == FileType.DOCUMENT:
return InputMediaDocument
else:
raise ErrorFileType
class RenderResult:
"""渲染结果"""
def __init__(
self,
html: str,
photo: Union[bytes, str],
file_type: FileType,
cache: HtmlToFileIdCache,
ttl: int = 24 * 60 * 60,
caption: Optional[str] = None,
parse_mode: Optional[str] = None,
filename: Optional[str] = None,
):
"""
`html`: str 渲染生成的 html
`photo`: Union[bytes, str] 渲染生成的图片bytes 表示是图片str 则为 file_id
"""
self.caption = caption
self.parse_mode = parse_mode
self.filename = filename
self.html = html
self.photo = photo
self.file_type = file_type
self._cache = cache
self.ttl = ttl
async def reply_photo(self, message: Message, *args, **kwargs):
"""是 `message.reply_photo` 的封装,上传成功后,缓存 telegram 返回的 file_id方便重复使用"""
if self.file_type != FileType.PHOTO:
raise ErrorFileType
reply = await message.reply_photo(photo=self.photo, *args, **kwargs)
await self.cache_file_id(reply)
return reply
async def reply_document(self, message: Message, *args, **kwargs):
"""是 `message.reply_document` 的封装,上传成功后,缓存 telegram 返回的 file_id方便重复使用"""
if self.file_type != FileType.DOCUMENT:
raise ErrorFileType
reply = await message.reply_document(document=self.photo, *args, **kwargs)
await self.cache_file_id(reply)
return reply
async def edit_media(self, message: Message, *args, **kwargs):
"""是 `message.edit_media` 的封装,上传成功后,缓存 telegram 返回的 file_id方便重复使用"""
if self.file_type != FileType.PHOTO:
raise ErrorFileType
media = InputMediaPhoto(
media=self.photo, caption=self.caption, parse_mode=self.parse_mode, filename=self.filename
)
edit_media = await message.edit_media(media, *args, **kwargs)
await self.cache_file_id(edit_media)
return edit_media
async def cache_file_id(self, reply: Message):
"""缓存 telegram 返回的 file_id"""
if self.is_file_id():
return
if self.file_type == FileType.PHOTO and reply.photo:
file_id = reply.photo[0].file_id
elif self.file_type == FileType.DOCUMENT and reply.document:
file_id = reply.document.file_id
else:
raise FileIdNotFound
await self._cache.set_data(self.html, self.file_type.name, file_id, self.ttl)
def is_file_id(self) -> bool:
return isinstance(self.photo, str)
class RenderGroupResult:
def __init__(self, results: List[RenderResult]):
self.results = results
async def reply_media_group(self, message: Message, *args, **kwargs):
"""是 `message.reply_media_group` 的封装,上传成功后,缓存 telegram 返回的 file_id方便重复使用"""
reply = await message.reply_media_group(
media=[
FileType.media_type(result.file_type)(
media=result.photo, caption=result.caption, parse_mode=result.parse_mode, filename=result.filename
)
for result in self.results
],
*args,
**kwargs,
)
for index, value in enumerate(reply):
result = self.results[index]
await result.cache_file_id(value)

View File

@ -1,29 +1,32 @@
import time
from typing import Optional
from urllib.parse import urlencode, urljoin, urlsplit
from jinja2 import Environment, FileSystemLoader, Template
from playwright.async_api import ViewportSize
from uuid import uuid4
from fastapi import HTTPException
from fastapi.responses import FileResponse, HTMLResponse
from fastapi.staticfiles import StaticFiles
from jinja2 import Environment, FileSystemLoader, Template
from playwright.async_api import ViewportSize
from core.base.aiobrowser import AioBrowser
from core.bot import bot
from core.base.webserver import webapp
from core.bot import bot
from core.template.cache import HtmlToFileIdCache, TemplatePreviewCache
from core.template.error import QuerySelectorNotFound
from core.template.models import FileType, RenderResult
from utils.const import PROJECT_ROOT
from utils.log import logger
from core.template.cache import TemplatePreviewCache
class _QuerySelectorNotFound(Exception):
pass
class TemplateService:
def __init__(self, browser: AioBrowser, preview_cache: TemplatePreviewCache, template_dir: str = "resources"):
def __init__(
self,
browser: AioBrowser,
html_to_file_id_cache: HtmlToFileIdCache,
preview_cache: TemplatePreviewCache,
template_dir: str = "resources",
):
self._browser = browser
self.template_dir = PROJECT_ROOT / template_dir
@ -36,10 +39,12 @@ class TemplateService:
self.previewer = TemplatePreviewer(self, preview_cache)
self.html_to_file_id_cache = html_to_file_id_cache
def get_template(self, template_name: str) -> Template:
return self._jinja2_env.get_template(template_name)
async def render_async(self, template_name: str, template_data: dict):
async def render_async(self, template_name: str, template_data: dict) -> str:
"""模板渲染
:param template_name: 模板文件名
:param template_data: 模板数据
@ -54,19 +59,28 @@ class TemplateService:
self,
template_name: str,
template_data: dict,
viewport: ViewportSize = None,
viewport: Optional[ViewportSize] = None,
full_page: bool = True,
evaluate: Optional[str] = None,
query_selector: str = None
) -> bytes:
query_selector: Optional[str] = None,
file_type: FileType = FileType.PHOTO,
ttl: int = 24 * 60 * 60,
caption: Optional[str] = None,
parse_mode: Optional[str] = None,
filename: Optional[str] = None,
) -> RenderResult:
"""模板渲染成图片
:param template_path: 模板目录
:param template_name: 模板文件名
:param template_data: 模板数据
:param viewport: 截图大小
:param full_page: 是否长截图
:param evaluate: 页面加载后运行的 js
:param query_selector: 截图选择器
:param file_type: 缓存的文件类型
:param ttl: 缓存时间
:param caption: 图片描述
:param parse_mode: 图片描述解析模式
:param filename: 文件名字
:return:
"""
start_time = time.time()
@ -79,6 +93,20 @@ class TemplateService:
html = await template.render_async(**template_data)
logger.debug(f"{template_name} 模板渲染使用了 {str(time.time() - start_time)}")
file_id = await self.html_to_file_id_cache.get_data(html, file_type.name)
if file_id and not bot.config.debug:
logger.debug(f"{template_name} 命中缓存,返回 file_id {file_id}")
return RenderResult(
html=html,
photo=file_id,
file_type=file_type,
cache=self.html_to_file_id_cache,
ttl=ttl,
caption=caption,
parse_mode=parse_mode,
filename=filename,
)
browser = await self._browser.get_browser()
start_time = time.time()
page = await browser.new_page(viewport=viewport)
@ -92,16 +120,25 @@ class TemplateService:
try:
card = await page.query_selector(query_selector)
if not card:
raise _QuerySelectorNotFound
raise QuerySelectorNotFound
clip = await card.bounding_box()
if not clip:
raise _QuerySelectorNotFound
except _QuerySelectorNotFound:
raise QuerySelectorNotFound
except QuerySelectorNotFound:
logger.warning(f"未找到 {query_selector} 元素")
png_data = await page.screenshot(clip=clip, full_page=full_page)
await page.close()
logger.debug(f"{template_name} 图片渲染使用了 {str(time.time() - start_time)}")
return png_data
return RenderResult(
html=html,
photo=png_data,
file_type=file_type,
cache=self.html_to_file_id_cache,
ttl=ttl,
caption=caption,
parse_mode=parse_mode,
filename=filename,
)
class TemplatePreviewer:
@ -128,7 +165,7 @@ class TemplatePreviewer:
"""注册预览用到的路由"""
@webapp.get("/preview/{path:path}")
async def preview_template(path: str, key: Optional[str] = None): # pylint: disable=W0612
async def preview_template(path: str, key: Optional[str] = None): # pylint: disable=W0612
# 如果是 /preview/ 开头的静态文件,直接返回内容。比如使用相对链接 ../ 引入的静态资源
if not path.endswith(".html"):
full_path = self.template_service.template_dir / path

View File

@ -3,13 +3,13 @@ import asyncio
import re
from datetime import datetime
from functools import lru_cache, partial
from typing import List, Match, Optional, Tuple
from typing import Any, Coroutine, List, Match, Optional, Tuple, Union
import ujson as json
from arkowrapper import ArkoWrapper
from genshin import Client
from pytz import timezone
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, InputMediaPhoto, Update, Message
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update, Message
from telegram.constants import ChatAction, ParseMode
from telegram.ext import CallbackContext, filters
@ -19,6 +19,7 @@ from core.cookies.error import CookiesNotFoundError, TooManyRequestPublicCookies
from core.cookies.services import CookiesService
from core.plugin import Plugin, handler
from core.template import TemplateService
from core.template.models import RenderGroupResult, RenderResult
from core.user import UserService
from core.user.error import UserNotFoundError
from metadata.genshin import game_id_to_role_id
@ -185,8 +186,10 @@ class Abyss(Plugin, BasePlugin):
await message.reply_chat_action(ChatAction.UPLOAD_PHOTO)
for group in ArkoWrapper(images).map(InputMediaPhoto).group(10): # 每 10 张图片分一个组
await message.reply_media_group(list(group), allow_sending_without_reply=True, write_timeout=60)
for group in ArkoWrapper(images).group(10): # 每 10 张图片分一个组
await RenderGroupResult(results=group).reply_media_group(
message, allow_sending_without_reply=True, write_timeout=60
)
if reply_text is not None:
await reply_text.delete()
@ -195,7 +198,17 @@ class Abyss(Plugin, BasePlugin):
async def get_rendered_pic(
self, client: Client, uid: int, floor: int, total: bool, previous: bool
) -> Optional[List[bytes]]:
) -> Union[
tuple[
Union[BaseException, Any],
Union[BaseException, Any],
Union[BaseException, Any],
Union[BaseException, Any],
Union[BaseException, Any],
],
list[RenderResult],
None,
]:
"""
获取渲染后的图片
@ -258,50 +271,45 @@ class Abyss(Plugin, BasePlugin):
data = json.loads(result)
render_data["data"] = data
render_result = []
render_inputs: List[Tuple[int, Coroutine[Any, Any, RenderResult]]] = []
async def overview_task():
render_result.append(
[
-1,
await self.template_service.render(
"genshin/abyss/overview.html", render_data, viewport={"width": 750, "height": 580}
),
]
def overview_task():
return -1, self.template_service.render(
"genshin/abyss/overview.html", render_data, viewport={"width": 750, "height": 580}
)
async def floor_task(floor_index: int):
def floor_task(floor_index: int):
floor_d = data["floors"][floor_index]
render_result.append(
[
floor_d["floor"],
await self.template_service.render(
"genshin/abyss/floor.html",
{
**render_data,
"floor": floor_d,
"total_stars": f"{floor_d['stars']}/{floor_d['max_stars']}",
},
viewport={"width": 690, "height": 500},
full_page=True,
),
]
return (
floor_d["floor"],
self.template_service.render(
"genshin/abyss/floor.html",
{
**render_data,
"floor": floor_d,
"total_stars": f"{floor_d['stars']}/{floor_d['max_stars']}",
},
viewport={"width": 690, "height": 500},
full_page=True,
ttl=15 * 24 * 60 * 60,
),
)
task_list = [asyncio.create_task(overview_task())]
render_inputs.append(overview_task())
for i, f in enumerate(data["floors"]):
if f["floor"] >= 9:
task_list.append(asyncio.create_task(floor_task(i)))
await asyncio.gather(*task_list)
render_inputs.append(floor_task(i))
render_group_inputs = list(map(lambda x: x[1], sorted(render_inputs, key=lambda x: x[0])))
return await asyncio.gather(*render_group_inputs)
return list(map(lambda x: x[1], sorted(render_result, key=lambda x: x[0])))
elif floor < 1:
render_data["data"] = json.loads(result)
return [
await self.template_service.render(
"genshin/abyss/overview.html",
render_data,
viewport={"width": 750, "height": 580},
"genshin/abyss/overview.html", render_data, viewport={"width": 750, "height": 580}
)
]
else:
@ -330,6 +338,6 @@ class Abyss(Plugin, BasePlugin):
render_data["total_stars"] = f"{floor_data[0]['stars']}/{floor_data[0]['max_stars']}"
return [
await self.template_service.render(
"genshin/abyss/floor.html", render_data, viewport={"width": 690, "height": 500}, full_page=True
"genshin/abyss/floor.html", render_data, viewport={"width": 690, "height": 500}
)
]

View File

@ -82,11 +82,11 @@ class AbyssTeam(Plugin, BasePlugin):
abyss_teams_data["teams"].append(team)
await message.reply_chat_action(ChatAction.UPLOAD_PHOTO)
png_data = await self.template_service.render(
render_result = await self.template_service.render(
"genshin/abyss_team/abyss_team.html",
abyss_teams_data,
{"width": 785, "height": 800},
full_page=True,
query_selector=".bg-contain",
)
await message.reply_photo(png_data, filename=f"abyss_team_{user.id}.png", allow_sending_without_reply=True)
await render_result.reply_photo(message, filename=f"abyss_team_{user.id}.png", allow_sending_without_reply=True)

View File

@ -1,12 +1,12 @@
"""练度统计"""
import asyncio
from typing import Iterable, List, Optional, Sequence, Tuple
from typing import Iterable, List, Optional, Sequence
from arkowrapper import ArkoWrapper
from enkanetwork import Assets as EnkaAssets, EnkaNetworkAPI
from genshin import Client, GenshinException
from genshin.models import CalculatorCharacterDetails, CalculatorTalent, Character
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, InputFile, Message, Update, User
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Message, Update, User
from telegram.constants import ChatAction, ParseMode
from telegram.ext import CallbackContext, filters
@ -17,6 +17,7 @@ from core.cookies.error import CookiesNotFoundError
from core.cookies.services import CookiesService
from core.plugin import Plugin, handler
from core.template import TemplateService
from core.template.models import FileType
from core.user.error import UserNotFoundError
from metadata.genshin import AVATAR_DATA, NAMECARD_DATA
from modules.wiki.base import Model
@ -113,18 +114,14 @@ class AvatarListPlugin(Plugin, BasePlugin):
async def get_avatars_data(
self, characters: Sequence[Character], client: Client, max_length: int = None
) -> List["AvatarData"]:
task_result: List[Tuple[int, AvatarData]] = []
async def _task(c, n):
if (result := await self.get_avatar_data(c, client)) is not None:
task_result.append((n, result))
return n, await self.get_avatar_data(c, client)
task_list = []
for num, character in enumerate(characters[:max_length]):
task_list.append(asyncio.create_task(_task(character, num)))
task_results = await asyncio.gather(
*[_task(character, num) for num, character in enumerate(characters[:max_length])]
)
await asyncio.gather(*task_list)
return list(map(lambda x: x[1], sorted(task_result, key=lambda x: x[0])))
return list(filter(lambda x: x, map(lambda x: x[1], sorted(task_results, key=lambda x: x[0]))))
async def get_final_data(self, client: Client, characters: Sequence[Character], update: Update):
try:
@ -218,7 +215,9 @@ class AvatarListPlugin(Plugin, BasePlugin):
"has_more": len(characters) != len(avatar_datas), # 是否显示了全部角色
}
await message.reply_chat_action(ChatAction.UPLOAD_DOCUMENT if all_avatars else ChatAction.UPLOAD_PHOTO)
as_document = True if all_avatars and len(characters) > 20 else False
await message.reply_chat_action(ChatAction.UPLOAD_DOCUMENT if as_document else ChatAction.UPLOAD_PHOTO)
image = await self.template_service.render(
"genshin/avatar_list/main.html",
@ -226,12 +225,14 @@ class AvatarListPlugin(Plugin, BasePlugin):
viewport={"width": 1040, "height": 500},
full_page=True,
query_selector=".container",
file_type=FileType.DOCUMENT if as_document else FileType.PHOTO,
ttl=30 * 24 * 60 * 60,
)
self._add_delete_message_job(context, notice.chat_id, notice.message_id, 5)
if all_avatars and len(characters) > 20:
await message.reply_document(InputFile(image, filename="练度统计.png"))
if as_document:
await image.reply_document(message, filename="练度统计.png")
else:
await message.reply_photo(image)
await image.reply_photo(message)
logger.info(
f"用户 {user.full_name}[{user.id}] [bold]练度统计[/bold]发送{'文件' if all_avatars else '图片'}成功",

View File

@ -18,7 +18,7 @@ from bs4 import BeautifulSoup
from genshin import Client
from httpx import AsyncClient, HTTPError
from pydantic import BaseModel
from telegram import InputMediaDocument, InputMediaPhoto, Message, Update, User
from telegram import Message, Update, User
from telegram.constants import ChatAction, ParseMode
from telegram.error import RetryAfter, TimedOut
from telegram.ext import CallbackContext
@ -28,6 +28,7 @@ from core.baseplugin import BasePlugin
from core.cookies.error import CookiesNotFoundError
from core.plugin import Plugin, handler
from core.template import TemplateService
from core.template.models import FileType, RenderGroupResult
from core.user.error import UserNotFoundError
from metadata.genshin import AVATAR_DATA, HONEY_DATA
from utils.bot import get_all_args
@ -186,7 +187,7 @@ class DailyMaterial(Plugin, BasePlugin):
title = "今日"
weekday = now.weekday() - (1 if now.hour < 4 else 0)
weekday = 6 if weekday < 0 else weekday
time = now.strftime("%m-%d %H:%M") + " 星期" + WEEK_MAP[weekday]
time = f"星期{WEEK_MAP[weekday]}"
full = bool(args and args[-1] == "full") # 判定最后一个参数是不是 full
logger.info(f'用户 {user.full_name}[{user.id}] 每日素材命令请求 || 参数 weekday="{WEEK_MAP[weekday]}" full={full}')
@ -275,35 +276,35 @@ class DailyMaterial(Plugin, BasePlugin):
setattr(render_data, {"avatar": "character"}.get(type_, type_), areas)
await message.reply_chat_action(ChatAction.TYPING)
render_tasks = [
asyncio.create_task(
self.template_service.render( # 渲染角色素材页
"genshin/daily_material/character.html", {"data": render_data}, {"width": 1164, "height": 500}
)
),
asyncio.create_task(
self.template_service.render( # 渲染武器素材页
"genshin/daily_material/weapon.html", {"data": render_data}, {"width": 1164, "height": 500}
)
),
]
results = await asyncio.gather(*render_tasks)
# 是否发送原图
file_type = FileType.DOCUMENT if full else FileType.PHOTO
character_img_data = results[0]
weapon_img_data = results[1]
character_img_data, weapon_img_data = await asyncio.gather(
self.template_service.render( # 渲染角色素材页
"genshin/daily_material/character.html",
{"data": render_data},
{"width": 1164, "height": 500},
file_type=file_type,
ttl=7 * 24 * 60 * 60,
),
self.template_service.render( # 渲染武器素材页
"genshin/daily_material/weapon.html",
{"data": render_data},
{"width": 1164, "height": 500},
file_type=file_type,
ttl=7 * 24 * 60 * 60,
),
)
self._add_delete_message_job(context, notice.chat_id, notice.message_id, 5)
await message.reply_chat_action(ChatAction.UPLOAD_PHOTO)
if full: # 是否发送原图
await message.reply_media_group(
[
InputMediaDocument(character_img_data, filename="可培养角色.png"),
InputMediaDocument(weapon_img_data, filename="可培养武器.png"),
]
)
else:
await message.reply_media_group([InputMediaPhoto(character_img_data), InputMediaPhoto(weapon_img_data)])
character_img_data.filename = f"{title}可培养角色.png"
weapon_img_data.filename = f"{title}可培养武器.png"
await RenderGroupResult([character_img_data, weapon_img_data]).reply_media_group(message)
logger.debug("角色、武器培养素材图发送成功")
@handler.command("refresh_daily_material", block=False)
@ -428,7 +429,7 @@ class DailyMaterial(Plugin, BasePlugin):
for ID, DATA in ITEMS.items():
if (ITEM := [ID, DATA[1], TYPE]) not in new_items:
new_items.append(ITEM)
task_list.append(asyncio.create_task(task(*ITEM)))
task_list.append(task(*ITEM))
await asyncio.gather(*task_list) # 等待所有任务执行完成
logger.info("图标素材下载完成")

View File

@ -11,7 +11,7 @@ from core.baseplugin import BasePlugin
from core.cookies.error import CookiesNotFoundError
from core.cookies.services import CookiesService
from core.plugin import Plugin, handler
from core.template.services import TemplateService
from core.template.services import RenderResult, TemplateService
from core.user.error import UserNotFoundError
from core.user.services import UserService
from utils.decorators.error import error_callable
@ -34,7 +34,7 @@ class DailyNote(Plugin, BasePlugin):
self.user_service = user_service
self.current_dir = os.getcwd()
async def _get_daily_note(self, client) -> bytes:
async def _get_daily_note(self, client) -> RenderResult:
daily_info = await client.get_genshin_notes(client.uid)
day = datetime.datetime.now().strftime("%m-%d %H:%M") + " 星期" + "一二三四五六日"[datetime.datetime.now().weekday()]
resin_recovery_time = (
@ -85,14 +85,14 @@ class DailyNote(Plugin, BasePlugin):
"transformer_ready": transformer_ready,
"transformer_recovery_time": transformer_recovery_time,
}
png_data = await self.template_service.render(
render_result = await self.template_service.render(
"genshin/daily_note/daily_note.html", daily_data, {"width": 600, "height": 548}, full_page=False
)
return png_data
return render_result
@handler(CommandHandler, command="dailynote", block=False)
@handler(MessageHandler, filters=filters.Regex("^当前状态(.*)"), block=False)
@restricts(return_data=ConversationHandler.END)
@restricts(30)
@error_callable
async def command_start(self, update: Update, context: CallbackContext) -> Optional[int]:
user = update.effective_user
@ -100,7 +100,7 @@ class DailyNote(Plugin, BasePlugin):
logger.info(f"用户 {user.full_name}[{user.id}] 查询游戏状态命令请求")
try:
client = await get_genshin_client(user.id)
png_data = await self._get_daily_note(client)
render_result = await self._get_daily_note(client)
except (UserNotFoundError, CookiesNotFoundError):
if filters.ChatType.GROUPS.filter(message):
buttons = [[InlineKeyboardButton("点我私聊", url=f"https://t.me/{context.bot.username}?start=set_cookie")]]
@ -120,4 +120,4 @@ class DailyNote(Plugin, BasePlugin):
self._add_delete_message_job(context, message.chat_id, message.message_id, 300)
return ConversationHandler.END
await message.reply_chat_action(ChatAction.UPLOAD_PHOTO)
await message.reply_photo(png_data, filename=f"{client.uid}.png", allow_sending_without_reply=True)
await render_result.reply_photo(message, filename=f"{client.uid}.png", allow_sending_without_reply=True)

View File

@ -14,6 +14,7 @@ from core.cookies import CookiesService
from core.cookies.error import CookiesNotFoundError
from core.plugin import Plugin, handler, conversation
from core.template import TemplateService
from core.template.models import FileType
from core.user import UserService
from core.user.error import UserNotFoundError
from metadata.scripts.paimon_moe import update_paimon_moe_zh, GACHA_LOG_PAIMON_MOE_PATH
@ -29,7 +30,6 @@ from modules.gacha_log.error import (
from modules.gacha_log.helpers import from_url_get_authkey
from modules.gacha_log.log import GachaLog
from utils.bot import get_all_args
from utils.const import PROJECT_ROOT
from utils.decorators.admins import bot_admins_rights_check
from utils.decorators.error import error_callable
from utils.decorators.restricts import restricts
@ -341,7 +341,7 @@ class GachaLogPlugin(Plugin.Conversation, BasePlugin.Conversation):
png_data = await self.template_service.render(
"genshin/gacha_log/gacha_log.html", data, full_page=True, query_selector=".body_box"
)
await message.reply_photo(png_data)
await png_data.reply_photo(message)
except GachaLogNotFound:
await message.reply_text("派蒙没有找到你的抽卡记录,快来私聊派蒙导入吧~")
except GachaLogAccountNotFound:
@ -390,15 +390,18 @@ class GachaLogPlugin(Plugin.Conversation, BasePlugin.Conversation):
if data["hasMore"] and not group:
document = True
data["hasMore"] = False
await message.reply_chat_action(ChatAction.UPLOAD_DOCUMENT if document else ChatAction.UPLOAD_PHOTO)
png_data = await self.template_service.render(
"genshin/gacha_count/gacha_count.html", data, full_page=True, query_selector=".body_box"
"genshin/gacha_count/gacha_count.html",
data,
full_page=True,
query_selector=".body_box",
file_type=FileType.DOCUMENT if document else FileType.PHOTO,
)
if document:
await message.reply_chat_action(ChatAction.UPLOAD_DOCUMENT)
await message.reply_document(png_data, filename="抽卡统计.png")
await png_data.reply_document(message, filename="抽卡统计.png")
else:
await message.reply_chat_action(ChatAction.UPLOAD_PHOTO)
await message.reply_photo(png_data)
await png_data.reply_photo(message)
except GachaLogNotFound:
await message.reply_text("派蒙没有找到你的抽卡记录,快来私聊派蒙导入吧~")
except GachaLogAccountNotFound:

View File

@ -1,9 +1,7 @@
from telegram import Update
from telegram.constants import ChatAction
from telegram.error import BadRequest
from telegram.ext import CommandHandler, CallbackContext
from core.bot import bot
from core.plugin import Plugin, handler
from core.template import TemplateService
from utils.decorators.error import error_callable
@ -13,8 +11,6 @@ from utils.log import logger
class HelpPlugin(Plugin):
def __init__(self, template_service: TemplateService = None):
self.file_id = None
self.help_png = None
if template_service is None:
raise ModuleNotFoundError
self.template_service = template_service
@ -26,18 +22,9 @@ class HelpPlugin(Plugin):
user = update.effective_user
message = update.effective_message
logger.info(f"用户 {user.full_name}[{user.id}] 发出help命令")
if self.file_id is None or bot.config.debug:
await message.reply_chat_action(ChatAction.TYPING)
help_png = await self.template_service.render("bot/help/help.html", {}, {"width": 1280, "height": 900})
await message.reply_chat_action(ChatAction.UPLOAD_PHOTO)
reply_photo = await message.reply_photo(help_png, filename="help.png", allow_sending_without_reply=True)
photo = reply_photo.photo[0]
self.file_id = photo.file_id
else:
try:
await message.reply_chat_action(ChatAction.UPLOAD_PHOTO)
await message.reply_photo(self.file_id, allow_sending_without_reply=True)
except BadRequest as error:
self.file_id = None
logger.error("发送图片失败尝试清空已经保存的file_id错误信息为", error)
await message.reply_text("发送图片失败", allow_sending_without_reply=True)
await message.reply_chat_action(ChatAction.TYPING)
render_result = await self.template_service.render(
"bot/help/help.html", {}, {"width": 1280, "height": 900}, ttl=30 * 24 * 60 * 60
)
await message.reply_chat_action(ChatAction.UPLOAD_PHOTO)
await render_result.reply_photo(message, filename="help.png", allow_sending_without_reply=True)

View File

@ -11,7 +11,7 @@ from core.baseplugin import BasePlugin
from core.cookies.error import CookiesNotFoundError
from core.cookies.services import CookiesService
from core.plugin import Plugin, handler
from core.template.services import TemplateService
from core.template.services import RenderResult, TemplateService
from core.user.error import UserNotFoundError
from core.user.services import UserService
from utils.bot import get_all_args
@ -65,7 +65,7 @@ class Ledger(Plugin, BasePlugin):
self.user_service = user_service
self.current_dir = os.getcwd()
async def _start_get_ledger(self, client, month=None) -> bytes:
async def _start_get_ledger(self, client, month=None) -> RenderResult:
try:
diary_info = await client.get_diary(client.uid, month=month)
except GenshinException as error:
@ -98,10 +98,10 @@ class Ledger(Plugin, BasePlugin):
"categories": categories,
"color": color,
}
png_data = await self.template_service.render(
render_result = await self.template_service.render(
"genshin/ledger/ledger.html", ledger_data, {"width": 580, "height": 610}
)
return png_data
return render_result
@handler(CommandHandler, command="ledger", block=False)
@handler(MessageHandler, filters=filters.Regex("^旅行札记查询(.*)"), block=False)
@ -122,7 +122,7 @@ class Ledger(Plugin, BasePlugin):
await message.reply_chat_action(ChatAction.TYPING)
try:
client = await get_genshin_client(user.id)
png_data = await self._start_get_ledger(client, month)
render_result = await self._start_get_ledger(client, month)
except (UserNotFoundError, CookiesNotFoundError):
if filters.ChatType.GROUPS.filter(message):
buttons = [[InlineKeyboardButton("点我私聊", url=f"https://t.me/{context.bot.username}?start=set_cookie")]]
@ -142,4 +142,4 @@ class Ledger(Plugin, BasePlugin):
self._add_delete_message_job(context, message.chat_id, message.message_id, 30)
return
await message.reply_chat_action(ChatAction.UPLOAD_PHOTO)
await message.reply_photo(png_data, filename=f"{client.uid}.png", allow_sending_without_reply=True)
await render_result.reply_photo(message, filename=f"{client.uid}.png", allow_sending_without_reply=True)

View File

@ -17,7 +17,7 @@ from enkanetwork import (
VaildateUIDError,
)
from pydantic import BaseModel
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, InputMediaPhoto, Update
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.constants import ChatAction
from telegram.ext import CallbackContext, CallbackQueryHandler, CommandHandler, MessageHandler, filters
@ -132,8 +132,8 @@ class PlayerCards(Plugin, BasePlugin):
await message.reply_text(f"角色展柜中未找到 {character_name}")
return
await message.reply_chat_action(ChatAction.UPLOAD_PHOTO)
pnd_data = await RenderTemplate(uid, characters, self.template_service).render() # pylint: disable=W0631
await message.reply_photo(pnd_data, filename=f"player_card_{uid}_{character_name}.png")
render_result = await RenderTemplate(uid, characters, self.template_service).render() # pylint: disable=W0631
await render_result.reply_photo(message, filename=f"player_card_{uid}_{character_name}.png")
@handler(CallbackQueryHandler, pattern=r"^get_player_card\|", block=False)
@restricts(restricts_time_of_groups=20, without_overlapping=True)
@ -171,8 +171,9 @@ class PlayerCards(Plugin, BasePlugin):
return
await callback_query.answer(text="正在渲染图片中 请稍等 请不要重复点击按钮", show_alert=False)
await message.reply_chat_action(ChatAction.UPLOAD_PHOTO)
pnd_data = await RenderTemplate(uid, characters, self.template_service).render() # pylint: disable=W0631
await message.edit_media(InputMediaPhoto(pnd_data, filename=f"player_card_{uid}_{result}.png"))
render_result = await RenderTemplate(uid, characters, self.template_service).render() # pylint: disable=W0631
render_result.filename = f"player_card_{uid}_{result}.png"
await render_result.edit_media(message)
class Artifact(BaseModel):
@ -195,15 +196,15 @@ class Artifact(BaseModel):
self.score = round(self.score, 1)
for r in (
("D", 10),
("C", 16.5),
("B", 23.1),
("A", 29.7),
("S", 36.3),
("SS", 42.9),
("SSS", 49.5),
("ACE", 56.1),
("ACE²", 66),
("D", 10),
("C", 16.5),
("B", 23.1),
("A", 29.7),
("S", 36.3),
("SS", 42.9),
("SSS", 49.5),
("ACE", 56.1),
("ACE²", 66),
):
if self.score >= r[1]:
self.score_label = r[0]
@ -243,15 +244,15 @@ class RenderTemplate:
artifact_total_score_label: str = "E"
for r in (
("D", 10),
("C", 16.5),
("B", 23.1),
("A", 29.7),
("S", 36.3),
("SS", 42.9),
("SSS", 49.5),
("ACE", 56.1),
("ACE²", 66),
("D", 10),
("C", 16.5),
("B", 23.1),
("A", 29.7),
("S", 36.3),
("SS", 42.9),
("SSS", 49.5),
("ACE", 56.1),
("ACE²", 66),
):
if artifact_total_score / 5 >= r[1]:
artifact_total_score_label = r[0]
@ -285,6 +286,7 @@ class RenderTemplate:
{"width": 950, "height": 1080},
full_page=True,
query_selector=".text-neutral-200",
ttl=7 * 24 * 60 * 60,
)
async def de_stats(self) -> List[Tuple[str, Any]]:

View File

@ -15,6 +15,7 @@ from telegram.ext import (
from core.baseplugin import BasePlugin
from core.cookies.error import CookiesNotFoundError, TooManyRequestPublicCookies
from core.plugin import Plugin, handler
from core.template.models import RenderResult
from core.template.services import TemplateService
from core.user.error import UserNotFoundError
from utils.decorators.error import error_callable
@ -51,7 +52,7 @@ class UserStatsPlugins(Plugin, BasePlugin):
client = await get_genshin_client(user.id)
except CookiesNotFoundError:
client, uid = await get_public_genshin_client(user.id)
png_data = await self.render(client, uid)
render_result = await self.render(client, uid)
except UserNotFoundError:
if filters.ChatType.GROUPS.filter(message):
buttons = [[InlineKeyboardButton("点我私聊", url=f"https://t.me/{context.bot.username}?start=set_uid")]]
@ -73,9 +74,9 @@ class UserStatsPlugins(Plugin, BasePlugin):
await message.reply_text("角色数据有误 估计是派蒙晕了")
return
await message.reply_chat_action(ChatAction.UPLOAD_PHOTO)
await message.reply_photo(png_data, filename=f"{client.uid}.png", allow_sending_without_reply=True)
await render_result.reply_photo(message, filename=f"{client.uid}.png", allow_sending_without_reply=True)
async def render(self, client: Client, uid: Optional[int] = None) -> bytes:
async def render(self, client: Client, uid: Optional[int] = None) -> RenderResult:
if uid is None:
uid = client.uid

View File

@ -111,9 +111,11 @@ class WeaponPlugin(Plugin, BasePlugin):
template_data = await input_template_data(weapon_data)
png_data = await self.template_service.render(
"genshin/weapon/weapon.html", template_data, {"width": 540, "height": 540}
"genshin/weapon/weapon.html", template_data, {"width": 540, "height": 540}, ttl=31 * 24 * 60 * 60
)
await message.reply_chat_action(ChatAction.UPLOAD_PHOTO)
await message.reply_photo(
png_data, filename=f"{template_data['weapon_name']}.png", allow_sending_without_reply=True
await png_data.reply_photo(
message,
filename=f"{template_data['weapon_name']}.png",
allow_sending_without_reply=True,
)