├── .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 | 
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 |
--------------------------------------------------------------------------------