mirror of
https://github.com/suyiiyii/nonebot-bison.git
synced 2026-05-09 18:27:56 +08:00
✨ 添加 Theme 功能
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
from .build import ArknightsTheme
|
||||
|
||||
__theme_meta__ = ArknightsTheme()
|
||||
@@ -0,0 +1,69 @@
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
|
||||
from nonebot_plugin_saa import Text, Image, MessageSegmentFactory
|
||||
|
||||
from nonebot_bison.theme import Theme, ThemeRenderError, ThemeRenderUnsupportError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from nonebot_bison.post import Post
|
||||
|
||||
|
||||
@dataclass
|
||||
class ArkData:
|
||||
announce_title: str
|
||||
content: str
|
||||
banner_image_url: str | Path | None
|
||||
|
||||
|
||||
class ArknightsTheme(Theme):
|
||||
"""Arknights 公告风格主题
|
||||
|
||||
需要安装`nonebot_plugin_htmlrender`插件
|
||||
"""
|
||||
|
||||
name: Literal["arknights"] = "arknights"
|
||||
need_browser: bool = True
|
||||
|
||||
template_path: Path = Path(__file__).parent / "templates"
|
||||
template_name: str = "announce.html.jinja"
|
||||
|
||||
async def render(self, post: "Post"):
|
||||
from nonebot_plugin_htmlrender import template_to_pic
|
||||
|
||||
if not post.title:
|
||||
raise ThemeRenderUnsupportError("标题为空")
|
||||
if post.images and len(post.images) > 1:
|
||||
raise ThemeRenderUnsupportError("图片数量大于1")
|
||||
|
||||
banner = post.images[0] if post.images else None
|
||||
|
||||
if banner is not None and not isinstance(banner, str | Path):
|
||||
raise ThemeRenderUnsupportError(f"图片类型错误, 期望 str 或 Path, 实际为 {type(banner)}")
|
||||
|
||||
ark_data = ArkData(
|
||||
announce_title=post.title,
|
||||
content=post.content,
|
||||
banner_image_url=banner,
|
||||
)
|
||||
|
||||
try:
|
||||
announce_pic = await template_to_pic(
|
||||
template_path=self.template_path.as_posix(),
|
||||
template_name=self.template_name,
|
||||
templates={
|
||||
"data": ark_data,
|
||||
},
|
||||
pages={
|
||||
"viewport": {"width": 600, "height": 100},
|
||||
"base_url": self.template_path.as_uri(),
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
raise ThemeRenderError(f"渲染文本失败: {e}")
|
||||
msgs: list[MessageSegmentFactory] = []
|
||||
msgs.append(Image(announce_pic))
|
||||
if post.url:
|
||||
msgs.append(Text(f"前往:{post.url}"))
|
||||
return [Image(announce_pic)]
|
||||
@@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, minimum-scale=1"
|
||||
/>
|
||||
<link rel="icon" href="data:;base64,=" />
|
||||
<title>公告</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="main">
|
||||
<div class="container">
|
||||
<div class="standerd-container">
|
||||
{% if data.banner_image_url %}
|
||||
<div class="banner-image-container">
|
||||
<img class="banner-image" src="{{ data.banner_image_url }}" />
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="head-title-container">
|
||||
<span class="head-title">{{ data.announce_title }}</span>
|
||||
</div>
|
||||
<div class="content">{{ data.content }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
引用自 https://ak.hycdn.cn/announce/assets/css/announcement.v_0_1_2.css
|
||||
**/
|
||||
@media screen and (max-device-width: 480px) {
|
||||
body {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
}
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
body,
|
||||
head {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
body {
|
||||
background-color: #313131;
|
||||
min-height: 100%;
|
||||
background-color: #d0d0cf;
|
||||
}
|
||||
.main {
|
||||
max-width: 980px;
|
||||
font-family: "Microsoft Yahei";
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
font-size: 1rem;
|
||||
min-height: 100%;
|
||||
}
|
||||
.main .container {
|
||||
min-height: 100%;
|
||||
}
|
||||
.main .container .standerd-container {
|
||||
padding: 2.72727273%;
|
||||
width: 94.54545455%;
|
||||
margin: auto;
|
||||
}
|
||||
.main .container .standerd-container .banner-image-container {
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
.main .container .standerd-container .banner-image-container .banner-image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
.main .container .standerd-container .head-title-container {
|
||||
margin: 0;
|
||||
background-image: url(
|
||||
https://ak.hycdn.cn/announce/assets/images/announcement/header.jpg);
|
||||
background-size: cover;
|
||||
position: relative;
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
.main .container .standerd-container .head-title-container::before {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding-top: 6.02564103%;
|
||||
}
|
||||
.main .container .standerd-container .head-title-container .head-title {
|
||||
padding-left: 0.25rem;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.main .container .standerd-container .content {
|
||||
line-height: 0.8rem;
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
.main .container .standerd-container .content h4 {
|
||||
font-size: 110%;
|
||||
margin-block-start: 0.5rem;
|
||||
margin-block-end: 0.5rem;
|
||||
}
|
||||
.main .container .standerd-container .content p {
|
||||
margin-block-start: 0.25rem;
|
||||
margin-block-end: 0.25rem;
|
||||
min-height: 0.8rem;
|
||||
}
|
||||
.main .container .standerd-container .content img {
|
||||
max-width: 100%;
|
||||
margin: auto;
|
||||
display: block;
|
||||
}
|
||||
.main .container .banner-image-container.cover {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
}
|
||||
.main .container .banner-image-container.cover .cover-jumper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.main .container .banner-image-container.cover .banner-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
from .build import BasicTheme
|
||||
|
||||
__theme_meta__ = BasicTheme()
|
||||
@@ -0,0 +1,40 @@
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
|
||||
from nonebot_plugin_saa import Text, Image, MessageSegmentFactory
|
||||
|
||||
from nonebot_bison.theme import Theme
|
||||
from nonebot_bison.utils import pic_merge, is_pics_mergable
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from nonebot_bison.post import Post
|
||||
|
||||
|
||||
class BasicTheme(Theme):
|
||||
"""最基本的主题
|
||||
|
||||
纯文本,应为每个Post必定支持的Theme
|
||||
"""
|
||||
|
||||
name: Literal["basic"] = "basic"
|
||||
|
||||
async def render(self, post: "Post") -> list[MessageSegmentFactory]:
|
||||
text = ""
|
||||
|
||||
if post.title:
|
||||
text += f"{post.title}\n\n"
|
||||
|
||||
text += post.content if len(post.content) < 500 else f"{post.content[:500]}..."
|
||||
|
||||
text += f"\n来源: {post.platform.name} {post.nickname or ''}\n"
|
||||
|
||||
if post.url:
|
||||
text += f"详情: {post.url}"
|
||||
|
||||
msgs: list[MessageSegmentFactory] = [Text(text)]
|
||||
if post.images:
|
||||
pics = post.images
|
||||
if is_pics_mergable(pics):
|
||||
pics = await pic_merge(list(pics), post.platform.client)
|
||||
msgs.extend(map(Image, pics))
|
||||
|
||||
return msgs
|
||||
@@ -0,0 +1,3 @@
|
||||
from .build import BriefTheme
|
||||
|
||||
__theme_meta__ = BriefTheme()
|
||||
@@ -0,0 +1,32 @@
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
|
||||
from nonebot_plugin_saa import Text, Image, MessageSegmentFactory
|
||||
|
||||
from nonebot_bison.utils import pic_merge, is_pics_mergable
|
||||
from nonebot_bison.theme import Theme, ThemeRenderUnsupportError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from nonebot_bison.post import Post
|
||||
|
||||
|
||||
class BriefTheme(Theme):
|
||||
"""简报主题,只发送标题、头图(如果有)、URL(如果有)"""
|
||||
|
||||
name: Literal["brief"] = "brief"
|
||||
|
||||
async def render(self, post: "Post") -> list[MessageSegmentFactory]:
|
||||
if not post.title:
|
||||
raise ThemeRenderUnsupportError("Post has no title")
|
||||
text = f"{post.title}\n\n"
|
||||
text += f"来源: {post.platform.name} {post.nickname or ''}\n"
|
||||
if post.url:
|
||||
text += f"详情: {post.url}"
|
||||
|
||||
msgs: list[MessageSegmentFactory] = [Text(text)]
|
||||
if post.images:
|
||||
pics = post.images
|
||||
if is_pics_mergable(pics):
|
||||
pics = await pic_merge(list(pics), post.platform.client)
|
||||
msgs.append(Image(pics[0]))
|
||||
|
||||
return msgs
|
||||
@@ -0,0 +1,10 @@
|
||||
# Jinja模版与LOGO图片说明
|
||||
|
||||
## LOGO图片
|
||||
|
||||
- `templates/ceobecanteen_logo.png`
|
||||
|
||||
### 版权声明
|
||||
|
||||
<a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/4.0/"><img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-sa/4.0/88x31.png" /></a><br />logo图片采用<a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/4.0/">知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议</a>进行许可。
|
||||
本项目<img src="templates/ceobecanteen_logo.png" style="width:100px">使用已经过 [Ceobe Canteen](https://github.com/Enraged-Dun-Cookie-Development-Team) 授权许可使用。
|
||||
@@ -0,0 +1,3 @@
|
||||
from .build import CeobeCanteenTheme
|
||||
|
||||
__theme_meta__ = CeobeCanteenTheme()
|
||||
@@ -0,0 +1,113 @@
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
|
||||
import jinja2
|
||||
from pydantic import BaseModel, root_validator
|
||||
from nonebot_plugin_saa import Text, Image, MessageSegmentFactory
|
||||
|
||||
from nonebot_bison.theme.utils import convert_to_qr
|
||||
from nonebot_bison.theme import Theme, ThemeRenderError, ThemeRenderUnsupportError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from nonebot_bison.post import Post
|
||||
|
||||
|
||||
class CeobeInfo(BaseModel):
|
||||
"""卡片的信息部分
|
||||
|
||||
datasource: 数据来源
|
||||
|
||||
time: 时间
|
||||
"""
|
||||
|
||||
datasource: str
|
||||
time: str
|
||||
|
||||
|
||||
class CeoboContent(BaseModel):
|
||||
"""卡片的内容部分
|
||||
|
||||
image: 图片链接
|
||||
text: 文字内容
|
||||
"""
|
||||
|
||||
image: str | None
|
||||
text: str | None
|
||||
|
||||
@root_validator
|
||||
def check(cls, values):
|
||||
if values["image"] is None and values["text"] is None:
|
||||
raise ValueError("image and text cannot be both None")
|
||||
return values
|
||||
|
||||
|
||||
class CeobeCard(BaseModel):
|
||||
info: CeobeInfo
|
||||
content: CeoboContent
|
||||
qr: str | None
|
||||
|
||||
|
||||
class CeobeCanteenTheme(Theme):
|
||||
"""小刻食堂 分享卡片风格主题
|
||||
|
||||
需要安装`nonebot_plugin_htmlrender`插件
|
||||
"""
|
||||
|
||||
name: Literal["ceobecanteen"] = "ceobecanteen"
|
||||
need_browser: bool = True
|
||||
|
||||
template_path: Path = Path(__file__).parent / "templates"
|
||||
template_name: str = "ceobe_canteen.html.jinja"
|
||||
|
||||
def parse(self, post: "Post") -> CeobeCard:
|
||||
"""解析 Post 为 CeobeCard"""
|
||||
if not post.nickname:
|
||||
raise ThemeRenderUnsupportError("post.nickname is None")
|
||||
if not post.timestamp:
|
||||
raise ThemeRenderUnsupportError("post.timestamp is None")
|
||||
info = CeobeInfo(
|
||||
datasource=post.nickname, time=datetime.fromtimestamp(post.timestamp).strftime("%Y-%m-%d %H:%M:%S")
|
||||
)
|
||||
|
||||
head_pic = post.images[0] if post.images else None
|
||||
if head_pic is not None and not isinstance(head_pic, str):
|
||||
raise ThemeRenderUnsupportError("post.images[0] is not str")
|
||||
|
||||
content = CeoboContent(image=head_pic, text=post.content)
|
||||
return CeobeCard(info=info, content=content, qr=convert_to_qr(post.url or "No URL"))
|
||||
|
||||
async def render(self, post: "Post") -> list[MessageSegmentFactory]:
|
||||
ceobe_card = self.parse(post)
|
||||
from nonebot_plugin_htmlrender import get_new_page
|
||||
|
||||
template_env = jinja2.Environment(
|
||||
loader=jinja2.FileSystemLoader(self.template_path),
|
||||
enable_async=True,
|
||||
)
|
||||
template = template_env.get_template(self.template_name)
|
||||
html = await template.render_async(card=ceobe_card)
|
||||
pages = {
|
||||
"viewport": {"width": 1000, "height": 3000},
|
||||
"base_url": self.template_path.as_uri(),
|
||||
}
|
||||
try:
|
||||
async with get_new_page(**pages) as page:
|
||||
await page.goto(self.template_path.as_uri())
|
||||
await page.set_content(html)
|
||||
await page.wait_for_timeout(1)
|
||||
img_raw = await page.locator("#ceobecanteen-card").screenshot(
|
||||
type="png",
|
||||
)
|
||||
except Exception as e:
|
||||
raise ThemeRenderError(f"Render error: {e}") from e
|
||||
msgs: list[MessageSegmentFactory] = [Image(img_raw)]
|
||||
|
||||
text = f"来源: {post.platform.name} {post.nickname or ''}\n"
|
||||
if post.url:
|
||||
text += f"详情: {post.url}"
|
||||
msgs.append(Text(text))
|
||||
|
||||
if post.images:
|
||||
msgs.extend(map(Image, post.images))
|
||||
return msgs
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 106 KiB |
@@ -0,0 +1,98 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>小刻食堂分享卡片</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="ceobecanteen-card">
|
||||
{% if card.content.image %}
|
||||
<img src="{{ card.content.image }}" class="cover-img">
|
||||
{% endif %}
|
||||
{% if card.content.text %}
|
||||
<div class="main-content">{{ card.content.text }}</div>
|
||||
{% endif %}
|
||||
<div class="footer">
|
||||
<div class="datasource">
|
||||
<div class="datasource-text">
|
||||
<div class="datasource-name">{{ card.info.datasource }}</div>
|
||||
<div class="time">{{ card.info.time }}</div>
|
||||
</div>
|
||||
<img class='qr' src="{{ card.qr }}">
|
||||
</div>
|
||||
<div class="source">
|
||||
<img class='bison-logo' src="bison_logo.jpg">
|
||||
<div class="source-text">
|
||||
<div class="slogan">小刻吃到饼啦!</div>
|
||||
<div class="linkage">bison&小刻食堂联动</div>
|
||||
<div class="description">来自小刻食堂再转发</div>
|
||||
</div>
|
||||
<img class='ceobe-logo' src="ceobecanteen_logo.png">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<style type="text/css">
|
||||
#ceobecanteen-card {
|
||||
width: 700px;
|
||||
background-color: rgb(240, 236, 233);
|
||||
}
|
||||
.cover-img {
|
||||
width: 100%;
|
||||
}
|
||||
.main-content {
|
||||
padding: 30px;
|
||||
white-space: pre-line;
|
||||
}
|
||||
.footer {
|
||||
margin: 0 2%;
|
||||
height: 80px;
|
||||
border-top: 1px solid rgb(185, 181, 177);
|
||||
display: flex;
|
||||
padding: 10px 0;
|
||||
}
|
||||
.footer .datasource {
|
||||
width: 45%;
|
||||
padding-right: 5px;
|
||||
border-right: 1px solid rgb(185, 181, 177);
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
.footer .datasource .datasource-text {
|
||||
align-self: center;
|
||||
}
|
||||
.footer .datasource .datasource-text .datasource-name {
|
||||
font-weight: bold;
|
||||
font-size: 24px;
|
||||
}
|
||||
.footer .datasource .datasource-text .time {
|
||||
font-size: 13px;
|
||||
color: rgb(138, 136, 134);
|
||||
}
|
||||
.footer .datasource .qr {
|
||||
width: 80px;
|
||||
object-fit: contain;
|
||||
}
|
||||
.source {
|
||||
width: 55%;
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
color: rgb(138, 136, 134);
|
||||
font-size: 13px;
|
||||
}
|
||||
.footer .source .source-text {
|
||||
align-self: center;
|
||||
}
|
||||
.source .slogan {
|
||||
font-weight: bold;
|
||||
font-size: 24px;
|
||||
color: black;
|
||||
}
|
||||
.source .bison-logo,
|
||||
.source .ceobe-logo {
|
||||
width: 60px;
|
||||
object-fit: contain;
|
||||
}
|
||||
</style>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 706 KiB |
@@ -0,0 +1,3 @@
|
||||
from .build import Ht2iTheme
|
||||
|
||||
__theme_meta__ = Ht2iTheme()
|
||||
@@ -0,0 +1,50 @@
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
|
||||
from nonebot_plugin_saa import Text, Image, MessageSegmentFactory
|
||||
|
||||
from nonebot_bison.theme import Theme, ThemeRenderError
|
||||
from nonebot_bison.utils import pic_merge, is_pics_mergable
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from nonebot_bison.post import Post
|
||||
|
||||
|
||||
class Ht2iTheme(Theme):
|
||||
"""使用浏览器将文本渲染为图片
|
||||
|
||||
HTML render Text To Image.
|
||||
需要安装`nonebot_plugin_htmlrender`插件
|
||||
"""
|
||||
|
||||
name: Literal["ht2i"] = "ht2i"
|
||||
need_browser: bool = True
|
||||
|
||||
async def _text_render(self, text: str):
|
||||
from nonebot_plugin_htmlrender import text_to_pic
|
||||
|
||||
try:
|
||||
return Image(await text_to_pic(text))
|
||||
except Exception as e:
|
||||
raise ThemeRenderError(f"渲染文本失败: {e}")
|
||||
|
||||
async def render(self, post: "Post"):
|
||||
_text = ""
|
||||
|
||||
if post.title:
|
||||
_text += f"{post.title}\n\n"
|
||||
|
||||
_text += post.content if len(post.content) < 500 else f"{post.content[:500]}..."
|
||||
|
||||
_text += f"\n来源: {post.platform.name} {post.nickname or ''}\n"
|
||||
|
||||
msgs: list[MessageSegmentFactory] = [await self._text_render(_text)]
|
||||
|
||||
if post.url:
|
||||
msgs.append(Text(f"详情: {post.url}"))
|
||||
if post.images:
|
||||
pics = post.images
|
||||
if is_pics_mergable(pics):
|
||||
pics = await pic_merge(list(pics), post.platform.client)
|
||||
msgs.extend(map(Image, pics))
|
||||
|
||||
return msgs
|
||||
Reference in New Issue
Block a user