添加 Theme 功能

This commit is contained in:
Azide 2023-10-13 23:18:39 +08:00 committed by felinae98
parent 6aaec45d15
commit f202071e9f
57 changed files with 1709 additions and 802 deletions

View File

@ -7,7 +7,7 @@ require("nonebot_plugin_saa")
import nonebot_plugin_saa
from .plugin_config import PlugConfig, plugin_config
from . import post, send, types, utils, config, platform, bootstrap, scheduler, admin_page, sub_manager
from . import post, send, theme, types, utils, config, platform, bootstrap, scheduler, admin_page, sub_manager
__help__version__ = "0.8.2"
nonebot_plugin_saa.enable_auto_select_bot()
@ -43,4 +43,5 @@ __all__ = [
"platform",
"types",
"utils",
"theme",
]

View File

@ -1,14 +1,56 @@
from typing import Any
from pathlib import Path
from functools import partial
from httpx import AsyncClient
from nonebot.plugin import require
from bs4 import BeautifulSoup as bs
from pydantic import Field, BaseModel
from ..post import Post
from ..types import Target, RawPost, Category
from .platform import NewMessage, StatusChange
from ..utils.scheduler_config import SchedulerConfig
from .platform import NewMessage, StatusChange, CategoryNotRecognize
class ArkResponseBase(BaseModel):
code: int
msg: str
class BulletinListItem(BaseModel):
cid: str
title: str
category: int
display_time: str = Field(alias="displayTime")
updated_at: int = Field(alias="updatedAt")
sticky: bool
class BulletinList(BaseModel):
list: list[BulletinListItem]
class Config:
extra = "ignore"
class BulletinData(BaseModel):
cid: str
display_type: int = Field(alias="displayType")
title: str
category: int
header: str
content: str
jump_link: str = Field(alias="jumpLink")
banner_image_url: str = Field(alias="bannerImageUrl")
display_time: str = Field(alias="displayTime")
updated_at: int = Field(alias="updatedAt")
class ArkBulletinListResponse(ArkResponseBase):
data: BulletinList
class ArkBulletinResponse(ArkResponseBase):
data: BulletinData
class ArknightsSchedConf(SchedulerConfig):
@ -26,74 +68,52 @@ class Arknights(NewMessage):
is_common = False
scheduler = ArknightsSchedConf
has_target = False
default_theme = "arknights"
@classmethod
async def get_target_name(cls, client: AsyncClient, target: Target) -> str | None:
return "明日方舟游戏信息"
async def get_sub_list(self, _) -> list[RawPost]:
async def get_sub_list(self, _) -> list[BulletinListItem]:
raw_data = await self.client.get("https://ak-webview.hypergryph.com/api/game/bulletinList?target=IOS")
return raw_data.json()["data"]["list"]
return ArkBulletinListResponse.parse_obj(raw_data.json()).data.list
def get_id(self, post: RawPost) -> Any:
return post["cid"]
def get_id(self, post: BulletinListItem) -> Any:
return post.cid
def get_date(self, _: RawPost) -> Any:
return None
def get_date(self, post: BulletinListItem) -> Any:
return post.updated_at
def get_category(self, _) -> Category:
return Category(1)
async def parse(self, raw_post: RawPost) -> Post:
async def parse(self, raw_post: BulletinListItem) -> Post:
raw_data = await self.client.get(
f"https://ak-webview.hypergryph.com/api/game/bulletin/{self.get_id(post=raw_post)}"
)
raw_data = raw_data.json()["data"]
data = ArkBulletinResponse.parse_obj(raw_data.json()).data
announce_title = raw_data.get("header") if raw_data.get("header") != "" else raw_data.get("title")
text = ""
def title_escape(text: str) -> str:
return text.replace("\n", " - ")
pics = []
if raw_data["bannerImageUrl"]:
pics.append(raw_post["bannerImageUrl"])
elif raw_data["content"]:
require("nonebot_plugin_htmlrender")
from nonebot_plugin_htmlrender import template_to_pic
template_path = str(Path(__file__).parent.parent / "post/templates/ark_announce")
pic_data = await template_to_pic(
template_path=template_path,
template_name="index.html",
templates={
"bannerImageUrl": raw_data["bannerImageUrl"],
"announce_title": announce_title,
"content": raw_data["content"],
},
pages={
"viewport": {"width": 400, "height": 100},
"base_url": f"file://{template_path}",
},
)
# render = Render()
# viewport = {"width": 320, "height": 6400, "deviceScaleFactor": 3}
# pic_data = await render.render(
# announce_url, viewport=viewport, target="div.main"
# )
if pic_data:
pics.append(pic_data)
else:
text = "图片渲染失败"
# gen title, content
if data.header:
# header是title的更详细版本
# header会和content一起出现
title = data.header
else:
raise CategoryNotRecognize("未找到可渲染部分")
# 只有一张图片
title = title_escape(data.title)
return Post(
"arknights",
text=text,
url="",
target_name="明日方舟游戏内公告",
pics=pics,
self,
content=data.content,
title=title,
nickname="明日方舟游戏内公告",
images=[data.banner_image_url] if data.banner_image_url else None,
url=data.jump_link or None,
timestamp=data.updated_at,
compress=True,
override_use_pic=False,
)
@ -106,6 +126,7 @@ class AkVersion(StatusChange):
is_common = False
scheduler = ArknightsSchedConf
has_target = False
default_theme = "brief"
@classmethod
async def get_target_name(cls, client: AsyncClient, target: Target) -> str | None:
@ -122,18 +143,15 @@ class AkVersion(StatusChange):
def compare_status(self, _, old_status, new_status):
res = []
ArkUpdatePost = partial(Post, self, "", nickname="明日方舟更新信息")
if old_status.get("preAnnounceType") == 2 and new_status.get("preAnnounceType") == 0:
res.append(
Post("arknights", text="登录界面维护公告上线(大概是开始维护了)", target_name="明日方舟更新信息")
)
res.append(ArkUpdatePost(title="登录界面维护公告上线(大概是开始维护了)"))
elif old_status.get("preAnnounceType") == 0 and new_status.get("preAnnounceType") == 2:
res.append(
Post("arknights", text="登录界面维护公告下线(大概是开服了,冲!)", target_name="明日方舟更新信息")
)
res.append(ArkUpdatePost(title="登录界面维护公告下线(大概是开服了,冲!)"))
if old_status.get("clientVersion") != new_status.get("clientVersion"):
res.append(Post("arknights", text="游戏本体更新(大更新)", target_name="明日方舟更新信息"))
res.append(ArkUpdatePost(title="游戏本体更新(大更新)"))
if old_status.get("resVersion") != new_status.get("resVersion"):
res.append(Post("arknights", text="游戏资源更新(小更新)", target_name="明日方舟更新信息"))
res.append(ArkUpdatePost(title="游戏资源更新(小更新)"))
return res
def get_category(self, _):
@ -180,13 +198,12 @@ class MonsterSiren(NewMessage):
imgs = [x["src"] for x in soup("img")]
text = f'{raw_post["title"]}\n{soup.text.strip()}'
return Post(
"monster-siren",
text=text,
pics=imgs,
self,
text,
images=imgs,
url=url,
target_name="塞壬唱片新闻",
nickname="塞壬唱片新闻",
compress=True,
override_use_pic=False,
)
@ -199,6 +216,7 @@ class TerraHistoricusComic(NewMessage):
is_common = False
scheduler = ArknightsSchedConf
has_target = False
default_theme = "brief"
@classmethod
async def get_target_name(cls, client: AsyncClient, target: Target) -> str | None:
@ -220,11 +238,11 @@ class TerraHistoricusComic(NewMessage):
async def parse(self, raw_post: RawPost) -> Post:
url = f'https://terra-historicus.hypergryph.com/comic/{raw_post["comicCid"]}/episode/{raw_post["episodeCid"]}'
return Post(
"terra-historicus",
text=f'{raw_post["title"]} - {raw_post["episodeShortTitle"]}',
pics=[raw_post["coverUrl"]],
self,
raw_post["subtitle"],
title=f'{raw_post["title"]} - {raw_post["episodeShortTitle"]}',
images=[raw_post["coverUrl"]],
url=url,
target_name="泰拉记事社漫画",
nickname="泰拉记事社漫画",
compress=True,
override_use_pic=False,
)

View File

@ -193,7 +193,7 @@ class Bilibili(NewMessage):
text += orig_text
else:
raise CategoryNotSupport(post_type)
return Post("bilibili", text=text, url=url, pics=pic, target_name=target_name)
return Post(self, text, url=url, images=pic, nickname=target_name)
class Bilibililive(StatusChange):
@ -206,6 +206,7 @@ class Bilibililive(StatusChange):
name = "Bilibili直播"
has_target = True
use_batch = True
default_theme = "brief"
@unique
class LiveStatus(Enum):
@ -334,11 +335,12 @@ class Bilibililive(StatusChange):
title = f"[{self.categories[raw_post.category].rstrip('提醒')}] {raw_post.title}"
target_name = f"{raw_post.uname} {raw_post.area_name}"
return Post(
self.name,
text=title,
self,
"",
title=title,
url=url,
pics=list(pic),
target_name=target_name,
images=list(pic),
nickname=target_name,
compress=True,
)
@ -353,6 +355,7 @@ class BilibiliBangumi(StatusChange):
name = "Bilibili剧集"
has_target = True
parse_target_promot = "请输入剧集主页"
default_theme = "brief"
_url = "https://api.bilibili.com/pgc/review/user"
@ -384,7 +387,7 @@ class BilibiliBangumi(StatusChange):
if res_dict["code"] == 0:
return {
"index": res_dict["result"]["media"]["new_ep"]["index"],
"index_show": res_dict["result"]["media"]["new_ep"]["index"],
"index_show": res_dict["result"]["media"]["new_ep"]["index_show"],
"season_id": res_dict["result"]["media"]["season_id"],
}
else:
@ -412,13 +415,15 @@ class BilibiliBangumi(StatusChange):
url = lastest_episode["link"]
pic: list[str] = [lastest_episode["cover"]]
target_name = detail_dict["result"]["season_title"]
text = lastest_episode["share_copy"]
content = raw_post["index_show"]
title = lastest_episode["share_copy"]
return Post(
self.name,
text=text,
self,
content,
title=title,
url=url,
pics=list(pic),
target_name=target_name,
images=list(pic),
nickname=target_name,
compress=True,
)

View File

@ -40,6 +40,7 @@ class FF14(NewMessage):
return None
async def parse(self, raw_post: RawPost) -> Post:
text = f'{raw_post["Title"]}\n{raw_post["Summary"]}'
title = raw_post["Title"]
text = raw_post["Summary"]
url = raw_post["Author"]
return Post("ff14", text=text, url=url, target_name="最终幻想XIV官方公告")
return Post(self, text, title=title, url=url, nickname="最终幻想XIV官方公告")

View File

@ -155,11 +155,11 @@ class McbbsNews(NewMessage):
pics = await self._news_render(post_url, f"#{post_id}")
return Post(
self.name,
text="{}\n\n└由 {} 发表".format(post["title"], post["author"]),
self,
"{}\n\n└由 {} 发表".format(post["title"], post["author"]),
url=post_url,
pics=list(pics),
target_name=post["category"],
images=list(pics),
nickname=post["category"],
)
async def _news_render(self, url: str, selector: str) -> list[bytes]:

View File

@ -68,7 +68,7 @@ class NcmArtist(NewMessage):
target_name = raw_post["artist"]["name"]
pics = [raw_post["picUrl"]]
url = "https://music.163.com/#/album?id={}".format(raw_post["id"])
return Post("ncm-artist", text=text, url=url, pics=pics, target_name=target_name)
return Post(self, text, url=url, images=pics, nickname=target_name)
class NcmRadio(NewMessage):
@ -126,4 +126,4 @@ class NcmRadio(NewMessage):
target_name = raw_post["radio"]["name"]
pics = [raw_post["coverUrl"]]
url = "https://music.163.com/#/program/{}".format(raw_post["id"])
return Post("ncm-radio", text=text, url=url, pics=pics, target_name=target_name)
return Post(self, text, url=url, images=pics, nickname=target_name)

View File

@ -95,6 +95,8 @@ class Platform(metaclass=PlatformABCMeta, base=True):
client: AsyncClient
reverse_category: dict[str, Category]
use_batch: bool = False
# TODO: 限定可使用的theme名称
default_theme: str = "basic"
@classmethod
@abstractmethod

View File

@ -53,28 +53,29 @@ class Rss(NewMessage):
entry["_target_name"] = feed.feed.title
return feed.entries
def _text_process(self, title: str, desc: str) -> str:
def _text_process(self, title: str, desc: str) -> tuple[str | None, str]:
"""检查标题和描述是否相似如果相似则标题为None, 否则返回标题和描述"""
similarity = 1.0 if len(title) == 0 or len(desc) == 0 else text_similarity(title, desc)
if similarity > 0.8:
text = title if len(title) > len(desc) else desc
else:
text = title + "\n\n" + desc
return text
return None, title if len(title) > len(desc) else desc
return title, desc
async def parse(self, raw_post: RawPost) -> Post:
title = raw_post.get("title", "")
soup = bs(raw_post.description, "html.parser")
desc = soup.text.strip()
text = self._text_process(title, desc)
title, desc = self._text_process(title, desc)
pics = [x.attrs["src"] for x in soup("img")]
if raw_post.get("media_content"):
for media in raw_post["media_content"]:
if media.get("medium") == "image" and media.get("url"):
pics.append(media.get("url"))
return Post(
"rss",
text=text,
self,
desc,
title=title,
url=raw_post.link,
pics=pics,
target_name=raw_post["_target_name"],
images=pics,
nickname=raw_post["_target_name"],
)

View File

@ -155,9 +155,9 @@ class Weibo(NewMessage):
detail_url = f"https://weibo.com/{info['user']['id']}/{info['bid']}"
# return parsed_text, detail_url, pic_urls
return Post(
"weibo",
text=parsed_text,
self,
parsed_text,
url=detail_url,
pics=pics,
target_name=info["user"]["screen_name"],
images=pics,
nickname=info["user"]["screen_name"],
)

View File

