├── .gitignore ├── .pdm.toml ├── LICENSE ├── README.md ├── assets ├── a.png └── adapter-bilibili.png ├── nonebot └── adapters │ └── bilibili │ ├── __init__.py │ ├── adapter.py │ ├── bili_interaction │ ├── __init__.py │ ├── heartbeat.py │ ├── interaction.py │ └── login_bili.py │ ├── bot.py │ ├── config.py │ ├── consts.py │ ├── event.py │ ├── message.py │ ├── types.py │ └── utils.py └── pyproject.toml /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ -------------------------------------------------------------------------------- /.pdm.toml: -------------------------------------------------------------------------------- 1 | [python] 2 | path = "C:\\Program Files\\Python310\\python.EXE" 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 wwweww 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![assets\adapter-bilibili.png](https://socialify.git.ci/wwweww/adapter-bilibili/image?description=1&descriptionEditable=%E9%80%82%E9%85%8D%E5%93%94%E5%93%A9%E5%93%94%E5%93%A9%E7%9B%B4%E6%92%AD%E9%97%B4websocket%E5%8D%8F%E8%AE%AE%E7%9A%84nonebot2%E9%80%82%E9%85%8D%E5%99%A8&font=Inter&forks=1&issues=1&logo=https%3A%2F%2Fgithub.com%2Fwwweww%2Fadapter-bilibili%2Fblob%2Fmain%2Fassets%2Fa.png%3Fraw%3Dtrue&name=1&pattern=Charlie%20Brown&stargazers=1&theme=Light) 2 | # 配置 3 | ``` 4 | DRIVER=~websockets # 必须有的正向ws的Driver 5 | rooms=[123, 123, 123] # 直播间房间号 6 | manual_login=true # 是否需要手动登录 (登录以后才可以用send方法向直播间发送消息) 默认false(还没实现) 7 | bili_cookie="buvid***" # 登录用的cookies (登录以后才可以用send方法向直播间发送消息) 默认"",没有配置将使用二维码登录 8 | ``` 9 | # 已实现事件 10 |
11 | 消息类 12 | 13 | `Danmu_msg`弹幕
14 | `Super_chat_message`醒目留言 15 |
16 | 17 |
18 | 通知类 19 | `Combo_send`连击礼物
20 | `Send_gift`投喂礼物
21 | `Common_notice_danmaku`限时任务
22 | `Entry_effect`舰长进房
23 | `Interact_word`普通进房消息
24 | `Guard_buy`上舰
25 | `User_toast_msg`续费舰长
26 | `Notice_msg`在本房间续费了舰长
27 | `Like_info_v3_click`点赞
28 | `Like_info_v3_update`总点赞数
29 | `Online_rank_count`在线等级统计
30 | `Room_change`房间信息变动
31 | `Room_real_time_message_update`房间数据
32 | `Watched_change`直播间实时观看人数
33 | `Stop_live_room_list`实时下播列表
34 | `Room_real_time_message_update`房间数据
35 | `Anchor_lot_start`天选之人开始
36 | `Anchor_lot_award`天选之人结果
37 | 38 |
39 | 40 | # 已实现api 41 | `send` 发送弹幕 42 | 43 | # ~~Todo~~(大饼): 44 | 45 | - 登录制作 46 | - 代码重构 47 | 48 | # 鸣谢 49 | 50 | - [ieew](https://github.com/ieew): 提供代码上的帮助 51 | - [17TheWord](https://github.com/17TheWord): 教我使用github 52 | - [NoneBot2](https://github.com/nonebot/nonebot2): 开源代码 让我拥有这次学习机会 53 | 54 | # 顺便一提 55 | 初出茅庐, 有啥好的意见or代码有啥bug欢迎提交 56 | -------------------------------------------------------------------------------- /assets/a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwweww/adapter-bilibili/be15856e9ad9cf49cadf598be20928b068f28e28/assets/a.png -------------------------------------------------------------------------------- /assets/adapter-bilibili.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wwweww/adapter-bilibili/be15856e9ad9cf49cadf598be20928b068f28e28/assets/adapter-bilibili.png -------------------------------------------------------------------------------- /nonebot/adapters/bilibili/__init__.py: -------------------------------------------------------------------------------- 1 | from .bot import Bot as Bot 2 | from .event import Event as Event 3 | from .adapter import Adapter as Adapter 4 | from .message import Message as Message 5 | from .message import MessageSegment as MessageSegment 6 | -------------------------------------------------------------------------------- /nonebot/adapters/bilibili/adapter.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from typing import Any, AsyncGenerator, Dict 4 | from typing_extensions import override 5 | 6 | from nonebot import get_plugin_config 7 | from nonebot.exception import WebSocketClosed 8 | from nonebot.utils import DataclassEncoder, escape_tag 9 | from nonebot.log import logger 10 | from nonebot.message import handle_event 11 | from nonebot.drivers import ( 12 | URL, 13 | Driver, 14 | Request, 15 | Response, 16 | WebSocket, 17 | ForwardDriver, 18 | ReverseDriver, 19 | HTTPServerSetup, 20 | WebSocketServerSetup, 21 | HTTPClientMixin, 22 | WebSocketClientMixin 23 | ) 24 | 25 | from nonebot.adapters import Adapter as BaseAdapter 26 | 27 | import json 28 | 29 | import brotli 30 | 31 | from .bot import Bot 32 | from .event import Event 33 | from .config import Config, LiveRoomInfo 34 | from .consts import WS_HEADERS, HEADERS, GET_DANMU_INFO 35 | from .message import Message, MessageSegment 36 | from .utils import ( 37 | make_auth_packet, 38 | rawData_to_jsonData, 39 | make_packet, 40 | init_random_cookie, 41 | extract_cookies 42 | ) 43 | 44 | def BilibiliLiveAdapterInitError(Exception): 45 | ... 46 | 47 | 48 | class Adapter(BaseAdapter): 49 | @override 50 | def __init__(self, driver: Driver, **kwargs: Any): 51 | if not isinstance(self.driver, HTTPClientMixin)\ 52 | or not isinstance(self.driver, WebSocketClientMixin): 53 | raise TypeError("Adapter need HTTPClient and WebSocketClient") 54 | super().__init__(driver, **kwargs) 55 | self.config: Config = get_plugin_config(Config) 56 | self.tasks = [] 57 | self.links: Dict[str, WebSocket] = dict() 58 | self.connects:list[AsyncGenerator[WebSocket, None]] = [] 59 | self.bots = dict() 60 | if self.config.bili_cookie: 61 | self.cookie = self.config.bili_cookie 62 | extracted_cookies = extract_cookies(self.cookie) 63 | self.uid = extracted_cookies["DedeUserID"] 64 | self.buvid = extracted_cookies["buvid3"] 65 | if not self.config.manual_login: 66 | self.uid = 0 67 | self.cookie = init_random_cookie() 68 | if not self.cookie.get("buvid3"): 69 | raise BilibiliLiveAdapterInitError("Failed to initialize cookies. " 70 | "Try to login or use your own cookies.") 71 | self.buvid = self.cookie.get("buvid3") 72 | else: 73 | # TODO: 登录制作 74 | ... 75 | self.setup() 76 | 77 | @classmethod 78 | @override 79 | def get_name(cls) -> str: 80 | return "BilibiliLive" 81 | 82 | @override 83 | def setup(self) -> None: 84 | self.driver.on_startup(self.startup()) 85 | self.driver.on_shutdown(self.shutdown()) 86 | 87 | async def startup(self): 88 | ( 89 | self.tasks.append( 90 | asyncio.create_task( 91 | self._client(room) 92 | ) 93 | ) 94 | for room 95 | in self.config.rooms 96 | ) 97 | 98 | async def shutdown(self): 99 | for ws in self.links.values: 100 | await ws.close() 101 | 102 | async def _client(self, room_id: LiveRoomInfo): 103 | while True: 104 | try: 105 | await self._join_room(room_id=room_id) 106 | asyncio.create_task(self.send_HB(room_id=room_id)) 107 | await self._on_message(room_id=room_id) 108 | except Exception as e: 109 | logger.error(f"[{room_id}]error: " + str(e)) 110 | await asyncio.sleep(5) 111 | 112 | async def _get_danmu_info(self, roomid): 113 | get_danmu_info_req = Request( 114 | "GET", 115 | url=URL(GET_DANMU_INFO), 116 | params={ 117 | "id": roomid, 118 | "type": 0 119 | }, 120 | cookies=self.cookie, 121 | headers=HEADERS 122 | ) 123 | resposne = await self.request(get_danmu_info_req) 124 | raw_content = resposne.content 125 | try: 126 | result = brotli.decompress(raw_content) 127 | except Exception as _: 128 | result = raw_content.decode("utf8", errors="ignore") 129 | p = json.loads(result) 130 | data = json.dumps(p, separators=(",",":")).encode("utf8") 131 | return data["data"]["token"], data["data"]["host_list"] 132 | 133 | async def _join_room(self, room_id): 134 | key, host_list = self._get_danmu_info(room_id) 135 | ws_req = Request( 136 | "GET", 137 | url=( 138 | f"wss://{host_list[0]['host']}:{host_list[0]['wss_port']}/sub" 139 | if len(host_list) > 9 140 | else "wss://broadcastlv.chat.bilibili.com:443/sub" 141 | ), 142 | headers=WS_HEADERS, 143 | cookies=self.cookie, 144 | timeout=30 145 | ) 146 | auth_data = make_auth_packet(self.uid, room_id, self.buvid, key) 147 | ws: WebSocket = self.websocket(ws_req) 148 | ws.send_bytes(auth_data) 149 | self.links[str(room_id)] = ws 150 | bot = Bot(self, room_id) 151 | self.bot_connect(bot) 152 | self.bots[str(room_id)] = bot 153 | 154 | async def _on_message(self, room_id): 155 | ws:WebSocket = self.links[room_id] 156 | bot = self.bots[str(room_id)] 157 | while True: 158 | data = await ws.receive() 159 | res = rawData_to_jsonData(data) 160 | if not res.get("body"): 161 | continue 162 | for EData in res["body"]: 163 | event_class = Event.new(EData) 164 | if event_class is None: 165 | continue 166 | asyncio.create_task(bot.handle_event(event_class)) 167 | 168 | async def send_HB(self, room_id): 169 | body = "[object Object]".encode("utf8") 170 | hb = make_packet(body, 2) 171 | ws = self.links[str(room_id)] 172 | while True: 173 | await asyncio.sleep(1) 174 | await ws.send_bytes(bytes.fromhex(hb)) 175 | if ws.closed: 176 | return 177 | logger.log( 178 | "DEBUG", 179 | f"[{room_id}] send HB" 180 | ) 181 | await asyncio.sleep(29) 182 | 183 | @override 184 | async def _call_api(self, bot: Bot, api: str, **data: Any) -> Any: 185 | logger.log( 186 | "DEBUG", 187 | "nononononononononono" 188 | ) 189 | -------------------------------------------------------------------------------- /nonebot/adapters/bilibili/bili_interaction/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | """ 4 | @Project :BiliInteraction 5 | @File :__init__.py.py 6 | @Author :Asadz 7 | @Date :2023/1/11 19:45 8 | """ 9 | from .interaction import login 10 | 11 | -------------------------------------------------------------------------------- /nonebot/adapters/bilibili/bili_interaction/heartbeat.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | """ 4 | @Project :nonebotAdapterTest 5 | @File :heartbeat.py 6 | @Author :Asadz 7 | @Date :2023/1/12 7:10 8 | """ 9 | from asyncio import sleep 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /nonebot/adapters/bilibili/bili_interaction/interaction.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | """ 4 | @Project :BiliInteraction 5 | @File :interaction.py 6 | @Author :Asadz 7 | @Date :2023/1/11 19:45 8 | """ 9 | import httpx 10 | from .login_bili import Login 11 | 12 | class BilibiliDriver(Login): 13 | def __init__(self): 14 | super().__init__() 15 | self.cookies = None 16 | self.jcr = None 17 | 18 | def login(self): 19 | qrcodeKey = self.getQRcode() 20 | self.jcr = self.qrWaiting(qrcodeKey) 21 | self.completionCookie(self.jcr) 22 | self.cookies = self.client.cookies 23 | 24 | async def send(self, msg, room_id): 25 | sendApi = "https://api.live.bilibili.com/msg/send" 26 | async with httpx.AsyncClient(cookies=self.cookies, headers=self.client.headers) as aclient: 27 | data = {'bubble': '0', 28 | 'msg': msg, 29 | 'color': '16777215', 30 | 'mode': '1', 31 | 'fontsize': '25', 32 | 'rnd': '1673365377', 33 | 'roomid': str(room_id), 34 | 'csrf': self.jcr, 35 | 'csrf_token': self.jcr, 36 | } 37 | await aclient.post(sendApi, data=data) 38 | 39 | 40 | def login(): 41 | return BilibiliDriver() 42 | -------------------------------------------------------------------------------- /nonebot/adapters/bilibili/bili_interaction/login_bili.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | """ 4 | @Project :BiliInteraction 5 | @File :login_bili.py 6 | @Author :Asadz 7 | @Date :2023/1/11 19:46 8 | """ 9 | import httpx 10 | from .. import utils 11 | 12 | 13 | class Login: 14 | def __init__(self): 15 | self.client = httpx.Client(headers={ 16 | 'origin': 'https://live.bilibili.com', 17 | 'sec-ch-ua': '"Google Chrome";v="107", "Chromium";v="107", "Not=A?Brand";v="24"', 18 | 'sec-ch-ua-mobile': '?0', 19 | 'sec-ch-ua-platform': '"Windows"', 20 | 'sec-fetch-dest': 'empty', 21 | 'sec-fetch-mode': 'cors', 22 | 'sec-fetch-site': 'same-site', 23 | 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36' 24 | }) 25 | 26 | def getQRcode(self): 27 | """ 28 | 在终端打印登录二维码 29 | :return: 用于登录验证的 二维码令牌 30 | """ 31 | getLoginQRApi = "https://passport.bilibili.com/x/passport-login/web/qrcode/generate?source=main-fe-header" 32 | qrData = self.client.get(getLoginQRApi).json() 33 | loginApi = qrData["data"]["url"] 34 | qrcodeKey = qrData["data"]["qrcode_key"] 35 | utils.printUrlQRcode(loginApi) 36 | return qrcodeKey 37 | 38 | def qrWaiting(self, qrcodeKey: str): 39 | """ 40 | 等待用户扫描二维码 41 | :param qrcodeKey: 二维码令牌 42 | :return: 用于验证csrf的数据 43 | """ 44 | params: dict[str, str] = { 45 | "qrcode_key": qrcodeKey, 46 | "source": "main - fe - header", 47 | } 48 | waitApi: str = "https://passport.bilibili.com/x/passport-login/web/qrcode/poll" 49 | issoSetApi = 'https://passport.bilibili.com/x/passport-login/web/sso/list?biliCSRF={}' 50 | while True: 51 | r = self.client.get(url=waitApi, params=params) 52 | if url := r.json()["data"]["url"]: 53 | self.client.get(url) 54 | break 55 | bili_jct = self.client.cookies.get("bili_jct") 56 | return bili_jct 57 | 58 | def completionCookie(self, jct: str): 59 | """ 60 | 登录后Cookie是不全的 这里补全Cookie 61 | :param jct: 登录返回的用于验证csrf的数据 62 | :return: None 63 | """ 64 | ssoSetApi = f'https://passport.bilibili.com/x/passport-login/web/sso/list?biliCSRF={jct}' 65 | apis = self.client.get(ssoSetApi).json()["data"]["sso"] 66 | for url in apis: 67 | self.client.post(url) 68 | -------------------------------------------------------------------------------- /nonebot/adapters/bilibili/bot.py: -------------------------------------------------------------------------------- 1 | from typing import Union, Any 2 | from typing_extensions import override 3 | 4 | from nonebot.adapters import Bot as BaseBot 5 | from nonebot.drivers import Request, URL 6 | 7 | from .event import Event 8 | from .message import Message, MessageSegment 9 | from .consts import HEADERS, SEND_API 10 | 11 | class Bot(BaseBot): 12 | 13 | @override 14 | async def send( 15 | self, 16 | event: Event, 17 | message: Union[str, Message, MessageSegment], 18 | **kwargs, 19 | ) -> Any: 20 | self.adapter.request( 21 | Request( 22 | "POST", 23 | url=URL(SEND_API), 24 | headers=HEADERS, 25 | data={ 26 | "bubble": "0", 27 | "msg": str(message), 28 | "color": "16777215", 29 | "mode": "1", 30 | "room_type": "0", 31 | "jumpfrom": "0", 32 | "reply_mid": "0", 33 | "reply_attr": "0", 34 | "replay_dmid": "", 35 | "statistics": "{\"appId\":100,\"platform\":5}", 36 | "reply_type": "0", 37 | "reply_uname": "", 38 | "fontsize": "25", 39 | "rnd": "1730528504", 40 | "roomid": "1331407", 41 | "csrf": "16549f1988456afa8043b52abfc1b682", 42 | "csrf_token": "16549f1988456afa8043b52abfc1b682" 43 | }, 44 | cookies=self.adapter.cookie 45 | ) 46 | ) 47 | -------------------------------------------------------------------------------- /nonebot/adapters/bilibili/config.py: -------------------------------------------------------------------------------- 1 | from pydantic import Field, BaseModel 2 | 3 | 4 | class LiveRoomInfo(BaseModel): 5 | """ 直播间id """ 6 | room_id: str = "1331407" 7 | 8 | 9 | class Config(BaseModel): 10 | """ 用户cookie可有可无 没有的话就看不见用户信息 """ 11 | bili_cookie: str = "None" 12 | rooms: list[str] 13 | manual_login: bool = False -------------------------------------------------------------------------------- /nonebot/adapters/bilibili/consts.py: -------------------------------------------------------------------------------- 1 | from struct import Struct 2 | 3 | HEADERS = headers = { 4 | "accept": "*/*", 5 | "accept-encoding": "gzip, deflate, br, zstd", 6 | "accept-language": "zh-CN,zh;q=0.9", 7 | "cache-control": "no-cache", 8 | "origin": "https://live.bilibili.com", 9 | "pragma": "no-cache", 10 | "priority": "u=1, i", 11 | "referer": "https://live.bilibili.com", 12 | "sec-ch-ua": "\"Chromium\";v=\"130\", \"Google Chrome\";v=\"130\", \"Not?A_Brand\";v=\"99\"", 13 | "sec-ch-ua-mobile": "?0", 14 | "sec-ch-ua-platform": "\"Windows\"", 15 | "sec-fetch-dest": "empty", 16 | "sec-fetch-mode": "cors", 17 | "sec-fetch-site": "same-site", 18 | "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36" 19 | } 20 | 21 | WS_HEADERS = { 22 | "host": "broadcastlv.chat.bilibili.com", 23 | "origin": "https://live.bilibili.com", 24 | "pragma": "no-cache", 25 | "sec-websocket-extensions": "permessage-deflate; client_max_window_bits", 26 | "sec-websocket-version": "13", 27 | "upgrade": "websocket", 28 | "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36" 29 | } 30 | 31 | GET_DANMU_INFO = "https://api.live.bilibili.com/xlive/web-room/v1/index/getDanmuInfo" 32 | 33 | HEADER_STRUCT = Struct(">I2H2I") 34 | 35 | SEND_API = "https://api.live.bilibili.com/msg/send" -------------------------------------------------------------------------------- /nonebot/adapters/bilibili/event.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | from typing_extensions import override 3 | from typing import Dict, Any, List, Type, Literal 4 | 5 | 6 | from pydantic import model_validator 7 | from nonebot.adapters import Event as BaseEvent 8 | from nonebot.compat import model_dump 9 | 10 | from .message import Message 11 | 12 | 13 | class Event(BaseEvent): 14 | 15 | @classmethod 16 | def new(cls, json_data: Dict): 17 | def all_subclasses(cls: Type[Event]): 18 | return set(cls.__subclasses__()).union( 19 | [s for c in cls.__subclasses__() for s in all_subclasses(c)]) 20 | 21 | event_type = json_data["cmd"] 22 | all_event = all_subclasses(cls) 23 | event_class = None 24 | for event in all_event: 25 | if event.__name__ == event_type.capitalize(): 26 | event_class = event 27 | break 28 | if event_class is None: 29 | return 30 | e = event_class.model_validate(json_data) 31 | return e 32 | 33 | @override 34 | def get_type(self) -> str: 35 | raise NotImplementedError 36 | 37 | @override 38 | def get_event_name(self) -> str: 39 | raise NotImplementedError 40 | 41 | @override 42 | def get_event_description(self) -> str: 43 | return str(model_dump(self)) 44 | 45 | @override 46 | def get_message(self) -> Message: 47 | raise NotImplementedError 48 | 49 | @override 50 | def get_plaintext(self) -> str: 51 | raise NotImplementedError 52 | 53 | @override 54 | def get_user_id(self) -> str: 55 | raise NotImplementedError 56 | 57 | @override 58 | def get_session_id(self) -> str: 59 | raise NotImplementedError 60 | 61 | @override 62 | def is_tome(self) -> bool: 63 | return False 64 | 65 | # 消息事件 -- 弹幕、醒目留言 66 | class MessageEvent(Event): 67 | message: Message 68 | session_id: str 69 | 70 | @overrides(Event) 71 | def get_type(self): 72 | return 'message' 73 | 74 | @overrides(Event) 75 | def get_message(self): 76 | return self.message 77 | 78 | @overrides(Event) 79 | def get_session_id(self) -> str: 80 | return f'group_{self.session_id}' 81 | 82 | 83 | class Danmu_msg(MessageEvent): 84 | """ 弹幕 """ 85 | info: List[Any] 86 | 87 | @root_validator(pre=True, allow_reuse=True) 88 | def check_message(cls, values: Dict[str, Any]) -> Dict[str, Any]: 89 | values["session_id"] = deepcopy(values["info"][9]["ct"]) 90 | values["message"] = deepcopy(values['info'][1]) 91 | return values 92 | 93 | @overrides(Event) 94 | def get_user_id(self) -> str: 95 | return str(self.info[2][0]) 96 | 97 | def get_user_name(self) -> str: 98 | return self.info[2][1] 99 | 100 | 101 | class Super_chat_message(MessageEvent): 102 | """ 醒目留言 """ 103 | data: Dict[str, Any] 104 | duration: int 105 | 106 | @model_validator(pre=True, allow_reuse=True) 107 | def check_message(cls, values: Dict[str, Any]) -> Dict[str, Any]: 108 | values["session_id"] = deepcopy(values["data"]["token"]) 109 | values["massage"] = deepcopy(values["data"]["message"]) 110 | values["duration"] = deepcopy(values["data"]["time"]) 111 | return values 112 | 113 | @overrides(Event) 114 | def get_user_id(self) -> str: 115 | return str(self.data["uid"]) 116 | 117 | 118 | # 通知事件 -- 入房、开通舰长、礼物 119 | class NoticeEvent(Event): 120 | 121 | @overrides(Event) 122 | def get_type(self) -> Literal["notice"]: 123 | return 'notice' 124 | 125 | 126 | class Combo_send(NoticeEvent): 127 | """ 连击礼物 """ 128 | data: Dict[Any, Any] 129 | 130 | 131 | class Send_gift(NoticeEvent): 132 | """ 投喂礼物 """ 133 | data: Dict[Any, Any] 134 | 135 | 136 | class Common_notice_danmaku(NoticeEvent): 137 | """ 限时任务(系统通知的) """ 138 | data: Dict[Any, Any] 139 | 140 | 141 | class Entry_effect(NoticeEvent): 142 | """ 舰长进房 """ 143 | data: Dict[Any, Any] 144 | 145 | 146 | class Interact_word(NoticeEvent): 147 | """ 普通进房消息 """ 148 | data: Dict[Any, Any] 149 | 150 | 151 | class Guard_buy(NoticeEvent): 152 | """ 上舰 """ 153 | data: Dict[Any, Any] 154 | 155 | 156 | class User_toast_msg(NoticeEvent): 157 | """ 续费舰长 """ 158 | data: Dict[Any, Any] 159 | 160 | 161 | class Notice_msg(NoticeEvent): 162 | """ 在本房间续费了舰长 """ 163 | id: int 164 | name: str 165 | full: Dict[str, Any] 166 | half: Dict[str, Any] 167 | side: Dict[str, Any] 168 | scatter: Dict[str, int] 169 | roomid: int 170 | real_roomid: int 171 | msg_common: int 172 | msg_self: str 173 | link_url: str 174 | msg_type: int 175 | shield_uid: int 176 | business_id: str 177 | marquee_id: str 178 | notice_type: int 179 | 180 | @overrides(Event) 181 | def get_event_name(self) -> str: 182 | return self.name 183 | 184 | 185 | class Like_info_v3_click(NoticeEvent): 186 | """ 点赞 """ 187 | data: Dict[Any, Any] 188 | 189 | 190 | class Like_info_v3_update(NoticeEvent): 191 | """ 总点赞数 """ 192 | data: Dict[Any, Any] 193 | 194 | 195 | class Online_rank_count(NoticeEvent): 196 | """ 在线等级统计 """ 197 | data: Dict[Any, Any] 198 | 199 | 200 | class Online_rank_v2(NoticeEvent): 201 | """ 在线等级榜 """ 202 | data: Dict[Any, Any] 203 | 204 | 205 | class Popular_rank_changed(NoticeEvent): 206 | data: Dict[Any, Any] 207 | 208 | 209 | class Room_change(Event): 210 | """ 房间信息变动(分区、标题等) """ 211 | data: Dict[Any, Any] 212 | 213 | 214 | class Room_real_time_message_update(NoticeEvent): 215 | """ 房间数据 """ 216 | data: Dict[Any, Any] 217 | 218 | 219 | class Watched_change(NoticeEvent): 220 | """ 直播间观看人数 """ 221 | data: Dict[Any, Any] 222 | 223 | 224 | class Stop_live_room_list(NoticeEvent): 225 | """ 下播列表 """ 226 | data: Dict[Any, Any] 227 | room_id_list: List[int] 228 | 229 | @root_validator(pre=True, allow_reuse=True) 230 | def check_message(cls, values: Dict[str, Any]) -> Dict[str, Any]: 231 | values["room_id_list"] = deepcopy(values["data"]["room_id_list"]) 232 | return values 233 | 234 | 235 | class Anchor_lot_start(NoticeEvent): 236 | """ 天选之人开始 """ 237 | data: Dict[Any, Any] 238 | 239 | def get_anchor_lot_info(self): 240 | """ 获取天选之人的相关信息 """ 241 | return { 242 | "award_name": self.data["award_name"], 243 | "danmu": self.data["danmu"], 244 | "gift_name": self.data["gift_name"] 245 | } 246 | 247 | 248 | class Anchor_lot_award(NoticeEvent): 249 | """ 天选之人结果 """ 250 | data: Dict[Any, Any] 251 | 252 | def winner_info(self): 253 | """ 获取中奖人信息 """ 254 | return self.data["award_users"] -------------------------------------------------------------------------------- /nonebot/adapters/bilibili/message.py: -------------------------------------------------------------------------------- 1 | from typing import Type, Union, Mapping, Iterable 2 | from typing_extensions import override 3 | 4 | from nonebot.adapters import Message as BaseMessage, MessageSegment as BaseMessageSegment 5 | 6 | 7 | class MessageSegment(BaseMessageSegment["Message"]): 8 | 9 | @classmethod 10 | @override 11 | def get_message_class(cls) -> Type["Message"]: 12 | return Message 13 | 14 | @override 15 | def __str__(self) -> str: 16 | return self.data["body"] 17 | 18 | @override 19 | def is_text(self) -> bool: 20 | return self.type == "danmu" 21 | 22 | 23 | @staticmethod 24 | def danmu(msg: str): 25 | return MessageSegment("danmu", {"msg": msg}) 26 | 27 | 28 | class Message(BaseMessage[MessageSegment]): 29 | 30 | @classmethod 31 | @override 32 | def get_segment_class(cls) -> Type[MessageSegment]: 33 | return MessageSegment 34 | 35 | @staticmethod 36 | @override 37 | def _construct(msg: str) -> Iterable[MessageSegment]: 38 | raise NotImplementedError 39 | -------------------------------------------------------------------------------- /nonebot/adapters/bilibili/types.py: -------------------------------------------------------------------------------- 1 | from typing import NamedTuple 2 | 3 | class HeaderTuple(NamedTuple): 4 | pack_len: int 5 | raw_header_size: int 6 | ver: int 7 | operation: int 8 | seq_id: int -------------------------------------------------------------------------------- /nonebot/adapters/bilibili/utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | import re 3 | from typing import Union 4 | from .consts import HEADER_STRUCT, HEADERS 5 | from .types import HeaderTuple 6 | 7 | from nonebot.log import logger 8 | 9 | import brotli 10 | import httpx 11 | 12 | class PacketOffset: 13 | WS_PACKAGE_OFFSET = slice(0, 4) 14 | WS_HEADER_OFFSET = slice(4, 2) 15 | WS_VERSION_OFFSET = slice(6, 2) 16 | WS_OPERATION_OFFSET = slice(8, 2) 17 | WS_SEQUENCE_OFFSET = slice(12, 4) 18 | U32 = lambda s=0: slice(s, s+4) 19 | U16 = lambda s=0: slice(s, s+2) 20 | WS_BINARY_HEADER_LIST = [ 21 | { 22 | "name": "Header Length", 23 | "key": "headerLen", 24 | "offset": WS_HEADER_OFFSET, 25 | }, 26 | { 27 | "name": "Protocol Version", 28 | "key": "ver", 29 | "offset": WS_VERSION_OFFSET, 30 | }, 31 | { 32 | "name": "Operation", 33 | "key": "op", 34 | "offset": WS_OPERATION_OFFSET, 35 | }, 36 | { 37 | "name": "Sequence Id", 38 | "key": "seq", 39 | "offset": WS_SEQUENCE_OFFSET, 40 | } 41 | ] 42 | 43 | 44 | def make_packet(data: Union[dict, str, bytes], 45 | operation: int): 46 | if isinstance(data, dict): 47 | body = json.dumps(data, separators=(",",":")).encode("utf8") 48 | elif isinstance(data, str): 49 | body = data.encode("utf8") 50 | else: 51 | body = data 52 | header = HEADER_STRUCT.pack(*HeaderTuple( 53 | pack_len=HEADER_STRUCT.size + len(body), 54 | raw_header_size=HEADER_STRUCT.size, 55 | ver=1, 56 | operation=operation, 57 | seq_id=1 58 | )) 59 | 60 | return header + body 61 | 62 | 63 | def make_auth_packet(uid, room_id, buvid3, key): 64 | auth_params = { 65 | 'uid': int(uid), 66 | 'roomid': int(room_id), 67 | 'protover': 3, 68 | 'buvid': buvid3, 69 | 'platform': 'web', 70 | 'type': 2, 71 | 'key': key 72 | } 73 | return make_packet(auth_params, 7) 74 | 75 | 76 | def rawData_to_jsonData(data: bytes): 77 | packetLen = int(data[PacketOffset.WS_PACKAGE_OFFSET].hex(), 16) 78 | result = dict() 79 | result["body"] = [] 80 | 81 | for e in PacketOffset.WS_BINARY_HEADER_LIST: 82 | result[e["key"]] = int(data[e['offset']].hex(), 16) 83 | 84 | if (packetLen < len(data)): 85 | return rawData_to_jsonData(data[:packetLen]) 86 | if (result["op"] and result["op"] == 3): 87 | result["body"] = {"count": int(data[PacketOffset.U32(16)].hex(), 16)} 88 | else: 89 | n = 0 90 | s = packetLen 91 | a = "" 92 | l = "" 93 | while n < len(data): 94 | s = int(data[PacketOffset.U32(n)].hex(), 16) 95 | a = int(data[PacketOffset.U16(n+4)].hex(), 16) 96 | try: 97 | if(result["ver"] == 3): 98 | h = data[n+a:n+s] 99 | l = brotli.decompress(h).decode("utf8", errors="ignore") 100 | elif (result["ver"] == 0 or result["ver"] == 1): 101 | l = data.decode("utf8", errors="ignore") 102 | l = json.loads(l[l.index("{"):]) 103 | result["body"].append(l) 104 | except Exception as e: 105 | logger.error("数据解析失败", e) 106 | 107 | n += s 108 | return result 109 | 110 | 111 | def init_random_cookie(): 112 | with httpx.Client(headers=HEADERS) as client: 113 | client.get("https://www.bilibili.com/") 114 | return client.cookies 115 | 116 | 117 | def extract_cookies(cookie_string): 118 | pattern = r'(?Pbuvid3|DedeUserID)=(?P[^;]+)' 119 | matches = re.findall(pattern, cookie_string) 120 | 121 | cookie_dict = {name: value for name, value in matches} 122 | return cookie_dict 123 | 124 | def get_room_id(room): 125 | with httpx.Client(headers=HEADERS) as client: 126 | resp = client.get("https://live.bilibili.com/1576468") 127 | res = resp.text 128 | pattern = r'"room_id":(\d{3,})' 129 | r = re.findall(pattern, res) 130 | if r: 131 | return r[0] 132 | else: 133 | raise Exception("Failed to get real room number") -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.pdm] 2 | 3 | [project] 4 | name = "nonebot-adapter-bilibili" 5 | version = "0.0.7" 6 | description = "适配哔哩哔哩直播间ws协议的Nonebo2适配器" 7 | authors = [ 8 | {name = "wwweww", email = "2646787260@qq.com"}, 9 | ] 10 | dependencies = [ 11 | "nonebot2>=2.0.0rc2", 12 | "qrcode-terminal>=0.8", 13 | "brotli>=1.1.0" 14 | ] 15 | requires-python = ">=3.8" 16 | readme = "README.md" 17 | license = {text = "MIT"} 18 | 19 | [project.urls] 20 | repository = "https://github.com/wwweww/adapter-bilibili" 21 | 22 | [tool.pdm.build] 23 | includes = ["nonebot"] 24 | 25 | [build-system] 26 | requires = ["pdm-pep517>=1.0"] 27 | build-backend = "pdm.pep517.api" 28 | 29 | --------------------------------------------------------------------------------