🚚 修改 nonebot_bison 项目结构 (#211)

* 🎨 修改 nonebot_bison 目录位置

* auto fix by pre-commit hooks

* 🚚 fix frontend build target

* 🚚 use soft link

* Revert "🚚 use soft link"

This reverts commit de21f79d5ae1bd5515b04f42a4138cb25ddf3e62.

* 🚚 modify dockerfile

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: felinae98 <731499577@qq.com>
This commit is contained in:
uy/sun
2023-03-09 17:32:51 +08:00
committed by GitHub
parent 3082587662
commit 90816796c7
65 changed files with 83 additions and 35 deletions
+3
View File
@@ -0,0 +1,3 @@
from .post import Post
__all__ = ["Post", "CustomPost"]
+55
View File
@@ -0,0 +1,55 @@
from abc import abstractmethod
from dataclasses import dataclass, field
from functools import reduce
from typing import Optional
from nonebot.adapters.onebot.v11.message import Message, MessageSegment
from ..plugin_config import plugin_config
@dataclass
class BasePost:
@abstractmethod
async def generate_text_messages(self) -> list[MessageSegment]:
"Generate Message list from this instance"
...
@abstractmethod
async def generate_pic_messages(self) -> list[MessageSegment]:
"Generate Message list from this instance with `use_pic`"
...
@dataclass
class OptionalMixin:
# Because of https://stackoverflow.com/questions/51575931/class-inheritance-in-python-3-7-dataclasses
override_use_pic: Optional[bool] = None
compress: bool = False
extra_msg: list[Message] = field(default_factory=list)
def _use_pic(self):
if not self.override_use_pic is None:
return self.override_use_pic
return plugin_config.bison_use_pic
@dataclass
class AbstractPost(OptionalMixin, BasePost):
async def generate_messages(self) -> list[Message]:
if self._use_pic():
msg_segments = await self.generate_pic_messages()
else:
msg_segments = await self.generate_text_messages()
if msg_segments:
if self.compress:
msgs = [reduce(lambda x, y: x.append(y), msg_segments, Message())]
else:
msgs = list(
map(lambda msg_segment: Message([msg_segment]), msg_segments)
)
else:
msgs = []
msgs.extend(self.extra_msg)
return msgs
+74
View File
@@ -0,0 +1,74 @@
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional
from nonebot.adapters.onebot.v11.message import Message, MessageSegment
from nonebot.log import logger
from nonebot.plugin import require
from .abstract_post import AbstractPost, BasePost
@dataclass
class _CustomPost(BasePost):
message_segments: list[MessageSegment] = field(default_factory=list)
css_path: Optional[str] = None # 模板文件所用css路径
async def generate_text_messages(self) -> list[MessageSegment]:
return self.message_segments
async def generate_pic_messages(self) -> list[MessageSegment]:
require("nonebot_plugin_htmlrender")
from nonebot_plugin_htmlrender import md_to_pic
pic_bytes = await md_to_pic(md=self._generate_md(), css_path=self.css_path)
return [MessageSegment.image(pic_bytes)]
def _generate_md(self) -> str:
md = ""
for message_segment in self.message_segments:
if message_segment.type == "text":
md += "{}<br>".format(message_segment.data.get("text", ""))
elif message_segment.type == "image":
# 先尝试获取file的值,没有再尝试获取url的值,都没有则为空
pic_res = message_segment.data.get("file") or message_segment.data.get(
"url", ""
)
if not pic_res:
logger.warning("无法获取到图片资源:MessageSegment.image中file/url字段均为空")
else:
md += "![Image]({})\n".format(pic_res)
else:
logger.warning("custom_post不支持处理类型:{}".format(message_segment.type))
continue
return md
@dataclass
class CustomPost(_CustomPost, AbstractPost):
"""基于 markdown 语法的,自由度较高的推送内容格式
简介:
支持处理text/image两种MessageSegment,
通过将text/image转换成对应的markdown语法以生成markdown文本。
理论上text类型中可以直接使用markdown语法,例如`##第一章`。
但会导致不启用`override_use_pic`时, 发送不会被渲染的纯文本消息。
图片渲染最终由htmlrender执行。
注意:
每一个MessageSegment元素都会被解释为单独的一行
可选参数:
`override_use_pic`:是否覆盖`bison_use_pic`全局配置
`compress`:将所有消息压缩为一条进行发送
`extra_msg`:需要附带发送的额外消息
成员函数:
`generate_text_messages()`:负责生成文本消息
`generate_pic_messages()`:负责生成图片消息
"""
pass
+164
View File
@@ -0,0 +1,164 @@
from dataclasses import dataclass, field
from functools import reduce
from io import BytesIO
from typing import Optional, Union
from nonebot.adapters.onebot.v11.message import Message, MessageSegment
from nonebot.log import logger
from PIL import Image
from ..utils import http_client, parse_text
from .abstract_post import AbstractPost, BasePost, OptionalMixin
@dataclass
class _Post(BasePost):
target_type: str
text: str
url: Optional[str] = None
target_name: Optional[str] = None
pics: list[Union[str, bytes]] = field(default_factory=list)
_message: Optional[list[MessageSegment]] = None
_pic_message: Optional[list[MessageSegment]] = None
async def _pic_url_to_image(self, data: Union[str, bytes]) -> Image.Image:
pic_buffer = BytesIO()
if isinstance(data, str):
async with http_client() as client:
res = await client.get(data)
pic_buffer.write(res.content)
else:
pic_buffer.write(data)
return Image.open(pic_buffer)
def _check_image_square(self, size: tuple[int, int]) -> bool:
return abs(size[0] - size[1]) / size[0] < 0.05
async def _pic_merge(self) -> None:
if len(self.pics) < 3:
return
first_image = await self._pic_url_to_image(self.pics[0])
if not self._check_image_square(first_image.size):
return
images: list[Image.Image] = [first_image]
# first row
for i in range(1, 3):
cur_img = await self._pic_url_to_image(self.pics[i])
if not self._check_image_square(cur_img.size):
return
if cur_img.size[1] != images[0].size[1]: # height not equal
return
images.append(cur_img)
_tmp = 0
x_coord = [0]
for i in range(3):
_tmp += images[i].size[0]
x_coord.append(_tmp)
y_coord = [0, first_image.size[1]]
async def process_row(row: int) -> bool:
if len(self.pics) < (row + 1) * 3:
return False
row_first_img = await self._pic_url_to_image(self.pics[row * 3])
if not self._check_image_square(row_first_img.size):
return False
if row_first_img.size[0] != images[0].size[0]:
return False
image_row: list[Image.Image] = [row_first_img]
for i in range(row * 3 + 1, row * 3 + 3):
cur_img = await self._pic_url_to_image(self.pics[i])
if not self._check_image_square(cur_img.size):
return False
if cur_img.size[1] != row_first_img.size[1]:
return False
if cur_img.size[0] != images[i % 3].size[0]:
return False
image_row.append(cur_img)
images.extend(image_row)
y_coord.append(y_coord[-1] + row_first_img.size[1])
return True
if await process_row(1):
matrix = (3, 2)
else:
matrix = (3, 1)
if await process_row(2):
matrix = (3, 3)
logger.info("trigger merge image")
target = Image.new("RGB", (x_coord[-1], y_coord[-1]))
for y in range(matrix[1]):
for x in range(matrix[0]):
target.paste(
images[y * matrix[0] + x],
(x_coord[x], y_coord[y], x_coord[x + 1], y_coord[y + 1]),
)
target_io = BytesIO()
target.save(target_io, "JPEG")
self.pics = self.pics[matrix[0] * matrix[1] :]
self.pics.insert(0, target_io.getvalue())
async def generate_text_messages(self) -> list[MessageSegment]:
if self._message is None:
await self._pic_merge()
msg_segments: list[MessageSegment] = []
text = ""
if self.text:
text += "{}".format(
self.text if len(self.text) < 500 else self.text[:500] + "..."
)
if text:
text += "\n"
text += "来源: {}".format(self.target_type)
if self.target_name:
text += " {}".format(self.target_name)
if self.url:
text += " \n详情: {}".format(self.url)
msg_segments.append(MessageSegment.text(text))
for pic in self.pics:
msg_segments.append(MessageSegment.image(pic))
self._message = msg_segments
return self._message
async def generate_pic_messages(self) -> list[MessageSegment]:
if self._pic_message is None:
await self._pic_merge()
msg_segments: list[MessageSegment] = []
text = ""
if self.text:
text += "{}".format(self.text)
text += "\n"
text += "来源: {}".format(self.target_type)
if self.target_name:
text += " {}".format(self.target_name)
msg_segments.append(await parse_text(text))
if not self.target_type == "rss" and self.url:
msg_segments.append(MessageSegment.text(self.url))
for pic in self.pics:
msg_segments.append(MessageSegment.image(pic))
self._pic_message = msg_segments
return self._pic_message
def __str__(self):
return "type: {}\nfrom: {}\ntext: {}\nurl: {}\npic: {}".format(
self.target_type,
self.target_name,
self.text if len(self.text) < 500 else self.text[:500] + "...",
self.url,
", ".join(
map(
lambda x: "b64img"
if isinstance(x, bytes) or x.startswith("base64")
else x,
self.pics,
)
),
)
@dataclass
class Post(AbstractPost, _Post):
pass
@@ -0,0 +1,112 @@
@charset "utf-8";
/**
* markdown.css
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
* details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see http://gnu.org/licenses/lgpl.txt.
*
* @project Weblog and Open Source Projects of Florian Wolters
* @version GIT: $Id$
* @package xhtml-css
* @author Florian Wolters <florian.wolters.85@googlemail.com>
* @copyright 2012 Florian Wolters
* @cssdoc version 1.0-pre
* @license http://gnu.org/licenses/lgpl.txt GNU Lesser General Public License
* @link http://github.com/FlorianWolters/jekyll-bootstrap-theme
* @media all
* @valid true
*/
body {
font-family: Helvetica, Arial, Freesans, clean, sans-serif;
padding: 1em;
margin: auto;
max-width: 42em;
background: #fefefe;
}
h1, h2, h3, h4, h5, h6 {
font-weight: bold;
}
h1 {
color: #000000;
font-size: 28px;
}
h2 {
border-bottom: 1px solid #CCCCCC;
color: #000000;
font-size: 24px;
}
h3 {
font-size: 18px;
}
h4 {
font-size: 16px;
}
h5 {
font-size: 14px;
}
h6 {
color: #777777;
background-color: inherit;
font-size: 14px;
}
hr {
height: 0.2em;
border: 0;
color: #CCCCCC;
background-color: #CCCCCC;
}
p, blockquote, ul, ol, dl, li, table, pre {
margin: 15px 0;
}
code, pre {
border-radius: 3px;
background-color: #F8F8F8;
color: inherit;
}
code {
border: 1px solid #EAEAEA;
margin: 0 2px;
padding: 0 5px;
}
pre {
border: 1px solid #CCCCCC;
line-height: 1.25em;
overflow: auto;
padding: 6px 10px;
}
pre>code {
border: 0;
margin: 0;
padding: 0;
}
a, a:visited {
color: #4183C4;
background-color: inherit;
text-decoration: none;
}