mirror of
https://github.com/suyiiyii/nonebot-bison.git
synced 2025-06-04 02:26:11 +08:00
♻️ 为B站动态API建立model,转发动态放入repost字段 (#507)
* 🐛 替换为pyd兼容性写法 * ♻️ 优化 Bilibili 推送解析 * 🐛 为B站动态API建立Model,动态转发部分放入repost字段 * 🔥 删掉无关紧要的ttl字段 * 🐛 修理转发链接解析 * 🧑💻 增加可读性 * 🐛 使用pyd兼容函数 * 🐛 处理 pyd1 model rebuild 时没有递归的情况 * ♻️ 将链接生成进行统一
This commit is contained in:
parent
6cceb07a33
commit
9bc5be5426
@ -1,16 +1,16 @@
|
|||||||
import re
|
import re
|
||||||
import json
|
import json
|
||||||
from abc import ABC
|
from abc import ABC
|
||||||
from typing import Any
|
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from enum import Enum, unique
|
from enum import Enum, unique
|
||||||
from typing_extensions import Self
|
from typing_extensions import Self
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Any, TypeVar, TypeAlias, NamedTuple
|
||||||
|
|
||||||
from httpx import AsyncClient
|
from httpx import AsyncClient
|
||||||
from nonebot.log import logger
|
from nonebot.log import logger
|
||||||
from pydantic import Field, BaseModel
|
from pydantic import Field, BaseModel
|
||||||
from nonebot.compat import type_validate_python
|
from nonebot.compat import PYDANTIC_V2, ConfigDict, type_validate_json, type_validate_python
|
||||||
|
|
||||||
from nonebot_bison.compat import model_rebuild
|
from nonebot_bison.compat import model_rebuild
|
||||||
|
|
||||||
@ -19,6 +19,90 @@ from ..types import Tag, Target, RawPost, ApiError, Category
|
|||||||
from ..utils import SchedulerConfig, http_client, text_similarity
|
from ..utils import SchedulerConfig, http_client, text_similarity
|
||||||
from .platform import NewMessage, StatusChange, CategoryNotSupport, CategoryNotRecognize
|
from .platform import NewMessage, StatusChange, CategoryNotSupport, CategoryNotRecognize
|
||||||
|
|
||||||
|
TBaseModel = TypeVar("TBaseModel", bound=type[BaseModel])
|
||||||
|
|
||||||
|
|
||||||
|
# 不能当成装饰器用
|
||||||
|
# 当装饰器用时,global namespace 中还没有被装饰的类,会报错
|
||||||
|
def model_rebuild_recurse(cls: TBaseModel) -> TBaseModel:
|
||||||
|
"""Recursively rebuild all BaseModel subclasses in the class."""
|
||||||
|
if not PYDANTIC_V2:
|
||||||
|
from inspect import isclass, getmembers
|
||||||
|
|
||||||
|
for _, sub_cls in getmembers(cls, lambda x: isclass(x) and issubclass(x, BaseModel)):
|
||||||
|
model_rebuild_recurse(sub_cls)
|
||||||
|
model_rebuild(cls)
|
||||||
|
return cls
|
||||||
|
|
||||||
|
|
||||||
|
class Base(BaseModel):
|
||||||
|
if PYDANTIC_V2:
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
else:
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
|
class APIBase(Base):
|
||||||
|
"""Bilibili API返回的基础数据"""
|
||||||
|
|
||||||
|
code: int
|
||||||
|
message: str
|
||||||
|
|
||||||
|
|
||||||
|
class UserAPI(APIBase):
|
||||||
|
class Card(Base):
|
||||||
|
name: str
|
||||||
|
|
||||||
|
class Data(Base):
|
||||||
|
card: "UserAPI.Card"
|
||||||
|
|
||||||
|
data: Data | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class PostAPI(APIBase):
|
||||||
|
class Info(Base):
|
||||||
|
uname: str
|
||||||
|
|
||||||
|
class UserProfile(Base):
|
||||||
|
info: "PostAPI.Info"
|
||||||
|
|
||||||
|
class Origin(Base):
|
||||||
|
uid: int
|
||||||
|
dynamic_id: int
|
||||||
|
dynamic_id_str: str
|
||||||
|
timestamp: int
|
||||||
|
type: int
|
||||||
|
rid: int
|
||||||
|
bvid: str | None = None
|
||||||
|
|
||||||
|
class Desc(Base):
|
||||||
|
dynamic_id: int
|
||||||
|
dynamic_id_str: str
|
||||||
|
timestamp: int
|
||||||
|
type: int
|
||||||
|
user_profile: "PostAPI.UserProfile"
|
||||||
|
rid: int
|
||||||
|
bvid: str | None = None
|
||||||
|
|
||||||
|
origin: "PostAPI.Origin | None" = None
|
||||||
|
|
||||||
|
class Card(Base):
|
||||||
|
desc: "PostAPI.Desc"
|
||||||
|
card: str
|
||||||
|
|
||||||
|
class Data(Base):
|
||||||
|
cards: "list[PostAPI.Card] | None"
|
||||||
|
|
||||||
|
data: Data | None = None
|
||||||
|
|
||||||
|
|
||||||
|
DynRawPost: TypeAlias = PostAPI.Card
|
||||||
|
|
||||||
|
model_rebuild_recurse(UserAPI)
|
||||||
|
model_rebuild_recurse(PostAPI)
|
||||||
|
|
||||||
|
|
||||||
class BilibiliClient:
|
class BilibiliClient:
|
||||||
_client: AsyncClient
|
_client: AsyncClient
|
||||||
@ -95,10 +179,10 @@ class Bilibili(NewMessage):
|
|||||||
async def get_target_name(cls, client: AsyncClient, target: Target) -> str | None:
|
async def get_target_name(cls, client: AsyncClient, target: Target) -> str | None:
|
||||||
res = await client.get("https://api.bilibili.com/x/web-interface/card", params={"mid": target})
|
res = await client.get("https://api.bilibili.com/x/web-interface/card", params={"mid": target})
|
||||||
res.raise_for_status()
|
res.raise_for_status()
|
||||||
res_data = res.json()
|
res_data = type_validate_json(UserAPI, res.content)
|
||||||
if res_data["code"]:
|
if res_data.code != 0:
|
||||||
return None
|
return None
|
||||||
return res_data["data"]["card"]["name"]
|
return res_data.data.card.name if res_data.data else None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def parse_target(cls, target_text: str) -> Target:
|
async def parse_target(cls, target_text: str) -> Target:
|
||||||
@ -109,7 +193,7 @@ class Bilibili(NewMessage):
|
|||||||
else:
|
else:
|
||||||
raise cls.ParseTargetException()
|
raise cls.ParseTargetException()
|
||||||
|
|
||||||
async def get_sub_list(self, target: Target) -> list[RawPost]:
|
async def get_sub_list(self, target: Target) -> list[DynRawPost]:
|
||||||
params = {"host_uid": target, "offset": 0, "need_top": 0}
|
params = {"host_uid": target, "offset": 0, "need_top": 0}
|
||||||
res = await self.client.get(
|
res = await self.client.get(
|
||||||
"https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/space_history",
|
"https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/space_history",
|
||||||
@ -117,42 +201,45 @@ class Bilibili(NewMessage):
|
|||||||
timeout=4.0,
|
timeout=4.0,
|
||||||
)
|
)
|
||||||
res.raise_for_status()
|
res.raise_for_status()
|
||||||
res_dict = res.json()
|
res_obj = type_validate_json(PostAPI, res.content)
|
||||||
if res_dict["code"] == 0:
|
|
||||||
return res_dict["data"].get("cards", [])
|
|
||||||
else:
|
|
||||||
raise ApiError(res.request.url)
|
|
||||||
|
|
||||||
def get_id(self, post: RawPost) -> Any:
|
if res_obj.code == 0:
|
||||||
return post["desc"]["dynamic_id"]
|
if (data := res_obj.data) and (card := data.cards):
|
||||||
|
return card
|
||||||
|
return []
|
||||||
|
raise ApiError(res.request.url)
|
||||||
|
|
||||||
def get_date(self, post: RawPost) -> int:
|
def get_id(self, post: DynRawPost) -> int:
|
||||||
return post["desc"]["timestamp"]
|
return post.desc.dynamic_id
|
||||||
|
|
||||||
|
def get_date(self, post: DynRawPost) -> int:
|
||||||
|
return post.desc.timestamp
|
||||||
|
|
||||||
def _do_get_category(self, post_type: int) -> Category:
|
def _do_get_category(self, post_type: int) -> Category:
|
||||||
if post_type == 2:
|
match post_type:
|
||||||
return Category(1)
|
case 2:
|
||||||
elif post_type == 64:
|
return Category(1)
|
||||||
return Category(2)
|
case 64:
|
||||||
elif post_type == 8:
|
return Category(2)
|
||||||
return Category(3)
|
case 8:
|
||||||
elif post_type == 4:
|
return Category(3)
|
||||||
return Category(4)
|
case 4:
|
||||||
elif post_type == 1:
|
return Category(4)
|
||||||
# 转发
|
case 1:
|
||||||
return Category(5)
|
# 转发
|
||||||
raise CategoryNotRecognize(post_type)
|
return Category(5)
|
||||||
|
case unknown_type:
|
||||||
|
raise CategoryNotRecognize(unknown_type)
|
||||||
|
|
||||||
def get_category(self, post: RawPost) -> Category:
|
def get_category(self, post: DynRawPost) -> Category:
|
||||||
post_type = post["desc"]["type"]
|
post_type = post.desc.type
|
||||||
return self._do_get_category(post_type)
|
return self._do_get_category(post_type)
|
||||||
|
|
||||||
def get_tags(self, raw_post: RawPost) -> list[Tag]:
|
def get_tags(self, raw_post: DynRawPost) -> list[Tag]:
|
||||||
# FIXME: 更深的原因可能是返回格式的变动,需要进一步确认
|
card_content = json.loads(raw_post.card)
|
||||||
if topic_info := raw_post["display"].get("topic_info"):
|
text: str = card_content["item"]["content"]
|
||||||
return [tp["topic_name"] for tp in topic_info["topic_details"]]
|
result: list[str] = re.findall(r"#(.*?)#", text)
|
||||||
|
return result
|
||||||
return []
|
|
||||||
|
|
||||||
def _text_process(self, dynamic: str, desc: str, title: str) -> str:
|
def _text_process(self, dynamic: str, desc: str, title: str) -> str:
|
||||||
similarity = 1.0 if len(dynamic) == 0 or len(desc) == 0 else text_similarity(dynamic, desc)
|
similarity = 1.0 if len(dynamic) == 0 or len(desc) == 0 else text_similarity(dynamic, desc)
|
||||||
@ -164,61 +251,85 @@ class Bilibili(NewMessage):
|
|||||||
text = dynamic + "\n=================\n" + title + "\n\n" + desc
|
text = dynamic + "\n=================\n" + title + "\n\n" + desc
|
||||||
return text
|
return text
|
||||||
|
|
||||||
def _get_info(self, post_type: Category, card) -> tuple[str, list]:
|
def _raw_post_parse(self, raw_post: DynRawPost, in_repost: bool = False):
|
||||||
if post_type == 1:
|
class ParsedPost(NamedTuple):
|
||||||
# 一般动态
|
text: str
|
||||||
text = card["item"]["description"]
|
pics: list[str]
|
||||||
pic = [img["img_src"] for img in card["item"]["pictures"]]
|
url: str | None
|
||||||
elif post_type == 2:
|
repost_owner: str | None = None
|
||||||
# 专栏文章
|
repost: "ParsedPost | None" = None
|
||||||
text = "{} {}".format(card["title"], card["summary"])
|
|
||||||
pic = card["image_urls"]
|
|
||||||
elif post_type == 3:
|
|
||||||
# 视频
|
|
||||||
dynamic = card.get("dynamic", "")
|
|
||||||
title = card["title"]
|
|
||||||
desc = card.get("desc", "")
|
|
||||||
text = self._text_process(dynamic, desc, title)
|
|
||||||
pic = [card["pic"]]
|
|
||||||
elif post_type == 4:
|
|
||||||
# 纯文字
|
|
||||||
text = card["item"]["content"]
|
|
||||||
pic = []
|
|
||||||
else:
|
|
||||||
raise CategoryNotSupport(post_type)
|
|
||||||
return text, pic
|
|
||||||
|
|
||||||
async def parse(self, raw_post: RawPost) -> Post:
|
card_content: dict[str, Any] = json.loads(raw_post.card)
|
||||||
card_content = json.loads(raw_post["card"])
|
repost_owner: str | None = ou["info"]["uname"] if (ou := card_content.get("origin_user")) else None
|
||||||
post_type = self.get_category(raw_post)
|
|
||||||
target_name = raw_post["desc"]["user_profile"]["info"]["uname"]
|
def extract_url_id(url_template: str, name: str) -> str | None:
|
||||||
if post_type >= 1 and post_type < 5:
|
if in_repost:
|
||||||
url = ""
|
if origin := raw_post.desc.origin:
|
||||||
if post_type == 1:
|
return url_template.format(getattr(origin, name))
|
||||||
|
return None
|
||||||
|
return url_template.format(getattr(raw_post.desc, name))
|
||||||
|
|
||||||
|
match self._do_get_category(raw_post.desc.type):
|
||||||
|
case 1:
|
||||||
# 一般动态
|
# 一般动态
|
||||||
url = "https://t.bilibili.com/{}".format(raw_post["desc"]["dynamic_id_str"])
|
url = extract_url_id("https://t.bilibili.com/{}", "dynamic_id_str")
|
||||||
elif post_type == 2:
|
text: str = card_content["item"]["description"]
|
||||||
|
pic: list[str] = [img["img_src"] for img in card_content["item"]["pictures"]]
|
||||||
|
return ParsedPost(text, pic, url, repost_owner)
|
||||||
|
case 2:
|
||||||
# 专栏文章
|
# 专栏文章
|
||||||
url = "https://www.bilibili.com/read/cv{}".format(raw_post["desc"]["rid"])
|
url = extract_url_id("https://www.bilibili.com/read/cv{}", "rid")
|
||||||
elif post_type == 3:
|
text = "{} {}".format(card_content["title"], card_content["summary"])
|
||||||
|
pic = card_content["image_urls"]
|
||||||
|
return ParsedPost(text, pic, url, repost_owner)
|
||||||
|
case 3:
|
||||||
# 视频
|
# 视频
|
||||||
url = "https://www.bilibili.com/video/{}".format(raw_post["desc"]["bvid"])
|
url = extract_url_id("https://www.bilibili.com/video/{}", "bvid")
|
||||||
elif post_type == 4:
|
dynamic = card_content.get("dynamic", "")
|
||||||
|
title = card_content["title"]
|
||||||
|
desc = card_content.get("desc", "")
|
||||||
|
text = self._text_process(dynamic, desc, title)
|
||||||
|
pic = [card_content["pic"]]
|
||||||
|
return ParsedPost(text, pic, url, repost_owner)
|
||||||
|
case 4:
|
||||||
# 纯文字
|
# 纯文字
|
||||||
url = "https://t.bilibili.com/{}".format(raw_post["desc"]["dynamic_id_str"])
|
url = extract_url_id("https://t.bilibili.com/{}", "dynamic_id_str")
|
||||||
text, pic = self._get_info(post_type, card_content)
|
text = card_content["item"]["content"]
|
||||||
elif post_type == 5:
|
pic = []
|
||||||
# 转发
|
return ParsedPost(text, pic, url, repost_owner)
|
||||||
url = "https://t.bilibili.com/{}".format(raw_post["desc"]["dynamic_id_str"])
|
case 5:
|
||||||
text = card_content["item"]["content"]
|
# 转发
|
||||||
orig_type = card_content["item"]["orig_type"]
|
url = extract_url_id("https://t.bilibili.com/{}", "dynamic_id_str")
|
||||||
orig = json.loads(card_content["origin"])
|
text = card_content["item"]["content"]
|
||||||
orig_text, pic = self._get_info(self._do_get_category(orig_type), orig)
|
orig_type: int = card_content["item"]["orig_type"]
|
||||||
text += "\n--------------\n"
|
orig_card: str = card_content["origin"]
|
||||||
text += orig_text
|
orig_post = DynRawPost(desc=raw_post.desc, card=orig_card)
|
||||||
else:
|
orig_post.desc.type = orig_type
|
||||||
raise CategoryNotSupport(post_type)
|
|
||||||
return Post(self, text, url=url, images=pic, nickname=target_name)
|
orig_parsed_post = self._raw_post_parse(orig_post, in_repost=True)
|
||||||
|
return ParsedPost(text, [], url, repost_owner, orig_parsed_post)
|
||||||
|
case unsupported_type:
|
||||||
|
raise CategoryNotSupport(unsupported_type)
|
||||||
|
|
||||||
|
async def parse(self, raw_post: DynRawPost) -> Post:
|
||||||
|
parsed_raw_post = self._raw_post_parse(raw_post)
|
||||||
|
|
||||||
|
post = Post(
|
||||||
|
self,
|
||||||
|
parsed_raw_post.text,
|
||||||
|
url=parsed_raw_post.url,
|
||||||
|
images=list(parsed_raw_post.pics),
|
||||||
|
nickname=raw_post.desc.user_profile.info.uname,
|
||||||
|
)
|
||||||
|
if rp := parsed_raw_post.repost:
|
||||||
|
post.repost = Post(
|
||||||
|
self,
|
||||||
|
rp.text,
|
||||||
|
url=rp.url,
|
||||||
|
images=list(rp.pics),
|
||||||
|
nickname=rp.repost_owner,
|
||||||
|
)
|
||||||
|
return post
|
||||||
|
|
||||||
|
|
||||||
class Bilibililive(StatusChange):
|
class Bilibililive(StatusChange):
|
||||||
|
@ -130,7 +130,7 @@ class Weibo(NewMessage):
|
|||||||
|
|
||||||
def _get_text(self, raw_text: str) -> str:
|
def _get_text(self, raw_text: str) -> str:
|
||||||
text = raw_text.replace("<br/>", "\n").replace("<br />", "\n")
|
text = raw_text.replace("<br/>", "\n").replace("<br />", "\n")
|
||||||
selector = etree.HTML(text)
|
selector = etree.HTML(text, parser=None)
|
||||||
if selector is None:
|
if selector is None:
|
||||||
return text
|
return text
|
||||||
url_elems = selector.xpath("//a[@href]/span[@class='surl-text']")
|
url_elems = selector.xpath("//a[@href]/span[@class='surl-text']")
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"code": 0,
|
"code": 0,
|
||||||
"msg": "",
|
"ttl": 0,
|
||||||
"message": "",
|
"message": "",
|
||||||
"data": {
|
"data": {
|
||||||
"has_more": 1,
|
"has_more": 1,
|
||||||
|
246
tests/platforms/static/bilibili_fake_dy_list.json
vendored
246
tests/platforms/static/bilibili_fake_dy_list.json
vendored
@ -1,246 +0,0 @@
|
|||||||
{
|
|
||||||
"data": {
|
|
||||||
"cards": [
|
|
||||||
{
|
|
||||||
"desc": {
|
|
||||||
"type": 2
|
|
||||||
},
|
|
||||||
"display": {
|
|
||||||
"topic_info": {
|
|
||||||
"topic_details": [
|
|
||||||
{
|
|
||||||
"topic_name": ""
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"desc": {
|
|
||||||
"type": 1
|
|
||||||
},
|
|
||||||
"display": {
|
|
||||||
"topic_info": {
|
|
||||||
"topic_details": [
|
|
||||||
{
|
|
||||||
"topic_name": ""
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"desc": {
|
|
||||||
"type": 8
|
|
||||||
},
|
|
||||||
"display": {
|
|
||||||
"topic_info": {
|
|
||||||
"topic_details": [
|
|
||||||
{
|
|
||||||
"topic_name": "明日方舟"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"topic_name": "风笛"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"topic_name": "琴柳"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"topic_name": "风暴瞭望"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"topic_name": "轮换池"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"topic_name": "打卡挑战"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"desc": {
|
|
||||||
"type": 2
|
|
||||||
},
|
|
||||||
"display": {
|
|
||||||
"topic_info": {
|
|
||||||
"topic_details": [
|
|
||||||
{
|
|
||||||
"topic_name": ""
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"desc": {
|
|
||||||
"type": 1
|
|
||||||
},
|
|
||||||
"display": {
|
|
||||||
"topic_info": {
|
|
||||||
"topic_details": [
|
|
||||||
{
|
|
||||||
"topic_name": "明日方舟"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"topic_name": "饼学大厦"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"topic_name": "可露希尔的秘密档案"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"desc": {
|
|
||||||
"type": 1
|
|
||||||
},
|
|
||||||
"display": {
|
|
||||||
"topic_info": {
|
|
||||||
"topic_details": [
|
|
||||||
{
|
|
||||||
"topic_name": "罗德岛相簿"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"topic_name": "可露希尔的秘密档案"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"topic_name": "罗德岛闲逛部"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"desc": {
|
|
||||||
"type": 8
|
|
||||||
},
|
|
||||||
"display": {
|
|
||||||
"topic_info": {
|
|
||||||
"topic_details": [
|
|
||||||
{
|
|
||||||
"topic_name": "明日方舟"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"topic_name": "轮换学"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"topic_name": "常驻标准寻访"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"topic_name": "轮换池"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"topic_name": "打卡挑战"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"topic_name": "舟游"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"desc": {
|
|
||||||
"type": 2
|
|
||||||
},
|
|
||||||
"display": {
|
|
||||||
"topic_info": {
|
|
||||||
"topic_details": [
|
|
||||||
{
|
|
||||||
"topic_name": "明日方舟"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"topic_name": "饼学大厦"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"desc": {
|
|
||||||
"type": 4
|
|
||||||
},
|
|
||||||
"display": {
|
|
||||||
"topic_info": {
|
|
||||||
"topic_details": [
|
|
||||||
{
|
|
||||||
"topic_name": ""
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"desc": {
|
|
||||||
"type": 1
|
|
||||||
},
|
|
||||||
"display": {
|
|
||||||
"topic_info": {
|
|
||||||
"topic_details": [
|
|
||||||
{
|
|
||||||
"topic_name": "明日方舟"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"topic_name": "饼学大厦"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"topic_name": "罗德岛相簿"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"topic_name": "可露希尔的秘密档案"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"topic_name": "罗德岛闲逛部"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"desc": {
|
|
||||||
"type": 1
|
|
||||||
},
|
|
||||||
"display": {
|
|
||||||
"topic_info": {
|
|
||||||
"topic_details": [
|
|
||||||
{
|
|
||||||
"topic_name": "明日方舟"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"topic_name": "饼学大厦"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"desc": {
|
|
||||||
"type": 1
|
|
||||||
},
|
|
||||||
"display": {
|
|
||||||
"topic_info": {
|
|
||||||
"topic_details": [
|
|
||||||
{
|
|
||||||
"topic_name": "明日方舟"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"topic_name": "饼学大厦"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"topic_name": "罗德岛相簿"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"topic_name": "可露希尔的秘密档案"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"topic_name": "罗德岛闲逛部"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"code": 0,
|
"code": 0,
|
||||||
"msg": "",
|
"ttl": 0,
|
||||||
"message": "",
|
"message": "",
|
||||||
"data": {
|
"data": {
|
||||||
"has_more": 1,
|
"has_more": 1,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"code": 0,
|
"code": 0,
|
||||||
"msg": "",
|
"ttl": 0,
|
||||||
"message": "",
|
"message": "",
|
||||||
"data": {
|
"data": {
|
||||||
"has_more": 1,
|
"has_more": 1,
|
||||||
|
@ -5,13 +5,16 @@ import respx
|
|||||||
import pytest
|
import pytest
|
||||||
from nonebug.app import App
|
from nonebug.app import App
|
||||||
from httpx import Response, AsyncClient
|
from httpx import Response, AsyncClient
|
||||||
|
from nonebot.compat import model_dump, type_validate_python
|
||||||
|
|
||||||
from .utils import get_json
|
from .utils import get_json
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture()
|
||||||
def bing_dy_list():
|
def bing_dy_list(app: App):
|
||||||
return get_json("bilibili_bing_list.json")["data"]["cards"]
|
from nonebot_bison.platform.bilibili import PostAPI
|
||||||
|
|
||||||
|
return type_validate_python(PostAPI, get_json("bilibili_bing_list.json")).data.cards # type: ignore
|
||||||
|
|
||||||
|
|
||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
@ -19,69 +22,73 @@ if typing.TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def bilibili(app: App):
|
def bilibili(app: App) -> "Bilibili":
|
||||||
from nonebot_bison.utils import ProcessContext
|
from nonebot_bison.utils import ProcessContext
|
||||||
from nonebot_bison.platform import platform_manager
|
from nonebot_bison.platform import platform_manager
|
||||||
|
|
||||||
return platform_manager["bilibili"](ProcessContext(), AsyncClient())
|
return platform_manager["bilibili"](ProcessContext(), AsyncClient()) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def without_dynamic(app: App):
|
def without_dynamic(app: App):
|
||||||
return {
|
from nonebot_bison.platform.bilibili import PostAPI
|
||||||
"code": 0,
|
|
||||||
"msg": "",
|
# 先验证实际的空动态返回能否通过校验,再重新导出
|
||||||
"message": "",
|
return model_dump(
|
||||||
"data": {
|
type_validate_python(
|
||||||
"has_more": 0,
|
PostAPI,
|
||||||
"next_offset": 0,
|
{
|
||||||
"_gt_": 0,
|
"code": 0,
|
||||||
},
|
"ttl": 1,
|
||||||
}
|
"message": "",
|
||||||
|
"data": {
|
||||||
|
"cards": None,
|
||||||
|
"has_more": 0,
|
||||||
|
"next_offset": 0,
|
||||||
|
"_gt_": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_tag_without_topic_info(bilibili, bing_dy_list):
|
async def test_get_tag(bilibili: "Bilibili", bing_dy_list):
|
||||||
simple_raw_post = {
|
from nonebot_bison.platform.bilibili import DynRawPost
|
||||||
"display": {
|
|
||||||
"topic_info": {
|
|
||||||
"topic_details": [
|
|
||||||
{
|
|
||||||
"topic_name": "可露希尔的秘密档案",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"topic_name": "罗德岛相簿",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
simple_raw_post_without_topic_info = {
|
raw_post_has_tag = type_validate_python(DynRawPost, bing_dy_list[0])
|
||||||
"display": {
|
raw_post_has_tag.card = '{"user":{"uid":111111,"uname":"1111","face":"https://i2.hdslb.com/bfs/face/0b.jpg"},"item":{"rp_id":11111,"uid":31111,"content":"#测试1#\\n测试\\n#测试2#\\n#测\\n测\\n测#","ctrl":"","reply":0}}'
|
||||||
"damedane": "dameyo",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
res1 = bilibili.get_tags(simple_raw_post)
|
raw_post_has_no_tag = type_validate_python(DynRawPost, bing_dy_list[1])
|
||||||
assert res1 == ["可露希尔的秘密档案", "罗德岛相簿"]
|
raw_post_has_no_tag.card = '{"user":{"uid":111111,"uname":"1111","face":"https://i2.hdslb.com/bfs/face/0b.jpg"},"item":{"rp_id":11111,"uid":31111,"content":"测试1\\n测试\\n测试2\\n#测\\n测\\n测#","ctrl":"","reply":0}}'
|
||||||
|
|
||||||
res2 = bilibili.get_tags(simple_raw_post_without_topic_info)
|
res1 = bilibili.get_tags(raw_post_has_tag)
|
||||||
|
assert res1 == ["测试1", "测试2"]
|
||||||
|
|
||||||
|
res2 = bilibili.get_tags(raw_post_has_no_tag)
|
||||||
assert res2 == []
|
assert res2 == []
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_video_forward(bilibili, bing_dy_list):
|
async def test_video_forward(bilibili, bing_dy_list):
|
||||||
from nonebot_bison.post import Post
|
from nonebot_bison.post import Post
|
||||||
|
|
||||||
post: Post = await bilibili.parse(bing_dy_list[1])
|
post: Post = await bilibili.parse(bing_dy_list[1])
|
||||||
assert (
|
assert post.content == """答案揭晓:宿舍!来看看投票结果\nhttps://t.bilibili.com/568093580488553786"""
|
||||||
post.content
|
assert post.repost is not None
|
||||||
== "答案揭晓:宿舍!来看看投票结果\nhttps://t.bilibili.com/568093580488553786\n--------------\n#可露希尔的秘密档案#"
|
# 注意原文前几行末尾是有空格的
|
||||||
" \n11:来宿舍休息一下吧 \n档案来源:lambda:\\罗德岛内务\\秘密档案 \n发布时间:9/12 1:00 P.M."
|
assert post.repost.content == (
|
||||||
" \n档案类型:可见 \n档案描述:今天请了病假在宿舍休息。很舒适。"
|
"#可露希尔的秘密档案# \n"
|
||||||
" \n提供者:赫默\n=================\n《可露希尔的秘密档案》11话:来宿舍休息一下吧"
|
"11:来宿舍休息一下吧 \n"
|
||||||
|
"档案来源:lambda:\\罗德岛内务\\秘密档案 \n"
|
||||||
|
"发布时间:9/12 1:00 P.M. \n"
|
||||||
|
"档案类型:可见 \n"
|
||||||
|
"档案描述:今天请了病假在宿舍休息。很舒适。 \n"
|
||||||
|
"提供者:赫默\n"
|
||||||
|
"=================\n"
|
||||||
|
"《可露希尔的秘密档案》11话:来宿舍休息一下吧"
|
||||||
)
|
)
|
||||||
|
assert post.url == "https://t.bilibili.com/569448354910819194"
|
||||||
|
assert post.repost.url == "https://www.bilibili.com/video/BV1E3411q7nU"
|
||||||
assert post.get_priority_themes()[0] == "basic"
|
assert post.get_priority_themes()[0] == "basic"
|
||||||
|
|
||||||
|
|
||||||
@ -99,40 +106,61 @@ async def test_video_forward_without_dynamic(bilibili, bing_dy_list):
|
|||||||
"包含慕夏对新PV的个人解读,风笛厨力疯狂放出,CP言论输出,9.16轮换池预测视频分析和理智规划杂谈内容。"
|
"包含慕夏对新PV的个人解读,风笛厨力疯狂放出,CP言论输出,9.16轮换池预测视频分析和理智规划杂谈内容。"
|
||||||
"\n注意:内含大量个人性质对风笛的厨力观点,与多CP混乱发言,不适者请及时点击退出或跳到下一片段。"
|
"\n注意:内含大量个人性质对风笛的厨力观点,与多CP混乱发言,不适者请及时点击退出或跳到下一片段。"
|
||||||
)
|
)
|
||||||
|
assert post.repost is None
|
||||||
|
assert post.url == "https://www.bilibili.com/video/BV1K44y1h7Xg"
|
||||||
assert post.get_priority_themes()[0] == "basic"
|
assert post.get_priority_themes()[0] == "basic"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_article_forward(bilibili, bing_dy_list):
|
async def test_article_forward(bilibili: "Bilibili", bing_dy_list):
|
||||||
post = await bilibili.parse(bing_dy_list[4])
|
post = await bilibili.parse(bing_dy_list[4])
|
||||||
assert (
|
assert post.content == (
|
||||||
post.content == "#明日方舟##饼学大厦#\n9.11专栏更新完毕,"
|
"#明日方舟##饼学大厦#\n"
|
||||||
"这还塌了实属没跟新运营对上\n后边除了周日发饼和PV没提及的中文语音,"
|
"9.11专栏更新完毕,这还塌了实属没跟新运营对上\n"
|
||||||
"稳了\n别忘了来参加#可露希尔的秘密档案#的主题投票\nhttps://t.bilibili.com/568093580488553786?tab=2"
|
"后边除了周日发饼和PV没提及的中文语音,稳了\n"
|
||||||
+ "\n--------------\n"
|
"别忘了来参加#可露希尔的秘密档案#的主题投票\n"
|
||||||
+ "【明日方舟】饼学大厦#12~14(风暴瞭望&玛莉娅·临光&红松林&感谢庆典)9.11更新"
|
"https://t.bilibili.com/568093580488553786?tab=2"
|
||||||
" 更新记录09.11更新:覆盖09.10更新;以及排期更新,猜测周一周五开活动09.10更新:以周五开活动为底,"
|
|
||||||
"PV/公告调整位置,整体结构更新09.08更新:饼学大厦#12更新,"
|
|
||||||
"新增一件六星商店服饰(周日发饼)09.06更新:饼学大厦整栋整栋翻新,"
|
|
||||||
"改为9.16开主线(四日无饼!)09.05凌晨更新:10.13后的排期(两日无饼,鹰角背刺,"
|
|
||||||
"心狠手辣)前言感谢楪筱祈ぺ的动态-哔哩哔哩 (bilibili.com) 对饼学的贡献!后续排期:9.17【风暴瞭望】、"
|
|
||||||
"10.01【玛莉娅·临光】复刻、10.1"
|
|
||||||
)
|
)
|
||||||
|
assert post.repost is not None
|
||||||
|
assert post.repost.content == (
|
||||||
|
"【明日方舟】饼学大厦#12~14(风暴瞭望&玛莉娅·临光&红松林&感谢庆典)"
|
||||||
|
"9.11更新 更新记录09.11更新:覆盖09.10更新;以及排期更新,猜测周一周五开活动"
|
||||||
|
"09.10更新:以周五开活动为底,PV/公告调整位置,整体结构更新"
|
||||||
|
"09.08更新:饼学大厦#12更新,新增一件六星商店服饰(周日发饼)"
|
||||||
|
"09.06更新:饼学大厦整栋整栋翻新,改为9.16开主线(四日无饼!)"
|
||||||
|
"09.05凌晨更新:10.13后的排期(两日无饼,鹰角背刺,心狠手辣)"
|
||||||
|
"前言感谢楪筱祈ぺ的动态-哔哩哔哩 (bilibili.com) 对饼学的贡献!"
|
||||||
|
"后续排期:9.17【风暴瞭望】、10.01【玛莉娅·临光】复刻、10.1"
|
||||||
|
)
|
||||||
|
assert post.url == "https://t.bilibili.com/569189870889648693"
|
||||||
|
assert post.repost.url == "https://www.bilibili.com/read/cv12993752"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_dynamic_forward(bilibili, bing_dy_list):
|
async def test_dynamic_forward(bilibili, bing_dy_list):
|
||||||
post = await bilibili.parse(bing_dy_list[5])
|
post = await bilibili.parse(bing_dy_list[5])
|
||||||
assert (
|
assert post.content == (
|
||||||
post.content == "饼组主线饼学预测——9.11版\n①今日结果\n9.11 殿堂上的游禽-星极(x,"
|
"饼组主线饼学预测——9.11版\n"
|
||||||
"新运营实锤了)\n②后续预测\n9.12 #罗德岛相簿#+#可露希尔的秘密档案#11话\n9.13"
|
"①今日结果\n"
|
||||||
" 六星先锋(执旗手)干员-琴柳\n9.14 宣传策略-空弦+家具\n9.15 轮换池(+中文语音前瞻)\n9.16"
|
"9.11 殿堂上的游禽-星极(x,新运营实锤了)\n"
|
||||||
" 停机\n9.17 #罗德岛闲逛部#+新六星EP+EP09·风暴瞭望开启\n9.19 #罗德岛相簿#"
|
"②后续预测\n"
|
||||||
+ "\n--------------\n"
|
"9.12 #罗德岛相簿#+#可露希尔的秘密档案#11话\n"
|
||||||
+ "#明日方舟#\n【新增服饰】\n//殿堂上的游禽 - 星极\n塞壬唱片偶像企划《闪耀阶梯》特供服饰/殿堂上的游禽。"
|
"9.13 六星先锋(执旗手)干员-琴柳\n9.14 宣传策略-空弦+家具\n"
|
||||||
"星极自费参加了这项企划,尝试着用大众能接受的方式演绎天空之上的故事。\n\n_____________\n谦逊留给观众,"
|
"9.15 轮换池(+中文语音前瞻)\n"
|
||||||
"骄傲发自歌喉,此夜,唯我璀璨。 "
|
"9.16 停机\n"
|
||||||
|
"9.17 #罗德岛闲逛部#+新六星EP+EP09·风暴瞭望开启\n"
|
||||||
|
"9.19 #罗德岛相簿#"
|
||||||
)
|
)
|
||||||
|
assert post.repost.content == (
|
||||||
|
"#明日方舟#\n"
|
||||||
|
"【新增服饰】\n"
|
||||||
|
"//殿堂上的游禽 - 星极\n"
|
||||||
|
"塞壬唱片偶像企划《闪耀阶梯》特供服饰/殿堂上的游禽。星极自费参加了这项企划,尝试着用大众能接受的方式演绎天空之上的故事。\n\n"
|
||||||
|
"_____________\n"
|
||||||
|
"谦逊留给观众,骄傲发自歌喉,此夜,唯我璀璨。 "
|
||||||
|
)
|
||||||
|
assert post.url == "https://t.bilibili.com/569107343093484983"
|
||||||
|
assert post.repost.url == "https://t.bilibili.com/569105539209306328"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@ -196,32 +224,3 @@ async def test_parse_target(bilibili: "Bilibili"):
|
|||||||
assert res2 == "161775300"
|
assert res2 == "161775300"
|
||||||
with pytest.raises(Platform.ParseTargetException):
|
with pytest.raises(Platform.ParseTargetException):
|
||||||
await bilibili.parse_target("https://www.bilibili.com/video/BV1qP4y1g738?spm_id_from=333.999.0.0")
|
await bilibili.parse_target("https://www.bilibili.com/video/BV1qP4y1g738?spm_id_from=333.999.0.0")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
|
||||||
def post_list():
|
|
||||||
return get_json("bilibili_fake_dy_list.json")["data"]["cards"]
|
|
||||||
|
|
||||||
|
|
||||||
# 测试新tag机制的平台推送情况
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_filter_user_custom(bilibili, post_list):
|
|
||||||
only_banned_tags = ["~可露希尔的秘密档案"]
|
|
||||||
res0 = await bilibili.filter_user_custom(post_list, [], only_banned_tags)
|
|
||||||
assert len(res0) == 8
|
|
||||||
|
|
||||||
only_subscribed_tags = ["可露希尔的秘密档案"]
|
|
||||||
res1 = await bilibili.filter_user_custom(post_list, [], only_subscribed_tags)
|
|
||||||
assert len(res1) == 4
|
|
||||||
|
|
||||||
multi_subs_tags_1 = ["可露希尔的秘密档案", "罗德岛相簿"]
|
|
||||||
res2 = await bilibili.filter_user_custom(post_list, [], multi_subs_tags_1)
|
|
||||||
assert len(res2) == 4
|
|
||||||
|
|
||||||
multi_subs_tags_2 = ["罗德岛相簿", "风暴瞭望"]
|
|
||||||
res3 = await bilibili.filter_user_custom(post_list, [], multi_subs_tags_2)
|
|
||||||
assert len(res3) == 4
|
|
||||||
|
|
||||||
multi_subs_tags_3 = ["明日方舟", "~饼学大厦"]
|
|
||||||
res4 = await bilibili.filter_user_custom(post_list, [], multi_subs_tags_3)
|
|
||||||
assert len(res4) == 2
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user