@ -1,12 +1,18 @@
import nonebot
from pydantic import BaseSettings
from pydantic import Field, BaseSettings
global_config = nonebot.get_driver().config
PlatformName = str
ThemeName = str
class PlugConfig(BaseSettings):
bison_config_path: str = ""
bison_use_pic: bool = False
bison_use_pic: bool = Field(
default=False,
description="发送消息时将所有文本转换为图片,防止风控,仅需要推送文转图可以为 platform 指定 theme",
)
bison_theme_use_browser: bool = Field(default=False, description="是否允许主题使用浏览器")
bison_init_filter: bool = True
bison_use_queue: bool = True
bison_outer_url: str = ""
@ -17,10 +23,12 @@ class PlugConfig(BaseSettings):
# 0不启用1首条消息单独发送剩余照片合并转发2以及以上所有消息全部合并转发
bison_resend_times: int = 0
bison_proxy: str | None
bison_ua: str = (
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36"
bison_ua: str = Field(
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36",
description="默认UA",
)
bison_show_network_warning: bool = True
bison_platform_theme: dict[PlatformName, ThemeName] = {}
@property
def outer_url(self) -> str:

View File

@ -1,3 +1 @@
from .post import Post
__all__ = ["Post"]
from .post import Post as Post

View File

@ -1,52 +1,51 @@
from functools import reduce
from abc import abstractmethod
from dataclasses import field, dataclass
from dataclasses import dataclass
from abc import ABC, abstractmethod
from nonebot_plugin_saa import MessageFactory, MessageSegmentFactory
from nonebot_plugin_saa import Text, MessageFactory, MessageSegmentFactory
from ..utils import text_to_image
from ..plugin_config import plugin_config
@dataclass
class BasePost:
@abstractmethod
async def generate_text_messages(self) -> list[MessageSegmentFactory]:
"Generate MessageFactory list from this instance"
...
@abstractmethod
async def generate_pic_messages(self) -> list[MessageSegmentFactory]:
"Generate MessageFactory 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: bool | None = None
@dataclass(kw_only=True)
class AbstractPost(ABC):
compress: bool = False
extra_msg: list[MessageFactory] = field(default_factory=list)
extra_msg: list[MessageFactory] | None = None
def _use_pic(self):
if self.override_use_pic is not None:
return self.override_use_pic
return plugin_config.bison_use_pic
@abstractmethod
async def generate(self) -> list[MessageSegmentFactory]:
"Generate MessageSegmentFactory list from this instance"
...
@dataclass
class AbstractPost(OptionalMixin, BasePost):
async def generate_messages(self) -> list[MessageFactory]:
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, MessageFactory([]))]
else:
msgs = [MessageFactory([msg_segment]) for msg_segment in msg_segments]
else:
msgs = []
msgs.extend(self.extra_msg)
"really call to generate messages"
msg_segments = await self.generate()
msg_segments = await self.message_segments_process(msg_segments)
msgs = await self.message_process(msg_segments)
return msgs
async def message_segments_process(self, msg_segments: list[MessageSegmentFactory]) -> list[MessageSegmentFactory]:
"generate message segments and process them"
async def convert(msg: MessageSegmentFactory) -> MessageSegmentFactory:
if isinstance(msg, Text):
return await text_to_image(msg)
else:
return msg
if plugin_config.bison_use_pic:
return [await convert(msg) for msg in msg_segments]
return msg_segments
async def message_process(self, msg_segments: list[MessageSegmentFactory]) -> list[MessageFactory]:
"generate messages and process them"
if self.compress:
msgs = [MessageFactory(msg_segments)]
else:
msgs = [MessageFactory(msg_segment) for msg_segment in msg_segments]
if self.extra_msg:
msgs.extend(self.extra_msg)
return msgs

View File

@ -1,68 +0,0 @@
from dataclasses import field, dataclass
from nonebot.log import logger
from nonebot.plugin import require
from nonebot.adapters.onebot.v11 import MessageSegment
from nonebot_plugin_saa import Text, Image, MessageSegmentFactory
from .abstract_post import BasePost, AbstractPost
@dataclass
class _CustomPost(BasePost):
ms_factories: list[MessageSegmentFactory] = field(default_factory=list)
css_path: str = "" # 模板文件所用css路径
async def generate_text_messages(self) -> list[MessageSegmentFactory]:
return self.ms_factories
async def generate_pic_messages(self) -> list[MessageSegmentFactory]:
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 [Image(pic_bytes)]
def _generate_md(self) -> str:
md = ""
for message_segment in self.ms_factories:
match message_segment:
case Text(data={"text": text}):
md += f"{text}<br>"
case Image(data={"image": image}):
# use onebot v11 to convert image into url
ob11_image = MessageSegment.image(image)
md += "![Image]({})\n".format(ob11_image.data["file"])
case _:
logger.warning(f"custom_post不支持处理类型:{type(message_segment)}")
continue
return md
@dataclass
class CustomPost(_CustomPost, AbstractPost):
"""基于 markdown 语法的,自由度较高的推送内容格式
简介:
支持处理text/image两种MessageSegmentFactory,
通过将text/image转换成对应的markdown语法以生成markdown文本
理论上text类型中可以直接使用markdown语法,例如`##第一章`。
但会导致不启用`override_use_pic`, 发送不会被渲染的纯文本消息
图片渲染最终由htmlrender执行
注意:
每一个MessageSegmentFactory元素都会被解释为单独的一行
可选参数:
`override_use_pic`:是否覆盖`bison_use_pic`全局配置
`compress`:将所有消息压缩为一条进行发送
`extra_msg`:需要附带发送的额外消息
成员函数:
`generate_text_messages()`:负责生成文本消息
`generate_pic_messages()`:负责生成图片消息
"""
pass

View File

@ -1,151 +1,84 @@
from io import BytesIO
from dataclasses import field, dataclass
from pathlib import Path
from typing import TYPE_CHECKING
from dataclasses import dataclass
from PIL import Image
from nonebot.log import logger
import nonebot_plugin_saa as saa
from nonebot_plugin_saa import MessageSegmentFactory
from ..utils import parse_text, http_client
from .abstract_post import BasePost, AbstractPost
from ..theme import theme_manager
from .abstract_post import AbstractPost
from ..plugin_config import plugin_config
from ..theme.types import ThemeRenderError, ThemeRenderUnsupportError
if TYPE_CHECKING:
from ..platform import Platform
@dataclass
class _Post(BasePost):
target_type: str
text: str
class Post(AbstractPost):
"""最通用的Post理论上包含所有常用的数据
对于更特殊的需要可以考虑另外实现一个Post
"""
platform: "Platform"
"""来源平台"""
content: str
"""文本内容"""
title: str | None = None
"""标题"""
images: list[str | bytes | Path | BytesIO] | None = None
"""图片列表"""
timestamp: int | None = None
"""发布/获取时间戳"""
url: str | None = None
target_name: str | None = None
pics: list[str | bytes] = field(default_factory=list)
"""来源链接"""
avatar: str | bytes | Path | BytesIO | None = None
"""发布者头像"""
nickname: str | None = None
"""发布者昵称"""
description: str | None = None
"""发布者个性签名等"""
repost: "Post | None" = None
"""转发的Post"""
_message: list[MessageSegmentFactory] | None = None
_pic_message: list[MessageSegmentFactory] | None = None
def get_config_theme(self) -> str | None:
"""获取用户指定的theme"""
return plugin_config.bison_platform_theme.get(self.platform.platform_name)
async def _pic_url_to_image(self, data: 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)
def get_priority_themes(self) -> list[str]:
"""获取渲染所使用的theme名列表按照优先级排序"""
themes_by_priority: list[str] = []
# 最先使用用户指定的theme
if user_theme := self.get_config_theme():
themes_by_priority.append(user_theme)
# 然后使用平台默认的theme
if self.platform.default_theme not in themes_by_priority:
themes_by_priority.append(self.platform.default_theme)
# 最后使用最基础的theme
if "basic" not in themes_by_priority:
themes_by_priority.append("basic")
return themes_by_priority
async def generate(self) -> list[MessageSegmentFactory]:
"""生成消息"""
themes = self.get_priority_themes()
for theme_name in themes:
if theme := theme_manager[theme_name]:
try:
logger.debug(f"Try to render Post with theme {theme_name}")
return await theme.do_render(self)
except ThemeRenderUnsupportError as e:
logger.warning(
f"Theme {theme_name} does not support Post of {self.platform.__class__.__name__}: {e}"
)
continue
except ThemeRenderError as e:
logger.exception(f"Theme {theme_name} render error: {e}")
continue
else:
logger.error(f"Theme {theme_name} not found")
continue
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[MessageSegmentFactory]:
if self._message is None:
await self._pic_merge()
msg_segments: list[MessageSegmentFactory] = []
text = ""
if self.text:
text += "{}".format(self.text if len(self.text) < 500 else self.text[:500] + "...")
if text:
text += "\n"
text += f"来源: {self.target_type}"
if self.target_name:
text += f" {self.target_name}"
if self.url:
text += f" \n详情: {self.url}"
msg_segments.append(saa.Text(text))
for pic in self.pics:
msg_segments.append(saa.Image(pic))
self._message = msg_segments
return self._message
async def generate_pic_messages(self) -> list[MessageSegmentFactory]:
if self._pic_message is None:
await self._pic_merge()
msg_segments: list[MessageSegmentFactory] = []
text = ""
if self.text:
text += f"{self.text}"
text += "\n"
text += f"来源: {self.target_type}"
if self.target_name:
text += f" {self.target_name}"
msg_segments.append(await parse_text(text))
if not self.target_type == "rss" and self.url:
msg_segments.append(saa.Text(self.url))
for pic in self.pics:
msg_segments.append(saa.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("b64img" if isinstance(x, bytes) or x.startswith("base64") else x for x in self.pics),
)
@dataclass
class Post(AbstractPost, _Post):
pass
raise ThemeRenderError(f"No theme can render Post of {self.platform.__class__.__name__}")

View File

@ -1,112 +0,0 @@
@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;
}

View File

@ -0,0 +1,22 @@
from pathlib import Path
from pkgutil import iter_modules
from importlib import import_module
from .types import Theme
from .registry import theme_manager
from .types import ThemeRegistrationError
from .types import ThemeRenderError as ThemeRenderError
from .types import ThemeRenderUnsupportError as ThemeRenderUnsupportError
_theme_dir = str((Path(__file__).parent / "themes").resolve())
for _, theme, _ in iter_modules([_theme_dir]):
theme_module = import_module(f"{__name__}.themes.{theme}")
if not hasattr(theme_module, "__theme_meta__"):
raise ThemeRegistrationError(f"{theme} has no __theme_meta__")
if not isinstance(theme_module.__theme_meta__, Theme):
raise ThemeRegistrationError(f"{theme}'s __theme_meta__ is not a AbstractTheme instance")
theme_manager.register(theme_module.__theme_meta__)

View File

@ -0,0 +1,36 @@
from nonebot import logger
from ..plugin_config import plugin_config
from .types import Theme, ThemeRegistrationError
class ThemeManager:
__themes: dict[str, Theme] = {}
def register(self, theme: Theme):
logger.trace(f"Registering theme: {theme}")
if theme.name in self.__themes:
raise ThemeRegistrationError(f"Theme {theme.name} duplicated registration")
if theme.need_browser and not plugin_config.bison_theme_use_browser:
logger.opt(colors=True).warning(f"Theme <b><u>{theme.name}</u></b> requires browser, but not allowed")
self.__themes[theme.name] = theme
logger.opt(colors=True).success(f"Theme <b><u>{theme.name}</u></b> registered")
def unregister(self, theme_name: str):
logger.trace(f"Unregistering theme: {theme_name}")
if theme_name not in self.__themes:
raise ThemeRegistrationError(f"Theme {theme_name} was not registered")
self.__themes.pop(theme_name)
logger.opt(colors=True).success(f"Theme <b><u>{theme_name}</u></b> unregistered")
def __getitem__(self, theme: str):
return self.__themes[theme]
def __len__(self):
return len(self.__themes)
def __contains__(self, theme: str):
return theme in self.__themes
theme_manager = ThemeManager()

View File

@ -0,0 +1,3 @@
from .build import ArknightsTheme
__theme_meta__ = ArknightsTheme()

View File

@ -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)]

View File

@ -15,15 +15,15 @@
<div class="main">
<div class="container">
<div class="standerd-container">
{% if bannerImageUrl %}
{% if data.banner_image_url %}
<div class="banner-image-container">
<img class="banner-image" src="{{ bannerImageUrl }}" />
<img class="banner-image" src="{{ data.banner_image_url }}" />
</div>
{% endif %}
<div class="head-title-container">
<span class="head-title">{{ announce_title }}</span>
<span class="head-title">{{ data.announce_title }}</span>
</div>
<div class="content">{{ content }}</div>
<div class="content">{{ data.content }}</div>
</div>
</div>
</div>

View File

@ -0,0 +1,3 @@
from .build import BasicTheme
__theme_meta__ = BasicTheme()

View File

@ -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

View File

@ -0,0 +1,3 @@
from .build import BriefTheme
__theme_meta__ = BriefTheme()

View File

@ -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

View File

@ -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) 授权许可使用。

View File

@ -0,0 +1,3 @@
from .build import CeobeCanteenTheme
__theme_meta__ = CeobeCanteenTheme()

View File

@ -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

View File

@ -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&amp;小刻食堂联动</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

View File

@ -0,0 +1,3 @@
from .build import Ht2iTheme
__theme_meta__ = Ht2iTheme()

View File

@ -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

View File

@ -0,0 +1,78 @@
from typing import TYPE_CHECKING
from abc import ABC, abstractmethod
from nonebot import logger, require
from pydantic import BaseModel, PrivateAttr
from nonebot_plugin_saa import MessageSegmentFactory
from ..plugin_config import plugin_config
if TYPE_CHECKING:
from ..post.abstract_post import AbstractPost
class Theme(ABC, BaseModel):
"""theme基类"""
name: str
"""theme名称"""
need_browser: bool = False
"""是否需要使用浏览器"""
_browser_checked: bool = PrivateAttr(default=False)
async def is_support_render(self, post: "AbstractPost") -> bool:
"""是否支持渲染该类型的Post"""
if self.need_browser and not plugin_config.bison_theme_use_browser:
logger.warning(f"Theme {self.name} need browser, but `bison_theme_use_browser` is False")
return False
return True
async def prepare(self):
if self.need_browser:
self.check_htmlrender_plugin_enable()
async def do_render(self, post: "AbstractPost") -> list[MessageSegmentFactory]:
"""真正调用的渲染函数,会对渲染过程进行一些处理"""
if not await self.is_support_render(post):
raise ThemeRenderUnsupportError(f"Theme [{self.name}] does not support render {post} by support check")
await self.prepare()
return await self.render(post)
def check_htmlrender_plugin_enable(self):
"""根据`need_browser`检测渲染插件"""
if self._browser_checked:
return
try:
require("nonebot_plugin_htmlrender")
self._browser_checked = True
except RuntimeError as e:
if "Cannot load plugin" in str(e):
raise ThemeRenderUnsupportError("需要安装`nonebot_plugin_htmlrender`插件")
else:
raise e
@abstractmethod
async def render(self, post: "AbstractPost") -> list[MessageSegmentFactory]:
"""对多种Post的实例可以考虑使用@overload"""
...
class ThemeRegistrationError(Exception):
"""Theme注册错误"""
pass
class ThemeRenderUnsupportError(Exception):
"""Theme不支持渲染该类型的Post"""
pass
class ThemeRenderError(Exception):
"""Theme渲染错误"""
pass

View File

@ -0,0 +1,22 @@
from qrcode import constants
from qrcode.main import QRCode
from qrcode.image.svg import SvgFragmentImage
def convert_to_qr(data: str) -> str:
"""Convert data to QR code
Args:
data (str): data to be converted
Returns:
bytes: QR code image
"""
qr = QRCode(
version=1,
error_correction=constants.ERROR_CORRECT_L,
box_size=10,
border=2,
image_factory=SvgFragmentImage,
)
qr.add_data(data)
qr.make(fit=True)
return qr.make_image().to_string().decode("utf-8")

View File

