mirror of
https://github.com/suyiiyii/nonebot-bison.git
synced 2026-05-08 17:58:55 +08:00
✨ 适配小刻食堂平台 (#379)
* 🐛 插入新的Schedulable时应传入use_batch参数 * ✨ 适配ceobecanteen平台 Co-authored-by: phidiaLam <2957035701@qq.com> * ✨ ✨ 明日方舟公告与官网采用截图分享 (#480) * ✨ 明日方舟公告与官网采用截图分享 * 💄 auto fix by pre-commit hooks * 🐛 修复缺少的导入,优化逻辑 --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Azide <rukuy@qq.com> * 🐛 优化截图图片效果 * 🐛 修复错误将转发内图片视作头图的问题 * 🍱 使用正式 Bison Logo * 💄 auto fix by pre-commit hooks * 🐛 请求小刻API时不在headers里添加过多字段 * 🐛 get_comb_id方法删除无用的targets参数 * 💡 get_comb_id方法更新注释 * 🔥 移除发送部分的更改 * ✨ 在命名中明确表示cond_func意图 * ♻️ 拆分get_comb_id功能 * ♻️ 调整缓存逻辑 * ✨ 使用uri在theme中调用platform截图 * ♻️ 重构截图逻辑 * ✨ 添加模糊匹配提示 * ✨ 适配新版Site * 💄 auto fix by pre-commit hooks * 🐛 去掉不必要的排序 * 🐛 修正不应出现的驼峰变量名 * ♻️ 按review意见修改 * ♻️ 调整截图函数逻辑 * 🔊 调低日志等级 * ✏️ 修复一些拼写和格式 --------- Co-authored-by: phidiaLam <2957035701@qq.com> Co-authored-by: 洛梧藤 <67498817+phidiaLam@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
@@ -3,8 +3,12 @@
|
||||
## LOGO图片
|
||||
|
||||
- `templates/ceobecanteen_logo.png`
|
||||
- `templates/bison_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) 授权许可使用。
|
||||
<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>进行许可。
|
||||
|
||||
本项目使用的 小刻食堂 LOGO <img src="templates/ceobecanteen_logo.png" alt="ceobecanteen-logo" style="width:100px">已经过 [小刻食堂](https://github.com/Enraged-Dun-Cookie-Development-Team) 授权许可使用。
|
||||
|
||||
本项目使用的 Nonebot-Bison LOGO <img src="templates/bison_logo.png" alt="nonebot-bison-logo" style="width:100px">由画师 不画涩图の企鹅 倾情贡献,非常感谢!
|
||||
|
||||
@@ -4,12 +4,15 @@ from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
|
||||
import jinja2
|
||||
from yarl import URL
|
||||
from httpx import AsyncClient
|
||||
from pydantic import BaseModel
|
||||
from PIL import Image as PILImage
|
||||
from nonebot_plugin_saa import Text, Image, MessageSegmentFactory
|
||||
|
||||
from nonebot_bison.compat import model_validator
|
||||
from nonebot_bison.theme.utils import convert_to_qr
|
||||
from nonebot_bison.utils.image import pic_merge, is_pics_mergable
|
||||
from nonebot_bison.utils import pic_merge, is_pics_mergable
|
||||
from nonebot_bison.theme.utils import convert_to_qr, web_embed_image
|
||||
from nonebot_bison.theme import Theme, ThemeRenderError, ThemeRenderUnsupportError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -35,13 +38,26 @@ class CeoboContent(BaseModel):
|
||||
text: 文字内容
|
||||
"""
|
||||
|
||||
image: str | None = None
|
||||
text: str
|
||||
|
||||
|
||||
class CeoboRetweet(BaseModel):
|
||||
"""卡片的转发部分
|
||||
|
||||
author: 原作者
|
||||
image: 图片链接
|
||||
content: 文字内容
|
||||
"""
|
||||
|
||||
image: str | None
|
||||
text: str | None
|
||||
content: str | None
|
||||
author: str
|
||||
|
||||
@model_validator(mode="before")
|
||||
def check(cls, values):
|
||||
if values["image"] is None and values["text"] is None:
|
||||
raise ValueError("image and text cannot be both None")
|
||||
if values["image"] is None and values["content"] is None:
|
||||
raise ValueError("image and content cannot be both None")
|
||||
return values
|
||||
|
||||
|
||||
@@ -49,6 +65,7 @@ class CeobeCard(BaseModel):
|
||||
info: CeobeInfo
|
||||
content: CeoboContent
|
||||
qr: str | None
|
||||
retweet: CeoboRetweet | None
|
||||
|
||||
|
||||
class CeobeCanteenTheme(Theme):
|
||||
@@ -63,8 +80,8 @@ class CeobeCanteenTheme(Theme):
|
||||
template_path: Path = Path(__file__).parent / "templates"
|
||||
template_name: str = "ceobe_canteen.html.jinja"
|
||||
|
||||
def parse(self, post: "Post") -> CeobeCard:
|
||||
"""解析 Post 为 CeobeCard"""
|
||||
async def parse(self, post: "Post") -> tuple[CeobeCard, list[str | bytes | Path | BytesIO]]:
|
||||
"""解析 Post 为 CeobeCard与处理好的图片列表"""
|
||||
if not post.nickname:
|
||||
raise ThemeRenderUnsupportError("post.nickname is None")
|
||||
if not post.timestamp:
|
||||
@@ -73,15 +90,96 @@ class CeobeCanteenTheme(Theme):
|
||||
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")
|
||||
http_client = await post.platform.ctx.get_client_for_static()
|
||||
images: list[str | bytes | Path | BytesIO] = []
|
||||
if post.images:
|
||||
images = await self.merge_pics(post.images, http_client)
|
||||
|
||||
content = CeoboContent(image=head_pic, text=post.content)
|
||||
return CeobeCard(info=info, content=content, qr=convert_to_qr(post.url or "No URL"))
|
||||
content = CeoboContent(text=post.content)
|
||||
|
||||
retweet: CeoboRetweet | None = None
|
||||
if post.repost:
|
||||
repost_head_pic: str | None = None
|
||||
if post.repost.images:
|
||||
repost_images = await self.merge_pics(post.repost.images, http_client)
|
||||
repost_head_pic = self.extract_head_pic(repost_images)
|
||||
images.extend(repost_images)
|
||||
|
||||
repost_nickname = f"@{post.repost.nickname}:" if post.repost.nickname else ""
|
||||
retweet = CeoboRetweet(image=repost_head_pic, content=post.repost.content, author=repost_nickname)
|
||||
|
||||
return (
|
||||
CeobeCard(
|
||||
info=info,
|
||||
content=content,
|
||||
qr=web_embed_image(convert_to_qr(post.url or "No URL", back_color=(240, 236, 233))),
|
||||
retweet=retweet,
|
||||
),
|
||||
images,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def merge_pics(
|
||||
images: list[str | bytes | Path | BytesIO],
|
||||
client: AsyncClient,
|
||||
) -> list[str | bytes | Path | BytesIO]:
|
||||
if is_pics_mergable(images):
|
||||
pics = await pic_merge(images, client)
|
||||
else:
|
||||
pics = images
|
||||
return list(pics)
|
||||
|
||||
@staticmethod
|
||||
def extract_head_pic(pics: list[str | bytes | Path | BytesIO]) -> str:
|
||||
head_pic = web_embed_image(pics[0]) if not isinstance(pics[0], str) else pics[0]
|
||||
return head_pic
|
||||
|
||||
@staticmethod
|
||||
def card_link(head_pic: PILImage.Image, card_body: PILImage.Image) -> PILImage.Image:
|
||||
"""将头像与卡片合并"""
|
||||
|
||||
def resize_image(img: PILImage.Image, size: tuple[int, int]) -> PILImage.Image:
|
||||
return img.resize(size)
|
||||
|
||||
# 统一图片宽度
|
||||
head_pic_w, head_pic_h = head_pic.size
|
||||
card_body_w, card_body_h = card_body.size
|
||||
|
||||
if head_pic_w > card_body_w:
|
||||
head_pic = resize_image(head_pic, (card_body_w, int(head_pic_h * card_body_w / head_pic_w)))
|
||||
else:
|
||||
card_body = resize_image(card_body, (head_pic_w, int(card_body_h * head_pic_w / card_body_w)))
|
||||
|
||||
# 合并图片
|
||||
card = PILImage.new("RGBA", (head_pic.width, head_pic.height + card_body.height))
|
||||
card.paste(head_pic, (0, 0))
|
||||
card.paste(card_body, (0, head_pic.height))
|
||||
return card
|
||||
|
||||
async def render(self, post: "Post") -> list[MessageSegmentFactory]:
|
||||
ceobe_card = self.parse(post)
|
||||
ceobe_card, merged_images = await self.parse(post)
|
||||
|
||||
need_card_link: bool = True
|
||||
head_pic = None
|
||||
|
||||
# 如果没有 post.images,则全部都是转发里的图片,不需要头图
|
||||
if post.images:
|
||||
match merged_images[0]:
|
||||
case bytes():
|
||||
head_pic = merged_images[0]
|
||||
merged_images = merged_images[1:]
|
||||
case BytesIO():
|
||||
head_pic = merged_images[0].getvalue()
|
||||
merged_images = merged_images[1:]
|
||||
case str(s) if URL(s).scheme in ("http", "https"):
|
||||
ceobe_card.content.image = merged_images[0]
|
||||
need_card_link = False
|
||||
case Path():
|
||||
ceobe_card.content.image = merged_images[0].as_uri()
|
||||
need_card_link = False
|
||||
case _:
|
||||
raise ThemeRenderError(f"Unknown image type: {type(merged_images[0])}")
|
||||
|
||||
from nonebot_plugin_htmlrender import get_new_page
|
||||
|
||||
template_env = jinja2.Environment(
|
||||
@@ -91,7 +189,8 @@ class CeobeCanteenTheme(Theme):
|
||||
template = template_env.get_template(self.template_name)
|
||||
html = await template.render_async(card=ceobe_card)
|
||||
pages = {
|
||||
"viewport": {"width": 1000, "height": 3000},
|
||||
"device_scale_factor": 2,
|
||||
"viewport": {"width": 512, "height": 455},
|
||||
"base_url": self.template_path.as_uri(),
|
||||
}
|
||||
try:
|
||||
@@ -99,12 +198,24 @@ class CeobeCanteenTheme(Theme):
|
||||
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",
|
||||
card_body = await page.locator("#ceobecanteen-card").screenshot(
|
||||
type="jpeg",
|
||||
quality=90,
|
||||
)
|
||||
except Exception as e:
|
||||
raise ThemeRenderError(f"Render error: {e}") from e
|
||||
msgs: list[MessageSegmentFactory] = [Image(img_raw)]
|
||||
|
||||
msgs: list[MessageSegmentFactory] = []
|
||||
if need_card_link and head_pic:
|
||||
card_pil = self.card_link(
|
||||
head_pic=PILImage.open(BytesIO(head_pic)),
|
||||
card_body=PILImage.open(BytesIO(card_body)),
|
||||
)
|
||||
card_data = BytesIO()
|
||||
card_pil.save(card_data, format="PNG")
|
||||
msgs.append(Image(card_data.getvalue()))
|
||||
else:
|
||||
msgs.append(Image(card_body))
|
||||
|
||||
text = f"来源: {post.platform.name} {post.nickname or ''}\n"
|
||||
if post.url:
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 106 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 640 KiB |
@@ -11,6 +11,19 @@
|
||||
{% endif %}
|
||||
{% if card.content.text %}
|
||||
<div class="main-content">{{ card.content.text }}</div>
|
||||
{% if card.retweet %}
|
||||
<div class="retweet">
|
||||
{% if card.retweet.author %}
|
||||
<div class="origin-author">{{ card.retweet.author }}</div>
|
||||
{% endif %}
|
||||
{% if card.retweet.content %}
|
||||
<div class="retweet-content">{{ card.retweet.content }}</div>
|
||||
{% endif %}
|
||||
{% if card.retweet.image %}
|
||||
<img class='retweet-image' src="{{ card.retweet.image }}">
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<div class="footer">
|
||||
<div class="datasource">
|
||||
@@ -21,7 +34,7 @@
|
||||
<img class='qr' src="{{ card.qr }}">
|
||||
</div>
|
||||
<div class="source">
|
||||
<img class='bison-logo' src="bison_logo.jpg">
|
||||
<img class='bison-logo' src="bison_logo.png">
|
||||
<div class="source-text">
|
||||
<div class="slogan">小刻吃到饼啦!</div>
|
||||
<div class="linkage">bison&小刻食堂联动</div>
|
||||
@@ -46,6 +59,16 @@
|
||||
padding: 30px;
|
||||
white-space: pre-line;
|
||||
}
|
||||
.retweet {
|
||||
margin: -20px 30px 20px;
|
||||
background-color: rgb(226, 223, 219);
|
||||
border: solid 1px rgb(212, 210, 207);
|
||||
border-radius: 3px;
|
||||
padding: 5px;
|
||||
}
|
||||
.retweet .retweet-image {
|
||||
width: 100%;
|
||||
}
|
||||
.footer {
|
||||
margin: 0 2%;
|
||||
height: 80px;
|
||||
|
||||
Reference in New Issue
Block a user