from typing import Any, Literal, TypeVar, TypeAlias from pydantic import BaseModel from nonebot.compat import PYDANTIC_V2, ConfigDict from nonebot_bison.compat import model_rebuild 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 DynamicType = Literal[ "DYNAMIC_TYPE_ARTICLE", "DYNAMIC_TYPE_AV", "DYNAMIC_TYPE_WORD", "DYNAMIC_TYPE_DRAW", "DYNAMIC_TYPE_FORWARD", "DYNAMIC_TYPE_LIVE", "DYNAMIC_TYPE_LIVE_RCMD", "DYNAMIC_TYPE_PGC", "DYNAMIC_TYPE_PGC_UNION", "DYNAMIC_TYPE_NONE", # 已删除的动态,一般只会出现在转发动态的源动态被删除 "DYNAMIC_TYPE_COMMON_SQUARE", "DYNAMIC_TYPE_COMMON_VERTICAL", "DYNAMIC_TYPE_COURSES_SEASON", ] # 参考 https://github.com/Yun-Shan/bilibili-dynamic class PostAPI(APIBase): class Basic(Base): rid_str: str """可能含义是referrer id,表示引用的对象的ID? 已知专栏动态时该ID与专栏ID一致,视频动态时与av号一致 """ class Modules(Base): class Author(Base): face: str mid: int name: str jump_url: str pub_ts: int type: Literal["AUTHOR_TYPE_NORMAL", "AUTHOR_TYPE_PGC"] """作者类型,一般情况下都是NORMAL,番剧推送是PGC""" class Additional(Base): type: str """用户发视频时同步发布的动态带图片: ADDITIONAL_TYPE_UGC 显示相关游戏: ADDITIONAL_TYPE_COMMON 显示预约: ADDITIONAL_TYPE_RESERVE 显示投票: ADDITIONAL_TYPE_VOTE 显示包月充电专属抽奖: ADDITIONAL_TYPE_UPOWER_LOTTERY 显示赛事时(暂时只看到回放的,理论上直播时应该也是这个): ADDITIONAL_TYPE_MATCH """ class Desc(Base): rich_text_nodes: list[dict[str, Any]] """描述的富文本节点,组成动态的各种内容 一个可能通用的结构: ```json [ { "jump_url": "//search.bilibili.com/all?keyword=鸣潮公测定档", "orig_text": "#鸣潮公测定档#", "text": "#鸣潮公测定档#", "type": "RICH_TEXT_NODE_TYPE_TOPIC" }, //... ] ``` """ text: str """描述的纯文本内容""" class Dynamic(Base): additional: "PostAPI.Modules.Additional | None" = None desc: "PostAPI.Modules.Desc | None" = None """动态描述,可能为空""" major: "Major | None" = None """主要内容,可能为空""" module_author: "PostAPI.Modules.Author" module_dynamic: "PostAPI.Modules.Dynamic" class Topic(Base): id: int name: str jump_url: str class Item(Base): basic: "PostAPI.Basic" id_str: str modules: "PostAPI.Modules" orig: "PostAPI.Item | PostAPI.DeletedItem | None" = None topic: "PostAPI.Topic | None" = None type: DynamicType class DeletedItem(Base): basic: "PostAPI.Basic" id_str: None modules: "PostAPI.Modules" type: Literal["DYNAMIC_TYPE_NONE"] def to_item(self) -> "PostAPI.Item": return PostAPI.Item( basic=self.basic, id_str="", modules=self.modules, type=self.type, ) class Data(Base): items: "list[PostAPI.Item | PostAPI.DeletedItem] | None" = None data: "PostAPI.Data | None" = None class VideoMajor(Base): class Archive(Base): aid: str bvid: str title: str desc: str """视频简介,太长的话会被截断""" cover: str jump_url: str type: Literal["MAJOR_TYPE_ARCHIVE"] archive: "VideoMajor.Archive" class LiveRecommendMajor(Base): class LiveRecommand(Base): content: str """直播卡片的内容,值为JSON文本,可以解析为LiveRecommendMajor.Content""" class Content(Base): type: int """直播类型""" live_play_info: "LiveRecommendMajor.LivePlayInfo" class LivePlayInfo(Base): uid: int """用户UID,不是直播间号""" room_type: int """房间类型""" room_paid_type: int """付费类型""" play_type: int """播放类型?""" live_status: int """直播状态""" live_screen_type: int """直播画面类型?""" room_id: int """直播间号""" cover: str """直播封面""" title: str online: int """开播时长?""" parent_area_id: int """主分区ID""" parent_area_name: str """主分区名称""" area_id: int """分区ID""" area_name: str """分区名称""" live_start_time: int """开播时间戳""" link: str """跳转链接,相对协议(即//开头而不是https://开头)""" live_id: str """直播ID,不知道有什么用""" watched_show: "LiveRecommendMajor.WatchedShow" class WatchedShow(Base): num: int """观看人数""" text_small: str """观看人数的文本描述: 例如 1.2万""" text_large: str """观看人数的文本描述: 例如 1.2万人看过""" switch: bool """未知""" icon: str """观看文本前的图标""" icon_web: str """观看文本前的图标(网页版)""" icon_location: str """图标位置?""" type: Literal["MAJOR_TYPE_LIVE_RCMD"] live_rcmd: "LiveRecommendMajor.LiveRecommand" class LiveMajor(Base): class Live(Base): id: int """直播间号""" title: str live_state: int """直播状态,1为直播中,0为未开播""" cover: str desc_first: str """直播信息的第一部分,用来显示分区""" desc_second: str jump_url: str """跳转链接,目前用的是相对协议(即//开头而不是https://开头)""" type: Literal["MAJOR_TYPE_LIVE"] live: "LiveMajor.Live" class ArticleMajor(Base): class Article(Base): id: int """专栏CID""" title: str desc: str """专栏简介""" covers: list[str] """专栏封面,一般是一张图片""" jump_url: str type: Literal["MAJOR_TYPE_ARTICLE"] article: "ArticleMajor.Article" class DrawMajor(Base): class Item(Base): width: int height: int size: float """文件大小,KiB(1024)""" src: str """图片链接""" class Draw(Base): id: int items: "list[DrawMajor.Item]" type: Literal["MAJOR_TYPE_DRAW"] draw: "DrawMajor.Draw" class PGCMajor(Base): """番剧推送""" class PGC(Base): title: str cover: str jump_url: str """通常https://www.bilibili.com/bangumi/play/ep{epid}""" epid: int season_id: int type: Literal["MAJOR_TYPE_PGC"] pgc: "PGCMajor.PGC" class OPUSMajor(Base): """通用图文内容""" class Summary(Base): rich_text_nodes: list[dict[str, Any]] """描述的富文本节点,组成动态的各种内容""" text: str class Pic(Base): width: int height: int size: int """文件大小,KiB(1024)""" url: str """图片链接""" class Opus(Base): jump_url: str title: str summary: "OPUSMajor.Summary" pics: "list[OPUSMajor.Pic]" type: Literal["MAJOR_TYPE_OPUS"] opus: "OPUSMajor.Opus" class CommonMajor(Base): """还是通用图文内容 主要跟特殊官方功能有关系,例如专属活动页、会员购、漫画、赛事中心、游戏中心、小黑屋、工房集市、装扮等 """ class Common(Base): cover: str """卡片左侧图片的URL""" title: str desc: str """内容""" jump_url: str type: Literal["MAJOR_TYPE_COMMON"] common: "CommonMajor.Common" class CoursesMajor(Base): """课程推送""" class Courses(Base): title: str sub_title: str """副标题,一般是课程的简介""" desc: str """课时信息""" cover: str jump_url: str id: int """课程ID""" type: Literal["MAJOR_TYPE_COURSES"] courses: "CoursesMajor.Courses" class DeletedMajor(Base): class None_(Base): tips: str type: Literal["MAJOR_TYPE_NONE"] none: "DeletedMajor.None_" class UnknownMajor(Base): type: str Major = ( VideoMajor | LiveRecommendMajor | LiveMajor | ArticleMajor | DrawMajor | PGCMajor | OPUSMajor | CommonMajor | CoursesMajor | DeletedMajor | UnknownMajor ) DynRawPost: TypeAlias = PostAPI.Item model_rebuild_recurse(VideoMajor) model_rebuild_recurse(LiveRecommendMajor) model_rebuild_recurse(LiveMajor) model_rebuild_recurse(ArticleMajor) model_rebuild_recurse(DrawMajor) model_rebuild_recurse(PGCMajor) model_rebuild_recurse(OPUSMajor) model_rebuild_recurse(CommonMajor) model_rebuild_recurse(CoursesMajor) model_rebuild_recurse(UserAPI) model_rebuild_recurse(PostAPI)