@ -12,6 +12,7 @@ from .http import http_client
from .context import ProcessContext
from ..plugin_config import plugin_config
from .scheduler_config import SchedulerConfig, scheduler
from .image import pic_merge, text_to_image, is_pics_mergable, pic_url_to_image
__all__ = [
"http_client",
@ -21,6 +22,10 @@ __all__ = [
"html_to_text",
"SchedulerConfig",
"scheduler",
"pic_merge",
"pic_url_to_image",
"is_pics_mergable",
"text_to_image",
]

View File

@ -0,0 +1,110 @@
from io import BytesIO
from typing import TypeGuard
from functools import partial
from PIL import Image
from httpx import AsyncClient
from nonebot import logger, require
from PIL.Image import Image as PILImage
from nonebot_plugin_saa import Text as SaaText
from nonebot_plugin_saa import Image as SaaImage
from ..plugin_config import plugin_config
async def pic_url_to_image(data: str | bytes, http_client: AsyncClient) -> PILImage:
pic_buffer = BytesIO()
if isinstance(data, str):
res = await http_client.get(data)
pic_buffer.write(res.content)
else:
pic_buffer.write(data)
return Image.open(pic_buffer)
def _check_image_square(size: tuple[int, int]) -> bool:
return abs(size[0] - size[1]) / size[0] < 0.05
async def pic_merge(pics: list[str | bytes], http_client: AsyncClient) -> list[str | bytes]:
if len(pics) < 3:
return pics
_pic_url_to_image = partial(pic_url_to_image, http_client=http_client)
first_image = await _pic_url_to_image(pics[0])
if not _check_image_square(first_image.size):
return pics
images: list[PILImage] = [first_image]
# first row
for i in range(1, 3):
cur_img = await _pic_url_to_image(pics[i])
if not _check_image_square(cur_img.size):
return pics
if cur_img.size[1] != images[0].size[1]: # height not equal
return pics
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(pics) < (row + 1) * 3:
return False
row_first_img = await _pic_url_to_image(pics[row * 3])
if not _check_image_square(row_first_img.size):
return False
if row_first_img.size[0] != images[0].size[0]:
return False
image_row: list[PILImage] = [row_first_img]
for i in range(row * 3 + 1, row * 3 + 3):
cur_img = await _pic_url_to_image(pics[i])
if not _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")
pics = pics[matrix[0] * matrix[1] :]
pics.insert(0, target_io.getvalue())
return pics
def is_pics_mergable(imgs: list) -> TypeGuard[list[str | bytes]]:
return all(isinstance(img, str | bytes) for img in imgs)
async def text_to_image(saa_text: SaaText) -> SaaImage:
"""使用 htmlrender 将 saa.Text 渲染为 saa.Image"""
if not plugin_config.bison_use_pic:
raise ValueError("请启用 bison_use_pic")
require("nonebot_plugin_htmlrender")
from nonebot_plugin_htmlrender import text_to_pic
render_data = await text_to_pic(str(saa_text))
return SaaImage(render_data)

229
poetry.lock generated
View File

@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
[[package]]
name = "aiodns"
@ -457,33 +457,33 @@ reference = "offical-source"
[[package]]
name = "black"
version = "24.1.1"
version = "24.2.0"
description = "The uncompromising code formatter."
optional = false
python-versions = ">=3.8"
files = [
{file = "black-24.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2588021038bd5ada078de606f2a804cadd0a3cc6a79cb3e9bb3a8bf581325a4c"},
{file = "black-24.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a95915c98d6e32ca43809d46d932e2abc5f1f7d582ffbe65a5b4d1588af7445"},
{file = "black-24.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fa6a0e965779c8f2afb286f9ef798df770ba2b6cee063c650b96adec22c056a"},
{file = "black-24.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:5242ecd9e990aeb995b6d03dc3b2d112d4a78f2083e5a8e86d566340ae80fec4"},
{file = "black-24.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fc1ec9aa6f4d98d022101e015261c056ddebe3da6a8ccfc2c792cbe0349d48b7"},
{file = "black-24.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0269dfdea12442022e88043d2910429bed717b2d04523867a85dacce535916b8"},
{file = "black-24.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3d64db762eae4a5ce04b6e3dd745dcca0fb9560eb931a5be97472e38652a161"},
{file = "black-24.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:5d7b06ea8816cbd4becfe5f70accae953c53c0e53aa98730ceccb0395520ee5d"},
{file = "black-24.1.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e2c8dfa14677f90d976f68e0c923947ae68fa3961d61ee30976c388adc0b02c8"},
{file = "black-24.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a21725862d0e855ae05da1dd25e3825ed712eaaccef6b03017fe0853a01aa45e"},
{file = "black-24.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07204d078e25327aad9ed2c64790d681238686bce254c910de640c7cc4fc3aa6"},
{file = "black-24.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:a83fe522d9698d8f9a101b860b1ee154c1d25f8a82ceb807d319f085b2627c5b"},
{file = "black-24.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:08b34e85170d368c37ca7bf81cf67ac863c9d1963b2c1780c39102187ec8dd62"},
{file = "black-24.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7258c27115c1e3b5de9ac6c4f9957e3ee2c02c0b39222a24dc7aa03ba0e986f5"},
{file = "black-24.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40657e1b78212d582a0edecafef133cf1dd02e6677f539b669db4746150d38f6"},
{file = "black-24.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e298d588744efda02379521a19639ebcd314fba7a49be22136204d7ed1782717"},
{file = "black-24.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:34afe9da5056aa123b8bfda1664bfe6fb4e9c6f311d8e4a6eb089da9a9173bf9"},
{file = "black-24.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:854c06fb86fd854140f37fb24dbf10621f5dab9e3b0c29a690ba595e3d543024"},
{file = "black-24.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3897ae5a21ca132efa219c029cce5e6bfc9c3d34ed7e892113d199c0b1b444a2"},
{file = "black-24.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:ecba2a15dfb2d97105be74bbfe5128bc5e9fa8477d8c46766505c1dda5883aac"},
{file = "black-24.1.1-py3-none-any.whl", hash = "sha256:5cdc2e2195212208fbcae579b931407c1fa9997584f0a415421748aeafff1168"},
{file = "black-24.1.1.tar.gz", hash = "sha256:48b5760dcbfe5cf97fd4fba23946681f3a81514c6ab8a45b50da67ac8fbc6c7b"},
{file = "black-24.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6981eae48b3b33399c8757036c7f5d48a535b962a7c2310d19361edeef64ce29"},
{file = "black-24.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d533d5e3259720fdbc1b37444491b024003e012c5173f7d06825a77508085430"},
{file = "black-24.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61a0391772490ddfb8a693c067df1ef5227257e72b0e4108482b8d41b5aee13f"},
{file = "black-24.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:992e451b04667116680cb88f63449267c13e1ad134f30087dec8527242e9862a"},
{file = "black-24.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:163baf4ef40e6897a2a9b83890e59141cc8c2a98f2dda5080dc15c00ee1e62cd"},
{file = "black-24.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e37c99f89929af50ffaf912454b3e3b47fd64109659026b678c091a4cd450fb2"},
{file = "black-24.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9de21bafcba9683853f6c96c2d515e364aee631b178eaa5145fc1c61a3cc92"},
{file = "black-24.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:9db528bccb9e8e20c08e716b3b09c6bdd64da0dd129b11e160bf082d4642ac23"},
{file = "black-24.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d84f29eb3ee44859052073b7636533ec995bd0f64e2fb43aeceefc70090e752b"},
{file = "black-24.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e08fb9a15c914b81dd734ddd7fb10513016e5ce7e6704bdd5e1251ceee51ac9"},
{file = "black-24.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:810d445ae6069ce64030c78ff6127cd9cd178a9ac3361435708b907d8a04c693"},
{file = "black-24.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ba15742a13de85e9b8f3239c8f807723991fbfae24bad92d34a2b12e81904982"},
{file = "black-24.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7e53a8c630f71db01b28cd9602a1ada68c937cbf2c333e6ed041390d6968faf4"},
{file = "black-24.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:93601c2deb321b4bad8f95df408e3fb3943d85012dddb6121336b8e24a0d1218"},
{file = "black-24.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0057f800de6acc4407fe75bb147b0c2b5cbb7c3ed110d3e5999cd01184d53b0"},
{file = "black-24.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:faf2ee02e6612577ba0181f4347bcbcf591eb122f7841ae5ba233d12c39dcb4d"},
{file = "black-24.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:057c3dc602eaa6fdc451069bd027a1b2635028b575a6c3acfd63193ced20d9c8"},
{file = "black-24.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08654d0797e65f2423f850fc8e16a0ce50925f9337fb4a4a176a7aa4026e63f8"},
{file = "black-24.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca610d29415ee1a30a3f30fab7a8f4144e9d34c89a235d81292a1edb2b55f540"},
{file = "black-24.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:4dd76e9468d5536abd40ffbc7a247f83b2324f0c050556d9c371c2b9a9a95e31"},
{file = "black-24.2.0-py3-none-any.whl", hash = "sha256:e8a6ae970537e67830776488bca52000eaa37fa63b9988e8c487458d9cd5ace6"},
{file = "black-24.2.0.tar.gz", hash = "sha256:bce4f25c27c3435e4dace4815bcb2008b87e167e3bf4ee47ccdc5ce906eb4894"},
]
[package.dependencies]
@ -2202,18 +2202,19 @@ reference = "offical-source"
[[package]]
name = "nonebot-adapter-onebot"
version = "2.3.1"
version = "2.4.0"
description = "OneBot(CQHTTP) adapter for nonebot2"
optional = false
python-versions = ">=3.8,<4.0"
files = [
{file = "nonebot_adapter_onebot-2.3.1-py3-none-any.whl", hash = "sha256:c4085f1fc1a62e46c737452b9ce3d6eb374812c78a419bb4fa378f48bd8e4088"},
{file = "nonebot_adapter_onebot-2.3.1.tar.gz", hash = "sha256:10cec3aee454700e6d2144748bd898772db7bd95247d51d3ccd3b31919e24689"},
{file = "nonebot_adapter_onebot-2.4.0-py3-none-any.whl", hash = "sha256:4a51da1913c8ab6008e8ef8a2877af7acc32eac82d6773ff443f743dda380e14"},
{file = "nonebot_adapter_onebot-2.4.0.tar.gz", hash = "sha256:27a29de8137ce60f0ea328c6fac63571ba7efef7619d0cc74c0dff42c3034b12"},
]
[package.dependencies]
msgpack = ">=1.0.3,<2.0.0"
nonebot2 = ">=2.1.0,<3.0.0"
nonebot2 = ">=2.2.0,<3.0.0"
pydantic = ">=1.10.0,<2.5.0 || >2.5.0,<2.5.1 || >2.5.1,<3.0.0"
typing-extensions = ">=4.0.0,<5.0.0"
[package.source]
@ -2362,17 +2363,18 @@ reference = "offical-source"
[[package]]
name = "nonebot-plugin-localstore"
version = "0.5.2"
version = "0.6.0"
description = "Local Storage Support for NoneBot2"
optional = false
python-versions = ">=3.8,<4.0"
files = [
{file = "nonebot_plugin_localstore-0.5.2-py3-none-any.whl", hash = "sha256:6cd3ee2120918e9fd2af572730aa0612f29d69da22c67bf7297bbdc86b489b57"},
{file = "nonebot_plugin_localstore-0.5.2.tar.gz", hash = "sha256:66ac53f954c7f9c4117c3bc472580c018bc83ec0c08bc12c4fd96ce875f8a1e3"},
{file = "nonebot_plugin_localstore-0.6.0-py3-none-any.whl", hash = "sha256:59f0126d85680601166a9a62cca886a33e1b0a8fef7cd67fff52747bd47f42d3"},
{file = "nonebot_plugin_localstore-0.6.0.tar.gz", hash = "sha256:7eb4039cb2e76c54b860b2b98f2b90cd25284919603e81dedec367f215662fcd"},
]
[package.dependencies]
nonebot2 = ">=2.0.0,<3.0.0"
nonebot2 = ">=2.2.0,<3.0.0"
pydantic = ">=1.10.0,<2.5.0 || >2.5.0,<2.5.1 || >2.5.1,<3.0.0"
typing-extensions = ">=4.0.0"
[package.source]
@ -3136,6 +3138,22 @@ type = "legacy"
url = "https://pypi.org/simple"
reference = "offical-source"
[[package]]
name = "pypng"
version = "0.20220715.0"
description = "Pure Python library for saving and loading PNG images"
optional = false
python-versions = "*"
files = [
{file = "pypng-0.20220715.0-py3-none-any.whl", hash = "sha256:4a43e969b8f5aaafb2a415536c1a8ec7e341cd6a3f957fd5b5f32a4cfeed902c"},
{file = "pypng-0.20220715.0.tar.gz", hash = "sha256:739c433ba96f078315de54c0db975aee537cbc3e1d0ae4ed9aab0ca1e427e2c1"},
]
[package.source]
type = "legacy"
url = "https://pypi.org/simple"
reference = "offical-source"
[[package]]
name = "pytest"
version = "7.4.4"
@ -3426,7 +3444,6 @@ files = [
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
@ -3466,6 +3483,34 @@ type = "legacy"
url = "https://pypi.org/simple"
reference = "offical-source"
[[package]]
name = "qrcode"
version = "7.4.2"
description = "QR Code image generator"
optional = false
python-versions = ">=3.7"
files = [
{file = "qrcode-7.4.2-py3-none-any.whl", hash = "sha256:581dca7a029bcb2deef5d01068e39093e80ef00b4a61098a2182eac59d01643a"},
{file = "qrcode-7.4.2.tar.gz", hash = "sha256:9dd969454827e127dbd93696b20747239e6d540e082937c90f14ac95b30f5845"},
]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
pypng = "*"
typing-extensions = "*"
[package.extras]
all = ["pillow (>=9.1.0)", "pytest", "pytest-cov", "tox", "zest.releaser[recommended]"]
dev = ["pytest", "pytest-cov", "tox"]
maintainer = ["zest.releaser[recommended]"]
pil = ["pillow (>=9.1.0)"]
test = ["coverage", "pytest"]
[package.source]
type = "legacy"
url = "https://pypi.org/simple"
reference = "offical-source"
[[package]]
name = "requests"
version = "2.31.0"
@ -3567,18 +3612,18 @@ reference = "offical-source"
[[package]]
name = "setuptools"
version = "69.0.3"
version = "69.1.0"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
optional = false
python-versions = ">=3.8"
files = [
{file = "setuptools-69.0.3-py3-none-any.whl", hash = "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05"},
{file = "setuptools-69.0.3.tar.gz", hash = "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78"},
{file = "setuptools-69.1.0-py3-none-any.whl", hash = "sha256:c054629b81b946d63a9c6e732bc8b2513a7c3ea645f11d0139a2191d735c60c6"},
{file = "setuptools-69.1.0.tar.gz", hash = "sha256:850894c4195f09c4ed30dba56213bf7c3f21d86ed6bdaafb5df5972593bfc401"},
]
[package.extras]
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
[package.source]
@ -3673,66 +3718,66 @@ reference = "offical-source"
[[package]]
name = "sqlalchemy"
version = "2.0.25"
version = "2.0.27"
description = "Database Abstraction Library"
optional = false
python-versions = ">=3.7"
files = [
{file = "SQLAlchemy-2.0.25-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4344d059265cc8b1b1be351bfb88749294b87a8b2bbe21dfbe066c4199541ebd"},
{file = "SQLAlchemy-2.0.25-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6f9e2e59cbcc6ba1488404aad43de005d05ca56e069477b33ff74e91b6319735"},
{file = "SQLAlchemy-2.0.25-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84daa0a2055df9ca0f148a64fdde12ac635e30edbca80e87df9b3aaf419e144a"},
{file = "SQLAlchemy-2.0.25-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc8b7dabe8e67c4832891a5d322cec6d44ef02f432b4588390017f5cec186a84"},
{file = "SQLAlchemy-2.0.25-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f5693145220517b5f42393e07a6898acdfe820e136c98663b971906120549da5"},
{file = "SQLAlchemy-2.0.25-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:db854730a25db7c956423bb9fb4bdd1216c839a689bf9cc15fada0a7fb2f4570"},
{file = "SQLAlchemy-2.0.25-cp310-cp310-win32.whl", hash = "sha256:14a6f68e8fc96e5e8f5647ef6cda6250c780612a573d99e4d881581432ef1669"},
{file = "SQLAlchemy-2.0.25-cp310-cp310-win_amd64.whl", hash = "sha256:87f6e732bccd7dcf1741c00f1ecf33797383128bd1c90144ac8adc02cbb98643"},
{file = "SQLAlchemy-2.0.25-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:342d365988ba88ada8af320d43df4e0b13a694dbd75951f537b2d5e4cb5cd002"},
{file = "SQLAlchemy-2.0.25-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f37c0caf14b9e9b9e8f6dbc81bc56db06acb4363eba5a633167781a48ef036ed"},
{file = "SQLAlchemy-2.0.25-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa9373708763ef46782d10e950b49d0235bfe58facebd76917d3f5cbf5971aed"},
{file = "SQLAlchemy-2.0.25-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d24f571990c05f6b36a396218f251f3e0dda916e0c687ef6fdca5072743208f5"},
{file = "SQLAlchemy-2.0.25-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75432b5b14dc2fff43c50435e248b45c7cdadef73388e5610852b95280ffd0e9"},
{file = "SQLAlchemy-2.0.25-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:884272dcd3ad97f47702965a0e902b540541890f468d24bd1d98bcfe41c3f018"},
{file = "SQLAlchemy-2.0.25-cp311-cp311-win32.whl", hash = "sha256:e607cdd99cbf9bb80391f54446b86e16eea6ad309361942bf88318bcd452363c"},
{file = "SQLAlchemy-2.0.25-cp311-cp311-win_amd64.whl", hash = "sha256:7d505815ac340568fd03f719446a589162d55c52f08abd77ba8964fbb7eb5b5f"},
{file = "SQLAlchemy-2.0.25-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0dacf67aee53b16f365c589ce72e766efaabd2b145f9de7c917777b575e3659d"},
{file = "SQLAlchemy-2.0.25-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b801154027107461ee992ff4b5c09aa7cc6ec91ddfe50d02bca344918c3265c6"},
{file = "SQLAlchemy-2.0.25-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59a21853f5daeb50412d459cfb13cb82c089ad4c04ec208cd14dddd99fc23b39"},
{file = "SQLAlchemy-2.0.25-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29049e2c299b5ace92cbed0c1610a7a236f3baf4c6b66eb9547c01179f638ec5"},
{file = "SQLAlchemy-2.0.25-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b64b183d610b424a160b0d4d880995e935208fc043d0302dd29fee32d1ee3f95"},
{file = "SQLAlchemy-2.0.25-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4f7a7d7fcc675d3d85fbf3b3828ecd5990b8d61bd6de3f1b260080b3beccf215"},
{file = "SQLAlchemy-2.0.25-cp312-cp312-win32.whl", hash = "sha256:cf18ff7fc9941b8fc23437cc3e68ed4ebeff3599eec6ef5eebf305f3d2e9a7c2"},
{file = "SQLAlchemy-2.0.25-cp312-cp312-win_amd64.whl", hash = "sha256:91f7d9d1c4dd1f4f6e092874c128c11165eafcf7c963128f79e28f8445de82d5"},
{file = "SQLAlchemy-2.0.25-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bb209a73b8307f8fe4fe46f6ad5979649be01607f11af1eb94aa9e8a3aaf77f0"},
{file = "SQLAlchemy-2.0.25-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:798f717ae7c806d67145f6ae94dc7c342d3222d3b9a311a784f371a4333212c7"},
{file = "SQLAlchemy-2.0.25-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fdd402169aa00df3142149940b3bf9ce7dde075928c1886d9a1df63d4b8de62"},
{file = "SQLAlchemy-2.0.25-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0d3cab3076af2e4aa5693f89622bef7fa770c6fec967143e4da7508b3dceb9b9"},
{file = "SQLAlchemy-2.0.25-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:74b080c897563f81062b74e44f5a72fa44c2b373741a9ade701d5f789a10ba23"},
{file = "SQLAlchemy-2.0.25-cp37-cp37m-win32.whl", hash = "sha256:87d91043ea0dc65ee583026cb18e1b458d8ec5fc0a93637126b5fc0bc3ea68c4"},
{file = "SQLAlchemy-2.0.25-cp37-cp37m-win_amd64.whl", hash = "sha256:75f99202324383d613ddd1f7455ac908dca9c2dd729ec8584c9541dd41822a2c"},
{file = "SQLAlchemy-2.0.25-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:420362338681eec03f53467804541a854617faed7272fe71a1bfdb07336a381e"},
{file = "SQLAlchemy-2.0.25-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c88f0c7dcc5f99bdb34b4fd9b69b93c89f893f454f40219fe923a3a2fd11625"},
{file = "SQLAlchemy-2.0.25-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3be4987e3ee9d9a380b66393b77a4cd6d742480c951a1c56a23c335caca4ce3"},
{file = "SQLAlchemy-2.0.25-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a159111a0f58fb034c93eeba211b4141137ec4b0a6e75789ab7a3ef3c7e7e3"},
{file = "SQLAlchemy-2.0.25-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8b8cb63d3ea63b29074dcd29da4dc6a97ad1349151f2d2949495418fd6e48db9"},
{file = "SQLAlchemy-2.0.25-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:736ea78cd06de6c21ecba7416499e7236a22374561493b456a1f7ffbe3f6cdb4"},
{file = "SQLAlchemy-2.0.25-cp38-cp38-win32.whl", hash = "sha256:10331f129982a19df4284ceac6fe87353ca3ca6b4ca77ff7d697209ae0a5915e"},
{file = "SQLAlchemy-2.0.25-cp38-cp38-win_amd64.whl", hash = "sha256:c55731c116806836a5d678a70c84cb13f2cedba920212ba7dcad53260997666d"},
{file = "SQLAlchemy-2.0.25-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:605b6b059f4b57b277f75ace81cc5bc6335efcbcc4ccb9066695e515dbdb3900"},
{file = "SQLAlchemy-2.0.25-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:665f0a3954635b5b777a55111ababf44b4fc12b1f3ba0a435b602b6387ffd7cf"},
{file = "SQLAlchemy-2.0.25-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecf6d4cda1f9f6cb0b45803a01ea7f034e2f1aed9475e883410812d9f9e3cfcf"},
{file = "SQLAlchemy-2.0.25-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c51db269513917394faec5e5c00d6f83829742ba62e2ac4fa5c98d58be91662f"},
{file = "SQLAlchemy-2.0.25-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:790f533fa5c8901a62b6fef5811d48980adeb2f51f1290ade8b5e7ba990ba3de"},
{file = "SQLAlchemy-2.0.25-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1b1180cda6df7af84fe72e4530f192231b1f29a7496951db4ff38dac1687202d"},
{file = "SQLAlchemy-2.0.25-cp39-cp39-win32.whl", hash = "sha256:555651adbb503ac7f4cb35834c5e4ae0819aab2cd24857a123370764dc7d7e24"},
{file = "SQLAlchemy-2.0.25-cp39-cp39-win_amd64.whl", hash = "sha256:dc55990143cbd853a5d038c05e79284baedf3e299661389654551bd02a6a68d7"},
{file = "SQLAlchemy-2.0.25-py3-none-any.whl", hash = "sha256:a86b4240e67d4753dc3092d9511886795b3c2852abe599cffe108952f7af7ac3"},
{file = "SQLAlchemy-2.0.25.tar.gz", hash = "sha256:a2c69a7664fb2d54b8682dd774c3b54f67f84fa123cf84dda2a5f40dcaa04e08"},
{file = "SQLAlchemy-2.0.27-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d04e579e911562f1055d26dab1868d3e0bb905db3bccf664ee8ad109f035618a"},
{file = "SQLAlchemy-2.0.27-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fa67d821c1fd268a5a87922ef4940442513b4e6c377553506b9db3b83beebbd8"},
{file = "SQLAlchemy-2.0.27-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c7a596d0be71b7baa037f4ac10d5e057d276f65a9a611c46970f012752ebf2d"},
{file = "SQLAlchemy-2.0.27-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:954d9735ee9c3fa74874c830d089a815b7b48df6f6b6e357a74130e478dbd951"},
{file = "SQLAlchemy-2.0.27-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5cd20f58c29bbf2680039ff9f569fa6d21453fbd2fa84dbdb4092f006424c2e6"},
{file = "SQLAlchemy-2.0.27-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:03f448ffb731b48323bda68bcc93152f751436ad6037f18a42b7e16af9e91c07"},
{file = "SQLAlchemy-2.0.27-cp310-cp310-win32.whl", hash = "sha256:d997c5938a08b5e172c30583ba6b8aad657ed9901fc24caf3a7152eeccb2f1b4"},
{file = "SQLAlchemy-2.0.27-cp310-cp310-win_amd64.whl", hash = "sha256:eb15ef40b833f5b2f19eeae65d65e191f039e71790dd565c2af2a3783f72262f"},
{file = "SQLAlchemy-2.0.27-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6c5bad7c60a392850d2f0fee8f355953abaec878c483dd7c3836e0089f046bf6"},
{file = "SQLAlchemy-2.0.27-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3012ab65ea42de1be81fff5fb28d6db893ef978950afc8130ba707179b4284a"},
{file = "SQLAlchemy-2.0.27-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbcd77c4d94b23e0753c5ed8deba8c69f331d4fd83f68bfc9db58bc8983f49cd"},
{file = "SQLAlchemy-2.0.27-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d177b7e82f6dd5e1aebd24d9c3297c70ce09cd1d5d37b43e53f39514379c029c"},
{file = "SQLAlchemy-2.0.27-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:680b9a36029b30cf063698755d277885d4a0eab70a2c7c6e71aab601323cba45"},
{file = "SQLAlchemy-2.0.27-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1306102f6d9e625cebaca3d4c9c8f10588735ef877f0360b5cdb4fdfd3fd7131"},
{file = "SQLAlchemy-2.0.27-cp311-cp311-win32.whl", hash = "sha256:5b78aa9f4f68212248aaf8943d84c0ff0f74efc65a661c2fc68b82d498311fd5"},
{file = "SQLAlchemy-2.0.27-cp311-cp311-win_amd64.whl", hash = "sha256:15e19a84b84528f52a68143439d0c7a3a69befcd4f50b8ef9b7b69d2628ae7c4"},
{file = "SQLAlchemy-2.0.27-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0de1263aac858f288a80b2071990f02082c51d88335a1db0d589237a3435fe71"},
{file = "SQLAlchemy-2.0.27-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce850db091bf7d2a1f2fdb615220b968aeff3849007b1204bf6e3e50a57b3d32"},
{file = "SQLAlchemy-2.0.27-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8dfc936870507da96aebb43e664ae3a71a7b96278382bcfe84d277b88e379b18"},
{file = "SQLAlchemy-2.0.27-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4fbe6a766301f2e8a4519f4500fe74ef0a8509a59e07a4085458f26228cd7cc"},
{file = "SQLAlchemy-2.0.27-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4535c49d961fe9a77392e3a630a626af5baa967172d42732b7a43496c8b28876"},
{file = "SQLAlchemy-2.0.27-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0fb3bffc0ced37e5aa4ac2416f56d6d858f46d4da70c09bb731a246e70bff4d5"},
{file = "SQLAlchemy-2.0.27-cp312-cp312-win32.whl", hash = "sha256:7f470327d06400a0aa7926b375b8e8c3c31d335e0884f509fe272b3c700a7254"},
{file = "SQLAlchemy-2.0.27-cp312-cp312-win_amd64.whl", hash = "sha256:f9374e270e2553653d710ece397df67db9d19c60d2647bcd35bfc616f1622dcd"},
{file = "SQLAlchemy-2.0.27-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e97cf143d74a7a5a0f143aa34039b4fecf11343eed66538610debc438685db4a"},
{file = "SQLAlchemy-2.0.27-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7b5a3e2120982b8b6bd1d5d99e3025339f7fb8b8267551c679afb39e9c7c7f1"},
{file = "SQLAlchemy-2.0.27-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e36aa62b765cf9f43a003233a8c2d7ffdeb55bc62eaa0a0380475b228663a38f"},
{file = "SQLAlchemy-2.0.27-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5ada0438f5b74c3952d916c199367c29ee4d6858edff18eab783b3978d0db16d"},
{file = "SQLAlchemy-2.0.27-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b1d9d1bfd96eef3c3faedb73f486c89e44e64e40e5bfec304ee163de01cf996f"},
{file = "SQLAlchemy-2.0.27-cp37-cp37m-win32.whl", hash = "sha256:ca891af9f3289d24a490a5fde664ea04fe2f4984cd97e26de7442a4251bd4b7c"},
{file = "SQLAlchemy-2.0.27-cp37-cp37m-win_amd64.whl", hash = "sha256:fd8aafda7cdff03b905d4426b714601c0978725a19efc39f5f207b86d188ba01"},
{file = "SQLAlchemy-2.0.27-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ec1f5a328464daf7a1e4e385e4f5652dd9b1d12405075ccba1df842f7774b4fc"},
{file = "SQLAlchemy-2.0.27-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ad862295ad3f644e3c2c0d8b10a988e1600d3123ecb48702d2c0f26771f1c396"},
{file = "SQLAlchemy-2.0.27-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48217be1de7d29a5600b5c513f3f7664b21d32e596d69582be0a94e36b8309cb"},
{file = "SQLAlchemy-2.0.27-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e56afce6431450442f3ab5973156289bd5ec33dd618941283847c9fd5ff06bf"},
{file = "SQLAlchemy-2.0.27-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:611068511b5531304137bcd7fe8117c985d1b828eb86043bd944cebb7fae3910"},
{file = "SQLAlchemy-2.0.27-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b86abba762ecfeea359112b2bb4490802b340850bbee1948f785141a5e020de8"},
{file = "SQLAlchemy-2.0.27-cp38-cp38-win32.whl", hash = "sha256:30d81cc1192dc693d49d5671cd40cdec596b885b0ce3b72f323888ab1c3863d5"},
{file = "SQLAlchemy-2.0.27-cp38-cp38-win_amd64.whl", hash = "sha256:120af1e49d614d2525ac247f6123841589b029c318b9afbfc9e2b70e22e1827d"},
{file = "SQLAlchemy-2.0.27-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d07ee7793f2aeb9b80ec8ceb96bc8cc08a2aec8a1b152da1955d64e4825fcbac"},
{file = "SQLAlchemy-2.0.27-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cb0845e934647232b6ff5150df37ceffd0b67b754b9fdbb095233deebcddbd4a"},
{file = "SQLAlchemy-2.0.27-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fc19ae2e07a067663dd24fca55f8ed06a288384f0e6e3910420bf4b1270cc51"},
{file = "SQLAlchemy-2.0.27-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b90053be91973a6fb6020a6e44382c97739736a5a9d74e08cc29b196639eb979"},
{file = "SQLAlchemy-2.0.27-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2f5c9dfb0b9ab5e3a8a00249534bdd838d943ec4cfb9abe176a6c33408430230"},
{file = "SQLAlchemy-2.0.27-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:33e8bde8fff203de50399b9039c4e14e42d4d227759155c21f8da4a47fc8053c"},
{file = "SQLAlchemy-2.0.27-cp39-cp39-win32.whl", hash = "sha256:d873c21b356bfaf1589b89090a4011e6532582b3a8ea568a00e0c3aab09399dd"},
{file = "SQLAlchemy-2.0.27-cp39-cp39-win_amd64.whl", hash = "sha256:ff2f1b7c963961d41403b650842dc2039175b906ab2093635d8319bef0b7d620"},
{file = "SQLAlchemy-2.0.27-py3-none-any.whl", hash = "sha256:1ab4e0448018d01b142c916cc7119ca573803a4745cfe341b8f95657812700ac"},
{file = "SQLAlchemy-2.0.27.tar.gz", hash = "sha256:86a6ed69a71fe6b88bf9331594fa390a2adda4a49b5c06f98e47bf0d392534f8"},
]
[package.dependencies]
aiosqlite = {version = "*", optional = true, markers = "extra == \"aiosqlite\""}
greenlet = {version = "!=0.4.17", optional = true, markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\" or extra == \"aiosqlite\""}
typing-extensions = {version = ">=4.6.0", optional = true, markers = "extra == \"aiosqlite\""}
typing-extensions = ">=4.6.0"
[package.extras]
aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"]
@ -3949,13 +3994,13 @@ reference = "offical-source"
[[package]]
name = "tzdata"
version = "2023.4"
version = "2024.1"
description = "Provider of IANA time zone data"
optional = false
python-versions = ">=2"
files = [
{file = "tzdata-2023.4-py2.py3-none-any.whl", hash = "sha256:aa3ace4329eeacda5b7beb7ea08ece826c28d761cda36e747cfbf97996d39bf3"},
{file = "tzdata-2023.4.tar.gz", hash = "sha256:dd54c94f294765522c77399649b4fefd95522479a664a0cec87f41bebc6148c9"},
{file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"},
{file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"},
]
[package.source]
@ -4460,4 +4505,4 @@ yaml = []
[metadata]
lock-version = "2.0"
python-versions = ">=3.10,<4.0.0"
content-hash = "29341c2d9cdb57d908f22fafd4296e31555d967bf894acaf6517ef4ad1166128"
content-hash = "44bdefe131938db105fdb9fa223ccdd964aab8878778d4b0b5db555852c566d7"

View File

@ -36,6 +36,7 @@ pillow = ">=8.1,<11.0"
pyjwt = "^2.1.0"
python-socketio = "^5.4.0"
tinydb = "^4.3.0"
qrcode = "^7.4.2"
[tool.poetry.group.dev.dependencies]
black = ">=23.7,<25.0"

View File

@ -41,6 +41,7 @@ async def app(tmp_path: Path, request: pytest.FixtureRequest, mocker: MockerFixt
plugin_config.bison_config_path = str(tmp_path / "legacy_config")
plugin_config.bison_filter_log = False
plugin_config.bison_theme_use_browser = True
datastore_config.datastore_config_dir = tmp_path / "config"
datastore_config.datastore_cache_dir = tmp_path / "cache"

View File

@ -1,3 +1,5 @@
from time import time
import respx
import pytest
from nonebug.app import App
@ -49,6 +51,7 @@ async def test_fetch_new(
monster_siren_list_0,
monster_siren_list_1,
):
from nonebot_bison.post import Post
from nonebot_bison.types import Target, SubUnit
ak_list_router = respx.get("https://ak-webview.hypergryph.com/api/game/bulletinList?target=IOS")
@ -60,37 +63,50 @@ async def test_fetch_new(
monster_siren_router = respx.get("https://monster-siren.hypergryph.com/api/news")
terra_list = respx.get("https://terra-historicus.hypergryph.com/api/recentUpdate")
ak_list_router.mock(return_value=Response(200, json=arknights_list__1))
detail_router.mock(return_value=Response(200, text=get_file("arknights-detail-807")))
mock_detail = get_json("arknights-detail-807")
mock_detail["data"]["bannerImageUrl"] = "https://example.com/1.jpg"
detail_router.mock(return_value=Response(200, json=mock_detail))
version_router.mock(return_value=Response(200, json=get_json("arknights-version-0.json")))
preannouncement_router.mock(return_value=Response(200, json=get_json("arknights-pre-0.json")))
monster_siren_router.mock(return_value=Response(200, json=monster_siren_list_0))
terra_list.mock(return_value=Response(200, json=get_json("terra-hist-0.json")))
target = Target("")
res = await arknights.fetch_new_post(SubUnit(target, [dummy_user_subinfo]))
res1 = await arknights.fetch_new_post(SubUnit(target, [dummy_user_subinfo]))
assert ak_list_router.called
assert len(res) == 0
assert len(res1) == 0
assert not detail_router.called
mock_data = arknights_list_0
mock_data["data"]["list"][0]["updatedAt"] = int(time())
ak_list_router.mock(return_value=Response(200, json=mock_data))
res3 = await arknights.fetch_new_post(SubUnit(target, [dummy_user_subinfo]))
assert len(res3[0][1]) == 1
res2 = await arknights.fetch_new_post(SubUnit(target, [dummy_user_subinfo]))
assert len(res2[0][1]) == 1
assert detail_router.called
post = res3[0][1][0]
assert post.target_type == "arknights"
assert post.text == ""
assert post.url == ""
assert post.target_name == "明日方舟游戏内公告"
assert len(post.pics) == 1
post2: Post = res2[0][1][0]
assert post2.platform.platform_name == "arknights"
assert post2.content
assert post2.title == "2023「夏日嘉年华」限时活动即将开启"
assert not post2.url
assert post2.nickname == "明日方舟游戏内公告"
assert post2.images
assert post2.images == ["https://example.com/1.jpg"]
assert post2.timestamp
assert "arknights" == post2.get_priority_themes()[0]
# assert(post.pics == ['https://ak-fs.hypergryph.com/announce/images/20210623/e6f49aeb9547a2278678368a43b95b07.jpg'])
await post.generate_messages()
terra_list.mock(return_value=Response(200, json=get_json("terra-hist-1.json")))
res = await arknights.fetch_new_post(SubUnit(target, [dummy_user_subinfo]))
assert len(res) == 1
post = res[0][1][0]
assert post.target_type == "terra-historicus"
assert post.text == "123罗德岛 - 「掠风」篇"
assert post.url == "https://terra-historicus.hypergryph.com/comic/6253/episode/4938"
assert post.pics == ["https://web.hycdn.cn/comic/pic/20220507/ab8a2ff408ec7d587775aed70b178ec0.png"]
res3 = await arknights.fetch_new_post(SubUnit(target, [dummy_user_subinfo]))
assert len(res3) == 1
post3: Post = res3[0][1][0]
assert post3.platform.platform_name == "arknights"
assert post3.nickname == "泰拉记事社漫画"
assert post3.title == "123罗德岛 - 「掠风」篇"
assert post3.content == "你可能不知道的罗德岛小剧场!"
assert post3.url == "https://terra-historicus.hypergryph.com/comic/6253/episode/4938"
assert post3.images == ["https://web.hycdn.cn/comic/pic/20220507/ab8a2ff408ec7d587775aed70b178ec0.png"]
assert "brief" == post3.get_priority_themes()[0]
@pytest.mark.render()
@ -103,6 +119,7 @@ async def test_send_with_render(
monster_siren_list_0,
monster_siren_list_1,
):
from nonebot_bison.post import Post
from nonebot_bison.types import Target, SubUnit
ak_list_router = respx.get("https://ak-webview.hypergryph.com/api/game/bulletinList?target=IOS")
@ -119,22 +136,26 @@ async def test_send_with_render(
preannouncement_router.mock(return_value=Response(200, json=get_json("arknights-pre-0.json")))
monster_siren_router.mock(return_value=Response(200, json=monster_siren_list_0))
terra_list.mock(return_value=Response(200, json=get_json("terra-hist-0.json")))
target = Target("")
res = await arknights.fetch_new_post(SubUnit(target, [dummy_user_subinfo]))
res1 = await arknights.fetch_new_post(SubUnit(target, [dummy_user_subinfo]))
assert ak_list_router.called
assert len(res) == 0
assert len(res1) == 0
assert not detail_router.called
mock_data = arknights_list_1
mock_data["data"]["list"][0]["updatedAt"] = int(time())
ak_list_router.mock(return_value=Response(200, json=mock_data))
res3 = await arknights.fetch_new_post(SubUnit(target, [dummy_user_subinfo]))
assert len(res3[0][1]) == 1
res2 = await arknights.fetch_new_post(SubUnit(target, [dummy_user_subinfo]))
assert len(res2[0][1]) == 1
assert detail_router.called
post = res3[0][1][0]
assert post.target_type == "arknights"
assert post.text == ""
assert post.url == ""
assert post.target_name == "明日方舟游戏内公告"
assert len(post.pics) == 1
post2: Post = res2[0][1][0]
assert post2.platform.platform_name == "arknights"
assert "《明日方舟》将于08月01日10:00 ~16:00的更新维护中对游戏内【公开招募】进行新增干员。" in post2.content
assert post2.title == "【公开招募】标签强制刷新通知"
assert post2.nickname == "明日方舟游戏内公告"
assert not post2.images
# assert(post.pics == ['https://ak-fs.hypergryph.com/announce/images/20210623/e6f49aeb9547a2278678368a43b95b07.jpg'])
r = await post.generate_messages()
r = await post2.generate_messages()
assert r

View File

@ -72,14 +72,17 @@ async def test_get_tag_without_topic_info(bilibili, bing_dy_list):
@pytest.mark.asyncio
async def test_video_forward(bilibili, bing_dy_list):
post = await bilibili.parse(bing_dy_list[1])
from nonebot_bison.post import Post
post: Post = await bilibili.parse(bing_dy_list[1])
assert (
post.text
post.content
== "答案揭晓:宿舍!来看看投票结果\nhttps://t.bilibili.com/568093580488553786\n--------------\n#可露希尔的秘密档案#"
" \n11来宿舍休息一下吧 \n档案来源lambda:\\罗德岛内务\\秘密档案 \n发布时间9/12 1:00 P.M."
" \n档案类型:可见 \n档案描述:今天请了病假在宿舍休息。很舒适。"
" \n提供者:赫默\n=================\n《可露希尔的秘密档案》11话来宿舍休息一下吧"
)
assert post.get_priority_themes()[0] == "basic"
@pytest.mark.asyncio
@ -87,7 +90,7 @@ async def test_video_forward_without_dynamic(bilibili, bing_dy_list):
# 视频简介和动态文本其中一方为空的情况
post = await bilibili.parse(bing_dy_list[2])
assert (
post.text
post.content
== "阿消的罗德岛闲谈直播#01:《女人最喜欢的女人,就是在战场上熠熠生辉的女人》"
+ "\n\n"
+ "本系列视频为饼组成员的有趣直播录播,主要内容为方舟相关,未来可能系列其他视频会包含部分饼组团建日常等。"
@ -96,13 +99,14 @@ async def test_video_forward_without_dynamic(bilibili, bing_dy_list):
"包含慕夏对新PV的个人解读风笛厨力疯狂放出CP言论输出9.16轮换池预测视频分析和理智规划杂谈内容。"
"\n注意:内含大量个人性质对风笛的厨力观点与多CP混乱发言不适者请及时点击退出或跳到下一片段。"
)
assert post.get_priority_themes()[0] == "basic"
@pytest.mark.asyncio
async def test_article_forward(bilibili, bing_dy_list):
post = await bilibili.parse(bing_dy_list[4])
assert (
post.text
post.content
== "#明日方舟##饼学大厦#\n9.11专栏更新完毕,"
"这还塌了实属没跟新运营对上\n后边除了周日发饼和PV没提及的中文语音"
"稳了\n别忘了来参加#可露希尔的秘密档案#的主题投票\nhttps://t.bilibili.com/568093580488553786?tab=2"
@ -121,7 +125,7 @@ async def test_article_forward(bilibili, bing_dy_list):
async def test_dynamic_forward(bilibili, bing_dy_list):
post = await bilibili.parse(bing_dy_list[5])
assert (
post.text
post.content
== "饼组主线饼学预测——9.11版\n①今日结果\n9.11 殿堂上的游禽-星极(x"
"新运营实锤了)\n②后续预测\n9.12 #罗德岛相簿#+#可露希尔的秘密档案#11话\n9.13"
" 六星先锋(执旗手)干员-琴柳\n9.14 宣传策略-空弦+家具\n9.15 轮换池(+中文语音前瞻)\n9.16"
@ -161,7 +165,9 @@ async def test_fetch_new(bilibili, dummy_user_subinfo):
post_router.mock(return_value=Response(200, json=get_json("bilibili_strange_post-0.json")))
bilibili_main_page_router = respx.get("https://www.bilibili.com/")
bilibili_main_page_router.mock(return_value=Response(200))
target = Target("161775300")
res = await bilibili.fetch_new_post(SubUnit(target, [dummy_user_subinfo]))
assert post_router.called
assert len(res) == 0
@ -173,7 +179,7 @@ async def test_fetch_new(bilibili, dummy_user_subinfo):
assert len(res2[0][1]) == 1
post = res2[0][1][0]
assert (
post.text
post.content
== "#罗德厨房——回甘##明日方舟#\r\n明日方舟官方美食漫画,正式开餐。\r\n往事如烟,安然即好。\r\nMenu"
" 01高脚羽兽烤串与罗德岛的领袖\r\n\r\n哔哩哔哩漫画阅读https://manga.bilibili.com/detail/mc31998?from=manga_search\r\n\r\n关注并转发本动态,"
"我们将会在5月27日抽取10位博士赠送【兔兔奇境】周边礼盒一份。 互动抽奖"

View File

@ -24,10 +24,13 @@ async def test_parse_target(bili_bangumi: "BilibiliBangumi"):
res1 = await bili_bangumi.parse_target("28339726")
assert res1 == "28339726"
res2 = await bili_bangumi.parse_target("md28339726")
assert res2 == "28339726"
res3 = await bili_bangumi.parse_target("https://www.bilibili.com/bangumi/media/md28339726")
assert res3 == "28339726"
with pytest.raises(Platform.ParseTargetException):
await bili_bangumi.parse_target("https://www.bilibili.com/bangumi/play/ep683045")
@ -42,21 +45,25 @@ async def test_fetch_bilibili_bangumi_status(bili_bangumi: "BilibiliBangumi", du
bili_bangumi_router.mock(return_value=Response(200, json=get_json("bilibili-gangumi-hanhua0.json")))
bilibili_main_page_router = respx.get("https://www.bilibili.com/")
bilibili_main_page_router.mock(return_value=Response(200))
target = Target("28235413")
res = await bili_bangumi.fetch_new_post(SubUnit(target, [dummy_user_subinfo]))
assert len(res) == 0
res = await bili_bangumi.fetch_new_post(SubUnit(target, [dummy_user_subinfo]))
assert len(res) == 0
target = Target("28235413")
res0 = await bili_bangumi.fetch_new_post(SubUnit(target, [dummy_user_subinfo]))
assert len(res0) == 0
res1 = await bili_bangumi.fetch_new_post(SubUnit(target, [dummy_user_subinfo]))
assert len(res1) == 0
bili_bangumi_router.mock(return_value=Response(200, json=get_json("bilibili-gangumi-hanhua1.json")))
bili_bangumi_detail_router.mock(return_value=Response(200, json=get_json("bilibili-gangumi-hanhua1-detail.json")))
res2 = await bili_bangumi.fetch_new_post(SubUnit(target, [dummy_user_subinfo]))
post = res2[0][1][0]
assert post.target_type == "Bilibili剧集"
assert post.text == "《汉化日记 第三季》第2话 什么是战区导弹防御系统工作日"
assert post.url == "https://www.bilibili.com/bangumi/play/ep519207"
assert post.target_name == "汉化日记 第三季"
assert post.pics == ["http://i0.hdslb.com/bfs/archive/ea0a302c954f9dbc3d593e676486396c551529c9.jpg"]
assert post.compress is True
post2 = res2[0][1][0]
assert post2.platform.name == "Bilibili剧集"
assert post2.title == "《汉化日记 第三季》第2话 什么是战区导弹防御系统工作日"
assert post2.content == "更新至第2话"
assert post2.url == "https://www.bilibili.com/bangumi/play/ep519207"
assert post2.nickname == "汉化日记 第三季"
assert post2.images == ["http://i0.hdslb.com/bfs/archive/ea0a302c954f9dbc3d593e676486396c551529c9.jpg"]
assert post2.compress is True
assert "brief" == post2.get_priority_themes()[0]

View File

@ -1,4 +1,5 @@
from copy import deepcopy
from typing import TYPE_CHECKING
import respx
import pytest
@ -7,6 +8,9 @@ from httpx import Response, AsyncClient
from .utils import get_json
if TYPE_CHECKING:
from nonebot_bison.platform.bilibili import Bilibililive
@pytest.fixture()
def bili_live(app: App):
@ -48,6 +52,7 @@ async def test_fetch_bililive_no_room(bili_live, dummy_only_open_user_subinfo):
@pytest.mark.asyncio
@respx.mock
async def test_fetch_first_live(bili_live, dummy_only_open_user_subinfo):
from nonebot_bison.post import Post
from nonebot_bison.types import Target, SubUnit
mock_bili_live_status = get_json("bili_live_status.json")
@ -60,27 +65,31 @@ async def test_fetch_first_live(bili_live, dummy_only_open_user_subinfo):
bilibili_main_page_router.mock(return_value=Response(200))
target = Target("13164144")
res = await bili_live.batch_fetch_new_post([(SubUnit(target, [dummy_only_open_user_subinfo]))])
res1 = await bili_live.batch_fetch_new_post([(SubUnit(target, [dummy_only_open_user_subinfo]))])
assert bili_live_router.call_count == 1
assert len(res) == 0
assert len(res1) == 0
mock_bili_live_status["data"][target]["live_status"] = 1
bili_live_router.mock(return_value=Response(200, json=mock_bili_live_status))
res2 = await bili_live.batch_fetch_new_post([(SubUnit(target, [dummy_only_open_user_subinfo]))])
assert bili_live_router.call_count == 2
assert len(res2) == 1
post = res2[0][1][0]
assert post.target_type == "Bilibili直播"
assert post.text == "[开播] 【Zc】从0挑战到15肉鸽目前10难度"
assert post.url == "https://live.bilibili.com/3044248"
assert post.target_name == "魔法Zc目录 其他单机"
assert post.pics == ["https://i0.hdslb.com/bfs/live/new_room_cover/fd357f0f3cbbb48e9acfbcda616b946c2454c56c.jpg"]
assert post.compress is True
post2: Post = res2[0][1][0]
assert post2.platform.name == "Bilibili直播"
assert post2.title == "[开播] 【Zc】从0挑战到15肉鸽目前10难度"
assert post2.content == ""
assert post2.url == "https://live.bilibili.com/3044248"
assert post2.nickname == "魔法Zc目录 其他单机"
assert post2.images == ["https://i0.hdslb.com/bfs/live/new_room_cover/fd357f0f3cbbb48e9acfbcda616b946c2454c56c.jpg"]
assert post2.compress is True
assert "brief" == post2.get_priority_themes()[0]
@pytest.mark.asyncio
@respx.mock
async def test_fetch_bililive_only_live_open(bili_live, dummy_only_open_user_subinfo):
async def test_fetch_bililive_only_live_open(bili_live: "Bilibililive", dummy_only_open_user_subinfo):
from nonebot_bison.post import Post
from nonebot_bison.types import Target, SubUnit
mock_bili_live_status = get_json("bili_live_status.json")
@ -92,20 +101,22 @@ async def test_fetch_bililive_only_live_open(bili_live, dummy_only_open_user_sub
bilibili_main_page_router.mock(return_value=Response(200))
target = Target("13164144")
res = await bili_live.batch_fetch_new_post([(SubUnit(target, [dummy_only_open_user_subinfo]))])
bili_live.set_stored_data(target, None)
res1 = await bili_live.batch_fetch_new_post([(SubUnit(target, [dummy_only_open_user_subinfo]))])
assert bili_live_router.call_count == 1
assert len(res[0][1]) == 0
assert len(res1) == 0
# 直播状态更新-上播
mock_bili_live_status["data"][target]["live_status"] = 1
bili_live_router.mock(return_value=Response(200, json=mock_bili_live_status))
res2 = await bili_live.batch_fetch_new_post([(SubUnit(target, [dummy_only_open_user_subinfo]))])
post = res2[0][1][0]
assert post.target_type == "Bilibili直播"
assert post.text == "[开播] 【Zc】从0挑战到15肉鸽目前10难度"
assert post.url == "https://live.bilibili.com/3044248"
assert post.target_name == "魔法Zc目录 其他单机"
assert post.pics == ["https://i0.hdslb.com/bfs/live/new_room_cover/fd357f0f3cbbb48e9acfbcda616b946c2454c56c.jpg"]
assert post.compress is True
post2: Post = res2[0][1][0]
assert post2.platform.name == "Bilibili直播"
assert post2.title == "[开播] 【Zc】从0挑战到15肉鸽目前10难度"
assert post2.url == "https://live.bilibili.com/3044248"
assert post2.nickname == "魔法Zc目录 其他单机"
assert post2.images == ["https://i0.hdslb.com/bfs/live/new_room_cover/fd357f0f3cbbb48e9acfbcda616b946c2454c56c.jpg"]
assert post2.compress is True
# 标题变更
mock_bili_live_status["data"][target]["title"] = "【Zc】从0挑战到15肉鸽目前11难度"
bili_live_router.mock(return_value=Response(200, json=mock_bili_live_status))
@ -133,6 +144,7 @@ def dummy_only_title_user_subinfo(app: App):
@pytest.mark.asyncio()
@respx.mock
async def test_fetch_bililive_only_title_change(bili_live, dummy_only_title_user_subinfo):
from nonebot_bison.post import Post
from nonebot_bison.types import Target, SubUnit
mock_bili_live_status = get_json("bili_live_status.json")
@ -163,13 +175,13 @@ async def test_fetch_bililive_only_title_change(bili_live, dummy_only_title_user
mock_bili_live_status["data"][target]["title"] = "【Zc】从0挑战到15肉鸽目前12难度"
bili_live_router.mock(return_value=Response(200, json=mock_bili_live_status))
res3 = await bili_live.batch_fetch_new_post([(SubUnit(target, [dummy_only_title_user_subinfo]))])
post = res3[0][1][0]
assert post.target_type == "Bilibili直播"
assert post.text == "[标题更新] 【Zc】从0挑战到15肉鸽目前12难度"
assert post.url == "https://live.bilibili.com/3044248"
assert post.target_name == "魔法Zc目录 其他单机"
assert post.pics == ["https://i0.hdslb.com/bfs/live-key-frame/keyframe10170435000003044248mwowx0.jpg"]
assert post.compress is True
post3: Post = res3[0][1][0]
assert post3.platform.name == "Bilibili直播"
assert post3.title == "[标题更新] 【Zc】从0挑战到15肉鸽目前12难度"
assert post3.url == "https://live.bilibili.com/3044248"
assert post3.nickname == "魔法Zc目录 其他单机"
assert post3.images == ["https://i0.hdslb.com/bfs/live-key-frame/keyframe10170435000003044248mwowx0.jpg"]
assert post3.compress is True
# 直播状态更新-下播
mock_bili_live_status["data"][target]["live_status"] = 0
bili_live_router.mock(return_value=Response(200, json=mock_bili_live_status))
@ -191,6 +203,7 @@ def dummy_only_close_user_subinfo(app: App):
@pytest.mark.asyncio
@respx.mock
async def test_fetch_bililive_only_close(bili_live, dummy_only_close_user_subinfo):
from nonebot_bison.post import Post
from nonebot_bison.types import Target, SubUnit
mock_bili_live_status = get_json("bili_live_status.json")
@ -228,13 +241,13 @@ async def test_fetch_bililive_only_close(bili_live, dummy_only_close_user_subinf
bili_live_router.mock(return_value=Response(200, json=mock_bili_live_status))
res4 = await bili_live.batch_fetch_new_post([(SubUnit(target, [dummy_only_close_user_subinfo]))])
assert bili_live_router.call_count == 5
post = res4[0][1][0]
assert post.target_type == "Bilibili直播"
assert post.text == "[下播] 【Zc】从0挑战到15肉鸽目前12难度"
assert post.url == "https://live.bilibili.com/3044248"
assert post.target_name == "魔法Zc目录 其他单机"
assert post.pics == ["https://i0.hdslb.com/bfs/live-key-frame/keyframe10170435000003044248mwowx0.jpg"]
assert post.compress is True
post4: Post = res4[0][1][0]
assert post4.platform.name == "Bilibili直播"
assert post4.title == "[下播] 【Zc】从0挑战到15肉鸽目前12难度"
assert post4.url == "https://live.bilibili.com/3044248"
assert post4.nickname == "魔法Zc目录 其他单机"
assert post4.images == ["https://i0.hdslb.com/bfs/live-key-frame/keyframe10170435000003044248mwowx0.jpg"]
assert post4.compress is True
@pytest.fixture()
@ -250,6 +263,7 @@ def dummy_bililive_user_subinfo(app: App):
@pytest.mark.asyncio
@respx.mock
async def test_fetch_bililive_combo(bili_live, dummy_bililive_user_subinfo):
from nonebot_bison.post import Post
from nonebot_bison.types import Target, SubUnit
mock_bili_live_status = get_json("bili_live_status.json")
@ -274,32 +288,32 @@ async def test_fetch_bililive_combo(bili_live, dummy_bililive_user_subinfo):
mock_bili_live_status["data"][target]["live_status"] = 1
bili_live_router.mock(return_value=Response(200, json=mock_bili_live_status))
res2 = await bili_live.batch_fetch_new_post([(SubUnit(target, [dummy_bililive_user_subinfo]))])
post2 = res2[0][1][0]
assert post2.target_type == "Bilibili直播"
assert post2.text == "[开播] 【Zc】从0挑战到15肉鸽目前11难度"
post2: Post = res2[0][1][0]
assert post2.platform.name == "Bilibili直播"
assert post2.title == "[开播] 【Zc】从0挑战到15肉鸽目前11难度"
assert post2.url == "https://live.bilibili.com/3044248"
assert post2.target_name == "魔法Zc目录 其他单机"
assert post2.pics == ["https://i0.hdslb.com/bfs/live/new_room_cover/fd357f0f3cbbb48e9acfbcda616b946c2454c56c.jpg"]
assert post2.nickname == "魔法Zc目录 其他单机"
assert post2.images == ["https://i0.hdslb.com/bfs/live/new_room_cover/fd357f0f3cbbb48e9acfbcda616b946c2454c56c.jpg"]
assert post2.compress is True
# 标题变更
mock_bili_live_status["data"][target]["title"] = "【Zc】从0挑战到15肉鸽目前12难度"
bili_live_router.mock(return_value=Response(200, json=mock_bili_live_status))
res3 = await bili_live.batch_fetch_new_post([(SubUnit(target, [dummy_bililive_user_subinfo]))])
post3 = res3[0][1][0]
assert post3.target_type == "Bilibili直播"
assert post3.text == "[标题更新] 【Zc】从0挑战到15肉鸽目前12难度"
post3: Post = res3[0][1][0]
assert post3.platform.name == "Bilibili直播"
assert post3.title == "[标题更新] 【Zc】从0挑战到15肉鸽目前12难度"
assert post3.url == "https://live.bilibili.com/3044248"
assert post3.target_name == "魔法Zc目录 其他单机"
assert post3.pics == ["https://i0.hdslb.com/bfs/live-key-frame/keyframe10170435000003044248mwowx0.jpg"]
assert post3.nickname == "魔法Zc目录 其他单机"
assert post3.images == ["https://i0.hdslb.com/bfs/live-key-frame/keyframe10170435000003044248mwowx0.jpg"]
assert post3.compress is True
# 直播状态更新-下播
mock_bili_live_status["data"][target]["live_status"] = 0
bili_live_router.mock(return_value=Response(200, json=mock_bili_live_status))
res4 = await bili_live.batch_fetch_new_post([(SubUnit(target, [dummy_bililive_user_subinfo]))])
post4 = res4[0][1][0]
assert post4.target_type == "Bilibili直播"
assert post4.text == "[下播] 【Zc】从0挑战到15肉鸽目前12难度"
post4: Post = res4[0][1][0]
assert post4.platform.name == "Bilibili直播"
assert post4.title == "[下播] 【Zc】从0挑战到15肉鸽目前12难度"
assert post4.url == "https://live.bilibili.com/3044248"
assert post4.target_name == "魔法Zc目录 其他单机"
assert post4.pics == ["https://i0.hdslb.com/bfs/live-key-frame/keyframe10170435000003044248mwowx0.jpg"]
assert post4.nickname == "魔法Zc目录 其他单机"
assert post4.images == ["https://i0.hdslb.com/bfs/live-key-frame/keyframe10170435000003044248mwowx0.jpg"]
assert post4.compress is True

View File

@ -27,6 +27,7 @@ def ff14_newdata_json_1():
@pytest.mark.asyncio
@respx.mock
async def test_fetch_new(ff14, dummy_user_subinfo, ff14_newdata_json_0, ff14_newdata_json_1):
from nonebot_bison.post import Post
from nonebot_bison.types import Target, SubUnit
newdata = respx.get(
@ -40,8 +41,9 @@ async def test_fetch_new(ff14, dummy_user_subinfo, ff14_newdata_json_0, ff14_new
newdata.mock(return_value=Response(200, json=ff14_newdata_json_1))
res = await ff14.fetch_new_post(SubUnit(target, [dummy_user_subinfo]))
assert newdata.called
post = res[0][1][0]
assert post.target_type == "ff14"
assert post.text == "最终幻想XIV 银质坠饰 <友谊永存>预售开启!\n最终幻想XIV 银质坠饰 <友谊永存>现已开启预售!"
post: Post = res[0][1][0]
assert post.platform.name == "最终幻想XIV官方公告"
assert post.title == "最终幻想XIV 银质坠饰 <友谊永存>预售开启!"
assert post.content == "最终幻想XIV 银质坠饰 <友谊永存>现已开启预售!"
assert post.url == "https://ff.web.sdo.com/web8/index.html#/newstab/newscont/336870"
assert post.target_name == "最终幻想XIV官方公告"
assert post.nickname == "最终幻想XIV官方公告"

View File

@ -25,24 +25,29 @@ def raw_post_list():
@respx.mock
@flaky(max_runs=3, min_passes=1)
async def test_fetch_new(mcbbsnews, dummy_user_subinfo, raw_post_list):
from nonebot_bison.post import Post
news_router = respx.get("https://www.mcbbs.net/forum-news-1.html")
news_router.mock(return_value=Response(200, text=get_file("mcbbsnews/mock/mcbbsnews_post_list_html-0.html")))
new_post = respx.get("https://www.mcbbs.net/thread-1340927-1-1.html")
new_post.mock(return_value=Response(200, text=get_file("mcbbsnews/mock/mcbbsnews_new_post_html.html")))
target = ""
res = await mcbbsnews.fetch_new_post(target, [dummy_user_subinfo])
assert news_router.called
assert len(res) == 0
news_router.mock(return_value=Response(200, text=get_file("mcbbsnews/mock/mcbbsnews_post_list_html-1.html")))
res = await mcbbsnews.fetch_new_post(target, [dummy_user_subinfo])
res1 = await mcbbsnews.fetch_new_post(target, [dummy_user_subinfo])
assert news_router.called
post = res[0][1][0]
post: Post = res1[0][1][0]
raw_post = raw_post_list[0]
assert post.target_type == "MCBBS幻翼块讯"
assert post.text == "{}\n\n└由 {} 发表".format(raw_post["title"], raw_post["author"])
assert post.platform.name == "MCBBS幻翼块讯"
assert post.content == "{}\n\n└由 {} 发表".format(raw_post["title"], raw_post["author"])
assert post.url == "https://www.mcbbs.net/{}".format(raw_post["url"])
assert post.target_name == raw_post["category"]
assert len(post.pics) == 1
assert post.nickname == raw_post["category"]
assert post.images
assert len(post.images) == 1
@pytest.mark.asyncio

View File

@ -52,8 +52,8 @@ async def test_fetch_new(ncm_artist, ncm_artist_0, ncm_artist_1, dummy_user_subi
ncm_router.mock(return_value=Response(200, json=ncm_artist_1))
res2 = await ncm_artist.fetch_new_post(SubUnit(target, [dummy_user_subinfo]))
post = res2[0][1][0]
assert post.target_type == "ncm-artist"
assert post.text == "新专辑发布Y1K"
assert post.platform.platform_name == "ncm-artist"
assert post.content == "新专辑发布Y1K"
assert post.url == "https://music.163.com/#/album?id=131074504"

View File

@ -41,6 +41,7 @@ def ncm_radio_1(ncm_radio_raw: dict):
@pytest.mark.asyncio
@respx.mock
async def test_fetch_new(ncm_radio, ncm_radio_0, ncm_radio_1, dummy_user_subinfo):
from nonebot_bison.post import Post
from nonebot_bison.types import Target, SubUnit
ncm_router = respx.post("http://music.163.com/api/dj/program/byradio")
@ -51,12 +52,12 @@ async def test_fetch_new(ncm_radio, ncm_radio_0, ncm_radio_1, dummy_user_subinfo
assert len(res) == 0
ncm_router.mock(return_value=Response(200, json=ncm_radio_1))
res2 = await ncm_radio.fetch_new_post(SubUnit(target, [dummy_user_subinfo]))
post = res2[0][1][0]
assert post.target_type == "ncm-radio"
assert post.text == "网易云电台更新:「松烟行动」灰齐山麓"
post: Post = res2[0][1][0]
assert post.platform.platform_name == "ncm-radio"
assert post.content == "网易云电台更新:「松烟行动」灰齐山麓"
assert post.url == "https://music.163.com/#/program/2494997688"
assert post.pics == ["http://p1.music.126.net/H5em5xUNIYXcjJhOmeaSqQ==/109951166647436789.jpg"]
assert post.target_name == "《明日方舟》游戏原声OST"
assert post.images == ["http://p1.music.126.net/H5em5xUNIYXcjJhOmeaSqQ==/109951166647436789.jpg"]
assert post.nickname == "《明日方舟》游戏原声OST"
async def test_parse_target(ncm_radio: "NcmRadio"):

View File

@ -65,10 +65,10 @@ def mock_platform_without_cats_tags(app: App):
async def parse(self, raw_post: "RawPost") -> "Post":
return Post(
"mock_platform",
self,
raw_post["text"],
"http://t.tt/" + str(self.get_id(raw_post)),
target_name="Mock",
nickname="Mock",
)
@classmethod
@ -127,10 +127,10 @@ def mock_platform(app: App):
async def parse(self, raw_post: "RawPost") -> "Post":
return Post(
"mock_platform",
self,
raw_post["text"],
"http://t.tt/" + str(self.get_id(raw_post)),
target_name="Mock",
nickname="Mock",
)
@classmethod
@ -194,10 +194,10 @@ def mock_platform_no_target(app: App, mock_scheduler_conf):
async def parse(self, raw_post: "RawPost") -> "Post":
return Post(
"mock_platform",
self,
raw_post["text"],
"http://t.tt/" + str(self.get_id(raw_post)),
target_name="Mock",
nickname="Mock",
)
@classmethod
@ -250,10 +250,10 @@ def mock_platform_no_target_2(app: App, mock_scheduler_conf):
async def parse(self, raw_post: "RawPost") -> "Post":
return Post(
"mock_platform_2",
self,
raw_post["text"],
"http://t.tt/" + str(self.get_id(raw_post)),
target_name="Mock",
nickname="Mock",
)
@classmethod
@ -314,7 +314,7 @@ def mock_status_change(app: App):
return []
async def parse(self, raw_post) -> "Post":
return Post("mock_status", raw_post["text"], "")
return Post(self, raw_post["text"], "")
def get_category(self, raw_post):
return raw_post["cat"]
@ -337,7 +337,7 @@ async def test_new_message_target_without_cats_tags(mock_platform_without_cats_t
assert len(res2) == 1
posts_1 = res2[0][1]
assert len(posts_1) == 3
id_set_1 = {x.text for x in posts_1}
id_set_1 = {x.content for x in posts_1}
assert "p2" in id_set_1
assert "p3" in id_set_1
assert "p4" in id_set_1
@ -369,9 +369,9 @@ async def test_new_message_target(mock_platform, user_info_factory):
assert len(posts_1) == 2
assert len(posts_2) == 1
assert len(posts_3) == 1
id_set_1 = {x.text for x in posts_1}
id_set_2 = {x.text for x in posts_2}
id_set_3 = {x.text for x in posts_3}
id_set_1 = {x.content for x in posts_1}
id_set_2 = {x.content for x in posts_2}
id_set_3 = {x.content for x in posts_3}
assert "p2" in id_set_1
assert "p3" in id_set_1
assert "p2" in id_set_2
@ -404,9 +404,9 @@ async def test_new_message_no_target(mock_platform_no_target, user_info_factory)
assert len(posts_1) == 2
assert len(posts_2) == 1
assert len(posts_3) == 1
id_set_1 = {x.text for x in posts_1}
id_set_2 = {x.text for x in posts_2}
id_set_3 = {x.text for x in posts_3}
id_set_1 = {x.content for x in posts_1}
id_set_2 = {x.content for x in posts_2}
id_set_3 = {x.content for x in posts_3}
assert "p2" in id_set_1
assert "p3" in id_set_1
assert "p2" in id_set_2
@ -432,7 +432,7 @@ async def test_status_change(mock_status_change, user_info_factory):
assert len(res2) == 1
posts = res2[0][1]
assert len(posts) == 1
assert posts[0].text == "on"
assert posts[0].content == "on"
res3 = await mock_status_change(ProcessContext(), AsyncClient()).fetch_new_post(
SubUnit(
Target("dummy"),
@ -444,7 +444,7 @@ async def test_status_change(mock_status_change, user_info_factory):
)
assert len(res3) == 2
assert len(res3[0][1]) == 1
assert res3[0][1][0].text == "off"
assert res3[0][1][0].content == "off"
assert len(res3[1][1]) == 0
res4 = await mock_status_change(ProcessContext(), AsyncClient()).fetch_new_post(
SubUnit(Target("dummy"), [user_info_factory([1, 2], [])])
@ -473,7 +473,7 @@ async def test_group(
assert len(res2) == 1
posts = res2[0][1]
assert len(posts) == 2
id_set_2 = {x.text for x in posts}
id_set_2 = {x.content for x in posts}
assert "p2" in id_set_2
assert "p6" in id_set_2
res3 = await group_platform.fetch_new_post(SubUnit(dummy, [user_info_factory([1, 4], [])]))
@ -512,10 +512,10 @@ async def test_batch_fetch_new_message(app: App):
async def parse(self, raw_post: "RawPost") -> "Post":
return Post(
"mock_platform",
self,
raw_post["text"],
"http://t.tt/" + str(self.get_id(raw_post)),
target_name="Mock",
nickname="Mock",
)
@classmethod
@ -556,7 +556,7 @@ async def test_batch_fetch_new_message(app: App):
send_set = set()
for platform_target, posts in res2:
for post in posts:
send_set.add((platform_target, post.text))
send_set.add((platform_target, post.content))
assert (TargetQQGroup(group_id=123), "p3") in send_set
assert (TargetQQGroup(group_id=123), "p4") in send_set
assert (TargetQQGroup(group_id=234), "p4") in send_set
@ -578,7 +578,7 @@ async def test_batch_fetch_compare_status(app: App):
enable_tag = False
schedule_type = "interval"
schedule_kw = {"seconds": 10}
has_target = False
has_target = True
categories = {
Category(1): "转发",
Category(2): "视频",
@ -603,7 +603,7 @@ async def test_batch_fetch_compare_status(app: App):
return []
async def parse(self, raw_post) -> "Post":
return Post("mock_status", raw_post["text"], "")
return Post(self, raw_post["text"], "")
def get_category(self, raw_post):
return raw_post["cat"]
@ -627,7 +627,7 @@ async def test_batch_fetch_compare_status(app: App):
send_set = set()
for platform_target, posts in res2:
for post in posts:
send_set.add((platform_target, post.text))
send_set.add((platform_target, post.content))
assert len(send_set) == 3
assert (TargetQQGroup(group_id=123), "off") in send_set
assert (TargetQQGroup(group_id=123), "on") in send_set

View File

@ -46,7 +46,7 @@ def update_time_feed_1():
root = ET.fromstring(file)
item = root.find("channel/item")
current_time = datetime.now(pytz.timezone("GMT")).strftime("%a, %d %b %Y %H:%M:%S %Z")
assert item
assert item is not None
pubdate_elem = item.find("pubDate")
assert pubdate_elem is not None
pubdate_elem.text = current_time
@ -85,8 +85,9 @@ async def test_fetch_new_1(
assert len(res2[0][1]) == 1
post1 = res2[0][1][0]
assert post1.url == "https://twitter.com/ArknightsStaff/status/1659091539023282178"
assert post1.title is None
assert (
post1.text
post1.content
== "【#統合戦略】 引き続き新テーマ「ミヅキと紺碧の樹」の新要素及びシステムの変更点を一部ご紹介します!"
" 今回は「灯火」、「ダイス」、「記号認識」、「鍵」についてです。詳細は添付の画像をご確認ください。"
"#アークナイツ https://t.co/ARmptV0Zvu"
@ -114,9 +115,8 @@ async def test_fetch_new_2(
assert len(res2[0][1]) == 1
post1 = res2[0][1][0]
assert post1.url == "http://www.ruanyifeng.com/blog/2023/05/weekly-issue-255.html"
assert (
post1.text == "科技爱好者周刊(第 255 期):对待 AI 的正确态度\n\n这里记录每周值得分享的科技内容,周五发布。..."
)
assert post1.title == "科技爱好者周刊(第 255 期):对待 AI 的正确态度"
assert post1.content == "这里记录每周值得分享的科技内容,周五发布。..."
@pytest.fixture()
@ -150,7 +150,8 @@ async def test_fetch_new_3(
assert len(res2[0][1]) == 1
post1 = res2[0][1][0]
assert post1.url == "https://github.com/R3nzTheCodeGOD/R3nzSkin/releases/tag/v3.0.9"
assert post1.text == "R3nzSkin\n\nNo content."
assert post1.title == "R3nzSkin"
assert post1.content == "No content."
@pytest.mark.asyncio
@ -173,7 +174,7 @@ async def test_fetch_new_4(
assert len(res2[0][1]) == 1
post1 = res2[0][1][0]
assert post1.url == "https://wallhaven.cc/w/85rjej"
assert post1.text == "85rjej.jpg"
assert post1.content == "85rjej.jpg"
def test_similar_text_process():

View File

@ -41,6 +41,7 @@ async def test_get_name(weibo):
@pytest.mark.asyncio
@respx.mock
async def test_fetch_new(weibo, dummy_user_subinfo):
from nonebot_bison.post import Post
from nonebot_bison.types import Target, SubUnit
ak_list_router = respx.get("https://m.weibo.cn/api/container/getIndex?containerid=1076036279793937")
@ -64,12 +65,13 @@ async def test_fetch_new(weibo, dummy_user_subinfo):
res3 = await weibo.fetch_new_post(SubUnit(target, [dummy_user_subinfo]))
assert len(res3[0][1]) == 1
assert not detail_router.called
post = res3[0][1][0]
assert post.target_type == "weibo"
assert post.text == "#明日方舟#\nSideStory「沃伦姆德的薄暮」复刻现已开启 "
post: Post = res3[0][1][0]
assert post.platform.platform_name == "weibo"
assert post.content == "#明日方舟#\nSideStory「沃伦姆德的薄暮」复刻现已开启 "
assert post.url == "https://weibo.com/6279793937/KkBtUx2dv"
assert post.target_name == "明日方舟Arknights"
assert len(post.pics) == 1
assert post.nickname == "明日方舟Arknights"
assert post.images
assert len(post.images) == 1
@pytest.mark.asyncio
@ -93,7 +95,7 @@ async def test_parse_long(weibo):
detail_router.mock(return_value=Response(200, text=get_file("weibo_detail_4645748019299849")))
raw_post = get_json("weibo_ak_list_1.json")["data"]["cards"][0]
post = await weibo.parse(raw_post)
assert "全文" not in post.text
assert "全文" not in post.content
assert detail_router.called

111
tests/post/test_generate.py Normal file
View File

@ -0,0 +1,111 @@
from time import time
from typing import Any
import pytest
from nonebug.app import App
from httpx import AsyncClient
now = time()
passed = now - 3 * 60 * 60
raw_post_list_1 = [{"id": 1, "text": "p1", "date": now, "tags": ["tag1"], "category": 1}]
raw_post_list_2 = raw_post_list_1 + [
{"id": 2, "text": "p2", "date": now, "tags": ["tag1"], "category": 1},
{"id": 3, "text": "p3", "date": now, "tags": ["tag2"], "category": 2},
{"id": 4, "text": "p4", "date": now, "tags": ["tag2"], "category": 3},
]
@pytest.fixture()
def mock_platform(app: App):
from nonebot_bison.post import Post
from nonebot_bison.types import Target, RawPost
from nonebot_bison.platform.platform import NewMessage
class MockPlatform(NewMessage):
platform_name = "mock_platform"
name = "Mock Platform"
enabled = True
is_common = True
schedule_interval = 10
enable_tag = False
categories = {}
has_target = True
sub_index = 0
@classmethod
async def get_target_name(cls, client, _: "Target"):
return "MockPlatform"
def get_id(self, post: "RawPost") -> Any:
return post["id"]
def get_date(self, raw_post: "RawPost") -> float:
return raw_post["date"]
async def parse(self, raw_post: "RawPost") -> "Post":
return Post(
self,
raw_post["text"],
url="http://t.tt/" + str(self.get_id(raw_post)),
nickname="Mock",
)
@classmethod
async def get_sub_list(cls, _: "Target"):
if cls.sub_index == 0:
cls.sub_index += 1
return raw_post_list_1
else:
return raw_post_list_2
return MockPlatform
@pytest.mark.asyncio
async def test_generate_msg(mock_platform):
from nonebot_plugin_saa import Text, Image
from nonebot_bison.post import Post
from nonebot_bison.utils import ProcessContext
from nonebot_bison.plugin_config import plugin_config
post: Post = await mock_platform(ProcessContext(), AsyncClient()).parse(raw_post_list_1[0])
assert post.platform.default_theme == "basic"
res = await post.generate()
assert len(res) == 1
assert isinstance(res[0], Text)
assert str(res[0]) == "p1\n来源: Mock Platform Mock\n详情: http://t.tt/1"
post.platform.default_theme = "ht2i"
assert post.get_config_theme() is None
assert set(post.get_priority_themes()) == {"basic", "ht2i"}
assert post.get_priority_themes()[0] == "ht2i"
res1 = await post.generate()
assert isinstance(res1[0], Image)
plugin_config.bison_theme_use_browser = False
res3 = await post.generate()
assert res3[0]
assert isinstance(res3[0], Text)
@pytest.mark.asyncio
@pytest.mark.render
async def test_msg_segments_convert(mock_platform):
from nonebot_plugin_saa import Image
from nonebot_bison.post import Post
from nonebot_bison.utils import ProcessContext
from nonebot_bison.plugin_config import plugin_config
plugin_config.bison_use_pic = True
post: Post = await mock_platform(ProcessContext(), AsyncClient()).parse(raw_post_list_1[0])
assert post.platform.default_theme == "basic"
res = await post.generate_messages()
assert len(res) == 1
assert isinstance(res[0][0], Image)

View File

@ -1,58 +0,0 @@
from pathlib import Path
import respx
import pytest
from httpx import Response
from nonebug.app import App
@pytest.fixture()
def ms_list():
from nonebot_plugin_saa import Text, Image, MessageSegmentFactory
msg_segments: list[MessageSegmentFactory] = []
msg_segments.append(Text("【Zc】每早合约日替攻略"))
msg_segments.append(
Image(
image="http://i0.hdslb.com/bfs/live/new_room_cover/cf7d4d3b2f336c6dba299644c3af952c5db82612.jpg",
)
)
msg_segments.append(Text("来源: Bilibili直播 魔法Zc目录"))
msg_segments.append(Text("详情: https://live.bilibili.com/3044248"))
return msg_segments
@pytest.fixture()
def expected_md():
return (
"【Zc】每早合约日替攻略<br>![Image](http://i0.hdslb.com/bfs/live/new_room_cover/cf7d4d3b2f336c6dba299644c3af952c5db82612.jpg)\n来源:"
" Bilibili直播 魔法Zc目录<br>详情: https://live.bilibili.com/3044248<br>"
)
def test_gene_md(app: App, expected_md, ms_list):
from nonebot_bison.post.custom_post import CustomPost
cp = CustomPost(ms_factories=ms_list)
cp_md = cp._generate_md()
assert cp_md == expected_md
@respx.mock
@pytest.mark.asyncio
async def test_gene_pic(app: App, ms_list, expected_md):
from nonebot_bison.post.custom_post import CustomPost
pic_router = respx.get("http://i0.hdslb.com/bfs/live/new_room_cover/cf7d4d3b2f336c6dba299644c3af952c5db82612.jpg")
pic_path = Path(__file__).parent / "platforms" / "static" / "custom_post_pic.jpg"
with open(pic_path, mode="rb") as f:
mock_pic = f.read()
pic_router.mock(return_value=Response(200, stream=mock_pic)) # type: ignore
cp = CustomPost(ms_factories=ms_list)
cp_pic_msg_md: str = cp._generate_md()
assert cp_pic_msg_md == expected_md

View File

@ -65,60 +65,52 @@ async def downloaded_resource_2():
@pytest.mark.external
@flaky
async def test_9_merge(app: App, downloaded_resource: list[bytes]):
from nonebot_bison.post import Post
from nonebot_bison.utils import pic_merge, http_client
post = Post("", "", "", pics=list(downloaded_resource))
await post._pic_merge()
assert len(post.pics) == 5
await post.generate_messages()
pics = await pic_merge(list(downloaded_resource), http_client())
assert len(pics) == 5
@pytest.mark.external
@flaky
async def test_9_merge_2(app: App, downloaded_resource_2: list[bytes]):
from nonebot_bison.post import Post
from nonebot_bison.utils import pic_merge, http_client
post = Post("", "", "", pics=list(downloaded_resource_2))
await post._pic_merge()
assert len(post.pics) == 4
await post.generate_messages()
pics = await pic_merge(list(downloaded_resource_2), http_client())
assert len(pics) == 4
@pytest.mark.external
@flaky
async def test_6_merge(app: App, downloaded_resource: list[bytes]):
from nonebot_bison.post import Post
from nonebot_bison.utils import pic_merge, http_client
post = Post("", "", "", pics=list(downloaded_resource[0:6] + downloaded_resource[9:]))
await post._pic_merge()
assert len(post.pics) == 5
pics = await pic_merge(list(downloaded_resource[0:6] + downloaded_resource[9:]), http_client())
assert len(pics) == 5
@pytest.mark.external
@flaky
async def test_3_merge(app: App, downloaded_resource: list[bytes]):
from nonebot_bison.post import Post
from nonebot_bison.utils import pic_merge, http_client
post = Post("", "", "", pics=list(downloaded_resource[0:3] + downloaded_resource[9:]))
await post._pic_merge()
assert len(post.pics) == 5
pics = await pic_merge(list(downloaded_resource[0:3] + downloaded_resource[9:]), http_client())
assert len(pics) == 5
@pytest.mark.external
@flaky
async def test_6_merge_only(app: App, downloaded_resource: list[bytes]):
from nonebot_bison.post import Post
from nonebot_bison.utils import pic_merge, http_client
post = Post("", "", "", pics=list(downloaded_resource[0:6]))
await post._pic_merge()
assert len(post.pics) == 1
pics = await pic_merge(list(downloaded_resource[0:6]), http_client())
assert len(pics) == 1
@pytest.mark.external
@flaky
async def test_3_merge_only(app: App, downloaded_resource: list[bytes]):
from nonebot_bison.post import Post
from nonebot_bison.utils import pic_merge, http_client
post = Post("", "", "", pics=list(downloaded_resource[0:3]))
await post._pic_merge()
assert len(post.pics) == 1
pics = await pic_merge(list(downloaded_resource[0:3]), http_client())
assert len(pics) == 1

View File

@ -20,3 +20,18 @@ async def test_render(app: App):
"并将其转换成一个完整的单页应用SPA其他的页面则会只在用户浏览到的时候才按需加载。"
)
assert isinstance(res, Image)
@pytest.mark.asyncio
@pytest.mark.render
async def test_convert(app: App):
from nonebot_plugin_saa import Text, Image
from nonebot_bison.utils import text_to_image
from nonebot_bison.plugin_config import plugin_config
plugin_config.bison_use_pic = True
text = Text("如果,生命的脚印终有一天会被时间的尘埃掩埋......那我们就永远不能——停下脚步")
res = await text_to_image(text)
assert isinstance(res, Image)

View File

@ -0,0 +1,33 @@
from typing import Literal
import pytest
from nonebug import App
@pytest.mark.asyncio
async def test_registry_new_theme(app: App):
from nonebot_bison.theme import Theme, ThemeRegistrationError, theme_manager
class MockTheme(Theme):
name: Literal["mock_theme"] = "mock_theme"
async def render(self, _):
return ""
assert len(theme_manager) == 5
assert "arknights" in theme_manager
assert "basic" in theme_manager
assert "brief" in theme_manager
assert "ceobecanteen" in theme_manager
assert "ht2i" in theme_manager
assert "mock_theme" not in theme_manager
theme_manager.register(MockTheme())
assert len(theme_manager) == 6
assert "mock_theme" in theme_manager
# duplicated registration
with pytest.raises(ThemeRegistrationError, match="duplicate"):
theme_manager.register(MockTheme())
theme_manager.unregister("mock_theme")

222
tests/theme/test_themes.py Normal file
View File

@ -0,0 +1,222 @@
from time import time
from typing import Any
import pytest
from flaky import flaky
from nonebug import App
from httpx import AsyncClient
now = time()
passed = now - 3 * 60 * 60
raw_post_list_1 = [{"id": 1, "text": "p1", "date": now, "tags": ["tag1"], "category": 1}]
raw_post_list_2 = raw_post_list_1 + [
{"id": 2, "text": "p2", "date": now, "tags": ["tag1"], "category": 1},
{"id": 3, "text": "p3", "date": now, "tags": ["tag2"], "category": 2},
{"id": 4, "text": "p4", "date": now, "tags": ["tag2"], "category": 3},
]
@pytest.fixture()
def mock_platform(app: App):
from nonebot_bison.post import Post
from nonebot_bison.types import Target, RawPost
from nonebot_bison.platform.platform import NewMessage
class MockPlatform(NewMessage):
platform_name = "mock_platform"
name = "Mock Platform"
enabled = True
is_common = True
schedule_interval = 10
enable_tag = False
categories = {}
has_target = True
sub_index = 0
@classmethod
async def get_target_name(cls, client, _: "Target"):
return "MockPlatform"
def get_id(self, post: "RawPost") -> Any:
return post["id"]
def get_date(self, raw_post: "RawPost") -> float:
return raw_post["date"]
async def parse(self, raw_post: "RawPost") -> "Post":
return Post(
self,
raw_post["text"],
url="http://t.tt/" + str(self.get_id(raw_post)),
nickname="Mock",
)
@classmethod
async def get_sub_list(cls, _: "Target"):
if cls.sub_index == 0:
cls.sub_index += 1
return raw_post_list_1
else:
return raw_post_list_2
return MockPlatform
@pytest.fixture()
def mock_post(app: App, mock_platform):
from nonebot_bison.post import Post
from nonebot_bison.utils import ProcessContext
return Post(
mock_platform(ProcessContext(), AsyncClient()),
"text",
title="title",
images=["http://t.tt/1.jpg"],
timestamp=1234567890,
url="http://t.tt/1",
avatar="http://t.tt/avatar.jpg",
nickname="Mock",
description="description",
)
@pytest.mark.asyncio
async def test_theme_need_browser(app: App, mock_post):
from nonebot_bison.theme import Theme, theme_manager
class MockTheme(Theme):
name: str = "mock_theme"
need_browser: bool = False
async def render(self, post):
return []
theme = MockTheme()
theme_manager.register(theme)
mock_post.platform.default_theme = theme.name
await theme.do_render(mock_post)
assert not theme._browser_checked
theme.need_browser = True
await theme.do_render(mock_post)
assert theme._browser_checked
theme_manager.unregister(theme.name)
@pytest.mark.asyncio
async def test_theme_no_enable_use_browser(app: App, mock_post):
from nonebot_bison.plugin_config import plugin_config
plugin_config.bison_theme_use_browser = False
from nonebot_bison.theme import Theme, ThemeRenderUnsupportError, theme_manager
class MockTheme(Theme):
name: str = "mock_theme"
need_browser: bool = True
async def render(self, post):
return []
theme = MockTheme()
theme_manager.register(theme)
mock_post.platform.default_theme = theme.name
with pytest.raises(ThemeRenderUnsupportError, match="not support render"):
await theme.do_render(mock_post)
theme_manager.unregister(theme.name)
plugin_config.bison_theme_use_browser = True
@pytest.mark.asyncio
@flaky(max_runs=3, min_passes=1)
async def test_arknights_theme(app: App, mock_post):
from nonebot_plugin_saa import Image
from nonebot_bison.theme import theme_manager
from nonebot_bison.theme.themes.arknights import ArknightsTheme
arknights_theme = theme_manager["arknights"]
assert isinstance(arknights_theme, ArknightsTheme)
assert arknights_theme.name == "arknights"
res = await arknights_theme.render(mock_post)
assert len(res) == 1
assert isinstance(res[0], Image)
@pytest.mark.asyncio
async def test_basic_theme(app: App, mock_post):
from nonebot_plugin_saa import Text, Image
from nonebot_bison.theme import theme_manager
from nonebot_bison.theme.themes.basic import BasicTheme
basic_theme = theme_manager["basic"]
assert isinstance(basic_theme, BasicTheme)
assert basic_theme.name == "basic"
res = await basic_theme.render(mock_post)
assert len(res) == 2
assert res[0] == Text("title\n\ntext\n来源: Mock Platform Mock\n详情: http://t.tt/1")
assert isinstance(res[1], Image)
@pytest.mark.asyncio
async def test_brief_theme(app: App, mock_post):
from nonebot_plugin_saa import Text, Image
from nonebot_bison.theme import theme_manager
from nonebot_bison.theme.themes.brief import BriefTheme
brief_theme = theme_manager["brief"]
assert isinstance(brief_theme, BriefTheme)
assert brief_theme.name == "brief"
res = await brief_theme.render(mock_post)
assert len(res) == 2
assert res[0] == Text("title\n\n来源: Mock Platform Mock\n详情: http://t.tt/1")
assert isinstance(res[1], Image)
@pytest.mark.render
@pytest.mark.asyncio
@flaky(max_runs=3, min_passes=1)
async def test_ceobecanteen_theme(app: App, mock_post):
from nonebot_plugin_saa import Text, Image
from nonebot_bison.theme import theme_manager
from nonebot_bison.theme.themes.ceobe_canteen import CeobeCanteenTheme
ceobecanteen_theme = theme_manager["ceobecanteen"]
assert isinstance(ceobecanteen_theme, CeobeCanteenTheme)
assert ceobecanteen_theme.name == "ceobecanteen"
res = await ceobecanteen_theme.render(mock_post)
assert len(res) == 3
assert isinstance(res[0], Image)
assert isinstance(res[2], Image)
assert res[1] == Text("来源: Mock Platform Mock\n详情: http://t.tt/1")
@pytest.mark.render
@pytest.mark.asyncio
@flaky(max_runs=3, min_passes=1)
async def test_ht2i_theme(app: App, mock_post):
from nonebot_plugin_saa import Text, Image
from nonebot_bison.theme import theme_manager
from nonebot_bison.theme.themes.ht2i import Ht2iTheme
ht2i_theme = theme_manager["ht2i"]
assert isinstance(ht2i_theme, Ht2iTheme)
assert ht2i_theme.name == "ht2i"
res = await ht2i_theme.render(mock_post)
assert len(res) == 3
assert isinstance(res[0], Image)
assert isinstance(res[2], Image)
assert res[1] == Text("详情: http://t.tt/1")