mirror of
https://github.com/suyiiyii/nonebot-bison.git
synced 2025-06-02 09:26:12 +08:00
✨ 添加 Theme 功能
This commit is contained in:
parent
6aaec45d15
commit
f202071e9f
@ -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",
|
||||
]
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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,
|
||||
)
|
||||
|
||||
|
@ -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官方公告")
|
||||
|
@ -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]:
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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"],
|
||||
)
|
||||
|
@ -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"],
|
||||
)
|
||||
|
@ -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:
|
||||
|
@ -1,3 +1 @@
|
||||
from .post import Post
|
||||
|
||||
__all__ = ["Post"]
|
||||
from .post import Post as Post
|
||||
|
@ -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
|
||||
|
@ -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 += "\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
|
@ -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__}")
|
||||
|
@ -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;
|
||||
}
|
22
nonebot_bison/theme/__init__.py
Normal file
22
nonebot_bison/theme/__init__.py
Normal 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__)
|
36
nonebot_bison/theme/registry.py
Normal file
36
nonebot_bison/theme/registry.py
Normal 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()
|
3
nonebot_bison/theme/themes/arknights/__init__.py
Normal file
3
nonebot_bison/theme/themes/arknights/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .build import ArknightsTheme
|
||||
|
||||
__theme_meta__ = ArknightsTheme()
|
69
nonebot_bison/theme/themes/arknights/build.py
Normal file
69
nonebot_bison/theme/themes/arknights/build.py
Normal 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)]
|
@ -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>
|
3
nonebot_bison/theme/themes/basic/__init__.py
Normal file
3
nonebot_bison/theme/themes/basic/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .build import BasicTheme
|
||||
|
||||
__theme_meta__ = BasicTheme()
|
40
nonebot_bison/theme/themes/basic/build.py
Normal file
40
nonebot_bison/theme/themes/basic/build.py
Normal 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
|
3
nonebot_bison/theme/themes/brief/__init__.py
Normal file
3
nonebot_bison/theme/themes/brief/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .build import BriefTheme
|
||||
|
||||
__theme_meta__ = BriefTheme()
|
32
nonebot_bison/theme/themes/brief/build.py
Normal file
32
nonebot_bison/theme/themes/brief/build.py
Normal 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
|
10
nonebot_bison/theme/themes/ceobe_canteen/README.md
Normal file
10
nonebot_bison/theme/themes/ceobe_canteen/README.md
Normal 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) 授权许可使用。
|
3
nonebot_bison/theme/themes/ceobe_canteen/__init__.py
Normal file
3
nonebot_bison/theme/themes/ceobe_canteen/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .build import CeobeCanteenTheme
|
||||
|
||||
__theme_meta__ = CeobeCanteenTheme()
|
113
nonebot_bison/theme/themes/ceobe_canteen/build.py
Normal file
113
nonebot_bison/theme/themes/ceobe_canteen/build.py
Normal 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 |
@ -0,0 +1,98 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>小刻食堂分享卡片</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="ceobecanteen-card">
|
||||
{% if card.content.image %}
|
||||
<img src="{{ card.content.image }}" class="cover-img">
|
||||
{% endif %}
|
||||
{% if card.content.text %}
|
||||
<div class="main-content">{{ card.content.text }}</div>
|
||||
{% endif %}
|
||||
<div class="footer">
|
||||
<div class="datasource">
|
||||
<div class="datasource-text">
|
||||
<div class="datasource-name">{{ card.info.datasource }}</div>
|
||||
<div class="time">{{ card.info.time }}</div>
|
||||
</div>
|
||||
<img class='qr' src="{{ card.qr }}">
|
||||
</div>
|
||||
<div class="source">
|
||||
<img class='bison-logo' src="bison_logo.jpg">
|
||||
<div class="source-text">
|
||||
<div class="slogan">小刻吃到饼啦!</div>
|
||||
<div class="linkage">bison&小刻食堂联动</div>
|
||||
<div class="description">来自小刻食堂再转发</div>
|
||||
</div>
|
||||
<img class='ceobe-logo' src="ceobecanteen_logo.png">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<style type="text/css">
|
||||
#ceobecanteen-card {
|
||||
width: 700px;
|
||||
background-color: rgb(240, 236, 233);
|
||||
}
|
||||
.cover-img {
|
||||
width: 100%;
|
||||
}
|
||||
.main-content {
|
||||
padding: 30px;
|
||||
white-space: pre-line;
|
||||
}
|
||||
.footer {
|
||||
margin: 0 2%;
|
||||
height: 80px;
|
||||
border-top: 1px solid rgb(185, 181, 177);
|
||||
display: flex;
|
||||
padding: 10px 0;
|
||||
}
|
||||
.footer .datasource {
|
||||
width: 45%;
|
||||
padding-right: 5px;
|
||||
border-right: 1px solid rgb(185, 181, 177);
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
.footer .datasource .datasource-text {
|
||||
align-self: center;
|
||||
}
|
||||
.footer .datasource .datasource-text .datasource-name {
|
||||
font-weight: bold;
|
||||
font-size: 24px;
|
||||
}
|
||||
.footer .datasource .datasource-text .time {
|
||||
font-size: 13px;
|
||||
color: rgb(138, 136, 134);
|
||||
}
|
||||
.footer .datasource .qr {
|
||||
width: 80px;
|
||||
object-fit: contain;
|
||||
}
|
||||
.source {
|
||||
width: 55%;
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
color: rgb(138, 136, 134);
|
||||
font-size: 13px;
|
||||
}
|
||||
.footer .source .source-text {
|
||||
align-self: center;
|
||||
}
|
||||
.source .slogan {
|
||||
font-weight: bold;
|
||||
font-size: 24px;
|
||||
color: black;
|
||||
}
|
||||
.source .bison-logo,
|
||||
.source .ceobe-logo {
|
||||
width: 60px;
|
||||
object-fit: contain;
|
||||
}
|
||||
</style>
|
Binary file not shown.
After Width: | Height: | Size: 706 KiB |
3
nonebot_bison/theme/themes/ht2i/__init__.py
Normal file
3
nonebot_bison/theme/themes/ht2i/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .build import Ht2iTheme
|
||||
|
||||
__theme_meta__ = Ht2iTheme()
|
50
nonebot_bison/theme/themes/ht2i/build.py
Normal file
50
nonebot_bison/theme/themes/ht2i/build.py
Normal 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
|
78
nonebot_bison/theme/types.py
Normal file
78
nonebot_bison/theme/types.py
Normal 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
|
22
nonebot_bison/theme/utils.py
Normal file
22
nonebot_bison/theme/utils.py
Normal 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")
|
@ -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",
|
||||
]
|
||||
|
||||
|
||||
|
110
nonebot_bison/utils/image.py
Normal file
110
nonebot_bison/utils/image.py
Normal 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
229
poetry.lock
generated
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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位博士赠送【兔兔奇境】周边礼盒一份。 互动抽奖"
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
|
@ -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官方公告"
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
||||
|
||||
|
@ -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"):
|
||||
|
@ -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
|
||||
|
@ -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():
|
||||
|
@ -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
111
tests/post/test_generate.py
Normal 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)
|
@ -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>\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
|
@ -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
|
||||
|
@ -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)
|
||||
|
33
tests/theme/test_registry.py
Normal file
33
tests/theme/test_registry.py
Normal 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
222
tests/theme/test_themes.py
Normal 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")
|
Loading…
x
Reference in New Issue
Block a user