├── Barrage ├── Huya.py ├── Tiktok.py ├── __init__.py └── blivedm │ ├── __init__.py │ ├── client.py │ ├── handlers.py │ └── models │ ├── __init__.py │ └── pb.py ├── Dictionary ├── Tiktok │ ├── Gift.py │ └── __init__.py └── __init__.py ├── GPT ├── Chat.py └── __init__.py ├── Model ├── Huya │ ├── BarrageMessage.py │ └── __init__.py ├── TikTok │ ├── BarrageMessage.py │ └── __init__.py └── __init__.py ├── Readme ├── readme_bilibili.md ├── readme_huya.md └── readme_tiktok.md ├── Resources └── Images │ ├── ChatGPT_logo.svg.png │ ├── ai_logo.png │ ├── bilibili_default_avatar.jpg │ ├── download.png │ ├── huya_default_avatar.png │ ├── logo.png │ └── tiktok_default_avatar.jfif ├── Screenshots ├── Pictures │ ├── bilibili.png │ └── huya.png └── Videos │ ├── bilibili.mp4 │ └── huya.mp4 ├── ThirdLib ├── huya_barrage │ ├── app.js │ ├── index.js │ ├── lib.js │ ├── package-lock.json │ └── package.json ├── huya_barrage_bravo │ ├── app.js │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── index.js │ │ └── lib │ │ │ ├── GUESS.js │ │ │ ├── HUYA.js │ │ │ ├── Taf.js │ │ │ └── TafMx.js │ └── yarn.lock └── tiktok_barrage_nodejs │ ├── WSLINK.exe │ ├── ak.html │ ├── app.js │ ├── client.js │ ├── index.js │ ├── made │ ├── d02.js │ ├── d03.js │ ├── dgiftcount.js │ ├── gitfcount │ │ ├── EasyNet.ec │ │ ├── HPSocket4C.dll │ │ ├── HP_Socket-20220501-无DLL.ec │ │ ├── Ws_WebServer[公众版].ec │ │ ├── config.ini │ │ ├── tft_dianzicheng.bak │ │ ├── tft_dianzicheng.e │ │ ├── zyJson1.3.ec │ │ ├── 模拟消息发送.html │ │ └── 精易模块v7.35.ec │ ├── room.css │ └── room.js │ ├── package-lock.json │ ├── package.json │ ├── template │ ├── ak.html │ ├── gift.json │ ├── giftinfo.html │ ├── gitlist.json │ ├── gitsmal.json │ └── message.json │ └── test │ └── index.js ├── Utils ├── Common.py ├── Config.py └── __init__.py ├── Windows ├── UI │ ├── MainWindow │ │ ├── MainWindow.py │ │ ├── MainWindow.qrc │ │ ├── MainWindow_UI.py │ │ ├── MainWindow_UI.ui │ │ ├── MainWindow_rc.py │ │ └── __init__.py │ └── __init__.py └── __init__.py ├── bilibili.py ├── config.example.json ├── huya.py ├── readme.md ├── readme_en.md ├── requirements.txt └── tiktok.py /Barrage/Huya.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import websockets 3 | from loguru import logger 4 | 5 | from Model.Huya import BarrageMessage 6 | 7 | 8 | class Huya: 9 | def __init__(self, url='ws://127.0.0.1:9528'): 10 | self.message_queue = asyncio.Queue() 11 | self.websocket = None 12 | self.url = url 13 | 14 | async def connect(self): 15 | self.websocket = await websockets.connect(self.url) 16 | logger.success("Connection opened") 17 | 18 | async def receive_messages(self): 19 | try: 20 | while True: 21 | message = await self.websocket.recv() 22 | await self.message_queue.put(message) 23 | # print("Received message:", message) 24 | except websockets.ConnectionClosed: 25 | logger.info("Connection closed") 26 | 27 | async def send_message(self, message): 28 | await self.websocket.send(message) 29 | 30 | async def get_message(self): 31 | return await self.message_queue.get() 32 | 33 | async def close(self): 34 | await self.websocket.close() 35 | logger.info("Connection closed") 36 | 37 | async def process_messages(self, callback_function): 38 | while True: 39 | message = await self.get_message() 40 | message = BarrageMessage.from_json(message) 41 | callback_function(message) 42 | if message.type == "gift": 43 | logger.info(f"{message.nickname} 送出了 {message.gift_name} x {message.gfcnt}") 44 | else: 45 | logger.info(f"{message.nickname}: {message.msg_content}") 46 | -------------------------------------------------------------------------------- /Barrage/Tiktok.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import websockets 3 | from loguru import logger 4 | 5 | from Dictionary.Tiktok import Gift 6 | from Model.TikTok import BarrageMessage 7 | 8 | 9 | class Tiktok: 10 | def __init__(self, url='ws://127.0.0.1:9527'): 11 | self.message_queue = asyncio.Queue() 12 | self.websocket = None 13 | self.url = url 14 | 15 | async def connect(self): 16 | self.websocket = await websockets.connect(self.url) 17 | logger.success("Connection opened") 18 | 19 | async def receive_messages(self): 20 | try: 21 | while True: 22 | message = await self.websocket.recv() 23 | await self.message_queue.put(message) 24 | # print("Received message:", message) 25 | except websockets.ConnectionClosed: 26 | logger.info("Connection closed") 27 | 28 | async def send_message(self, message): 29 | await self.websocket.send(message) 30 | 31 | async def get_message(self): 32 | return await self.message_queue.get() 33 | 34 | async def close(self): 35 | await self.websocket.close() 36 | logger.info("Connection closed") 37 | 38 | @staticmethod 39 | async def recv_gift(msg_content): 40 | gift = Gift() 41 | if gift.is_gift_exist(msg_content.gift_name): 42 | logger.info(f"{msg_content.user_nickname} 送出了 {msg_content.gift_name} x {msg_content.gift_number}") 43 | else: 44 | logger.warning("unknown gift / gift not found") 45 | logger.warning(msg_content) 46 | 47 | async def process_messages(self, callback_function): 48 | while True: 49 | message = await self.get_message() 50 | message = BarrageMessage(message) 51 | callback_function(message) 52 | if message.isGift: 53 | asyncio.ensure_future(Tiktok.recv_gift(message)) 54 | else: 55 | logger.info(f"{message.user_nickname}: {message.msg_content}") 56 | -------------------------------------------------------------------------------- /Barrage/__init__.py: -------------------------------------------------------------------------------- 1 | from .Tiktok import Tiktok 2 | from .Huya import Huya 3 | from .blivedm import * 4 | -------------------------------------------------------------------------------- /Barrage/blivedm/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from .models import * 3 | from .handlers import * 4 | from .client import * 5 | -------------------------------------------------------------------------------- /Barrage/blivedm/client.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | import enum 4 | import json 5 | import logging 6 | import ssl as ssl_ 7 | import struct 8 | from typing import * 9 | 10 | import aiohttp 11 | import brotli 12 | 13 | from . import handlers 14 | 15 | __all__ = ( 16 | 'BLiveClient', 17 | ) 18 | 19 | logger = logging.getLogger('blivedm') 20 | 21 | ROOM_INIT_URL = 'https://api.live.bilibili.com/xlive/web-room/v1/index/getInfoByRoom' 22 | DANMAKU_SERVER_CONF_URL = 'https://api.live.bilibili.com/xlive/web-room/v1/index/getDanmuInfo' 23 | DEFAULT_DANMAKU_SERVER_LIST = [ 24 | {'host': 'broadcastlv.chat.bilibili.com', 'port': 2243, 'wss_port': 443, 'ws_port': 2244} 25 | ] 26 | 27 | HEADER_STRUCT = struct.Struct('>I2H2I') 28 | 29 | 30 | class HeaderTuple(NamedTuple): 31 | pack_len: int 32 | raw_header_size: int 33 | ver: int 34 | operation: int 35 | seq_id: int 36 | 37 | 38 | # WS_BODY_PROTOCOL_VERSION 39 | class ProtoVer(enum.IntEnum): 40 | NORMAL = 0 41 | HEARTBEAT = 1 42 | DEFLATE = 2 43 | BROTLI = 3 44 | 45 | 46 | # go-common\app\service\main\broadcast\model\operation.go 47 | class Operation(enum.IntEnum): 48 | HANDSHAKE = 0 49 | HANDSHAKE_REPLY = 1 50 | HEARTBEAT = 2 51 | HEARTBEAT_REPLY = 3 52 | SEND_MSG = 4 53 | SEND_MSG_REPLY = 5 54 | DISCONNECT_REPLY = 6 55 | AUTH = 7 56 | AUTH_REPLY = 8 57 | RAW = 9 58 | PROTO_READY = 10 59 | PROTO_FINISH = 11 60 | CHANGE_ROOM = 12 61 | CHANGE_ROOM_REPLY = 13 62 | REGISTER = 14 63 | REGISTER_REPLY = 15 64 | UNREGISTER = 16 65 | UNREGISTER_REPLY = 17 66 | # B站业务自定义OP 67 | # MinBusinessOp = 1000 68 | # MaxBusinessOp = 10000 69 | 70 | 71 | # WS_AUTH 72 | class AuthReplyCode(enum.IntEnum): 73 | OK = 0 74 | TOKEN_ERROR = -101 75 | 76 | 77 | class InitError(Exception): 78 | """初始化失败""" 79 | 80 | 81 | class AuthError(Exception): 82 | """认证失败""" 83 | 84 | 85 | class BLiveClient: 86 | """ 87 | B站直播弹幕客户端,负责连接房间 88 | 89 | :param room_id: URL中的房间ID,可以用短ID 90 | :param uid: B站用户ID,0表示未登录 91 | :param session: cookie、连接池 92 | :param heartbeat_interval: 发送心跳包的间隔时间(秒) 93 | :param ssl: True表示用默认的SSLContext验证,False表示不验证,也可以传入SSLContext 94 | """ 95 | 96 | def __init__( 97 | self, 98 | room_id, 99 | uid=0, 100 | session: Optional[aiohttp.ClientSession] = None, 101 | heartbeat_interval=30, 102 | ssl: Union[bool, ssl_.SSLContext] = True, 103 | ): 104 | self._tmp_room_id = room_id 105 | """用来init_room的临时房间ID,可以用短ID""" 106 | self._uid = uid 107 | 108 | if session is None: 109 | self._session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10)) 110 | self._own_session = True 111 | else: 112 | self._session = session 113 | self._own_session = False 114 | assert self._session.loop is asyncio.get_event_loop() # noqa 115 | 116 | self._heartbeat_interval = heartbeat_interval 117 | self._ssl = ssl if ssl else ssl_._create_unverified_context() # noqa 118 | 119 | self._handlers: List[handlers.HandlerInterface] = [] 120 | """消息处理器,可动态增删""" 121 | 122 | # 在调用init_room后初始化的字段 123 | self._room_id = None 124 | """真实房间ID""" 125 | self._room_short_id = None 126 | """房间短ID,没有则为0""" 127 | self._room_owner_uid = None 128 | """主播用户ID""" 129 | self._host_server_list: Optional[List[dict]] = None 130 | """ 131 | 弹幕服务器列表 132 | [{host: "tx-bj4-live-comet-04.chat.bilibili.com", port: 2243, wss_port: 443, ws_port: 2244}, ...] 133 | """ 134 | self._host_server_token = None 135 | """连接弹幕服务器用的token""" 136 | 137 | # 在运行时初始化的字段 138 | self._websocket: Optional[aiohttp.ClientWebSocketResponse] = None 139 | """WebSocket连接""" 140 | self._network_future: Optional[asyncio.Future] = None 141 | """网络协程的future""" 142 | self._heartbeat_timer_handle: Optional[asyncio.TimerHandle] = None 143 | """发心跳包定时器的handle""" 144 | 145 | @property 146 | def is_running(self) -> bool: 147 | """ 148 | 本客户端正在运行,注意调用stop后还没完全停止也算正在运行 149 | """ 150 | return self._network_future is not None 151 | 152 | @property 153 | def room_id(self) -> Optional[int]: 154 | """ 155 | 房间ID,调用init_room后初始化 156 | """ 157 | return self._room_id 158 | 159 | @property 160 | def room_short_id(self) -> Optional[int]: 161 | """ 162 | 房间短ID,没有则为0,调用init_room后初始化 163 | """ 164 | return self._room_short_id 165 | 166 | @property 167 | def room_owner_uid(self) -> Optional[int]: 168 | """ 169 | 主播用户ID,调用init_room后初始化 170 | """ 171 | return self._room_owner_uid 172 | 173 | def add_handler(self, handler: 'handlers.HandlerInterface'): 174 | """ 175 | 添加消息处理器 176 | 注意多个处理器是并发处理的,不要依赖处理的顺序 177 | 消息处理器和接收消息运行在同一协程,如果处理消息耗时太长会阻塞接收消息,这种情况建议将消息推到队列,让另一个协程处理 178 | 179 | :param handler: 消息处理器 180 | """ 181 | if handler not in self._handlers: 182 | self._handlers.append(handler) 183 | 184 | def remove_handler(self, handler: 'handlers.HandlerInterface'): 185 | """ 186 | 移除消息处理器 187 | 188 | :param handler: 消息处理器 189 | """ 190 | try: 191 | self._handlers.remove(handler) 192 | except ValueError: 193 | pass 194 | 195 | def start(self): 196 | """ 197 | 启动本客户端 198 | """ 199 | if self.is_running: 200 | logger.warning('room=%s client is running, cannot start() again', self.room_id) 201 | return 202 | 203 | self._network_future = asyncio.create_task(self._network_coroutine_wrapper()) 204 | 205 | def stop(self): 206 | """ 207 | 停止本客户端 208 | """ 209 | if not self.is_running: 210 | logger.warning('room=%s client is stopped, cannot stop() again', self.room_id) 211 | return 212 | 213 | self._network_future.cancel() 214 | 215 | async def stop_and_close(self): 216 | """ 217 | 便利函数,停止本客户端并释放本客户端的资源,调用后本客户端将不可用 218 | """ 219 | if self.is_running: 220 | self.stop() 221 | await self.join() 222 | await self.close() 223 | 224 | async def join(self): 225 | """ 226 | 等待本客户端停止 227 | """ 228 | if not self.is_running: 229 | logger.warning('room=%s client is stopped, cannot join()', self.room_id) 230 | return 231 | 232 | await asyncio.shield(self._network_future) 233 | 234 | async def close(self): 235 | """ 236 | 释放本客户端的资源,调用后本客户端将不可用 237 | """ 238 | if self.is_running: 239 | logger.warning('room=%s is calling close(), but client is running', self.room_id) 240 | 241 | # 如果session是自己创建的则关闭session 242 | if self._own_session: 243 | await self._session.close() 244 | 245 | async def init_room(self): 246 | """ 247 | 初始化连接房间需要的字段 248 | 249 | :return: True代表没有降级,如果需要降级后还可用,重载这个函数返回True 250 | """ 251 | res = True 252 | if not await self._init_room_id_and_owner(): 253 | res = False 254 | # 失败了则降级 255 | self._room_id = self._room_short_id = self._tmp_room_id 256 | self._room_owner_uid = 0 257 | 258 | if not await self._init_host_server(): 259 | res = False 260 | # 失败了则降级 261 | self._host_server_list = DEFAULT_DANMAKU_SERVER_LIST 262 | self._host_server_token = None 263 | return res 264 | 265 | async def _init_room_id_and_owner(self): 266 | try: 267 | async with self._session.get( 268 | ROOM_INIT_URL, 269 | headers={ 270 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)' 271 | ' Chrome/102.0.0.0 Safari/537.36' 272 | }, 273 | params={ 274 | 'room_id': self._tmp_room_id 275 | }, 276 | ssl=self._ssl 277 | ) as res: 278 | if res.status != 200: 279 | logger.warning('room=%d _init_room_id_and_owner() failed, status=%d, reason=%s', self._tmp_room_id, 280 | res.status, res.reason) 281 | return False 282 | data = await res.json() 283 | if data['code'] != 0: 284 | logger.warning('room=%d _init_room_id_and_owner() failed, message=%s', self._tmp_room_id, 285 | data['message']) 286 | return False 287 | if not self._parse_room_init(data['data']): 288 | return False 289 | except (aiohttp.ClientConnectionError, asyncio.TimeoutError): 290 | logger.exception('room=%d _init_room_id_and_owner() failed:', self._tmp_room_id) 291 | return False 292 | return True 293 | 294 | def _parse_room_init(self, data): 295 | room_info = data['room_info'] 296 | self._room_id = room_info['room_id'] 297 | self._room_short_id = room_info['short_id'] 298 | self._room_owner_uid = room_info['uid'] 299 | return True 300 | 301 | async def _init_host_server(self): 302 | try: 303 | async with self._session.get( 304 | DANMAKU_SERVER_CONF_URL, 305 | headers={ 306 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)' 307 | ' Chrome/102.0.0.0 Safari/537.36' 308 | }, 309 | params={ 310 | 'id': self._room_id, 311 | 'type': 0 312 | }, 313 | ssl=self._ssl 314 | ) as res: 315 | if res.status != 200: 316 | logger.warning('room=%d _init_host_server() failed, status=%d, reason=%s', self._room_id, 317 | res.status, res.reason) 318 | return False 319 | data = await res.json() 320 | if data['code'] != 0: 321 | logger.warning('room=%d _init_host_server() failed, message=%s', self._room_id, data['message']) 322 | return False 323 | if not self._parse_danmaku_server_conf(data['data']): 324 | return False 325 | except (aiohttp.ClientConnectionError, asyncio.TimeoutError): 326 | logger.exception('room=%d _init_host_server() failed:', self._room_id) 327 | return False 328 | return True 329 | 330 | def _parse_danmaku_server_conf(self, data): 331 | self._host_server_list = data['host_list'] 332 | self._host_server_token = data['token'] 333 | if not self._host_server_list: 334 | logger.warning('room=%d _parse_danmaku_server_conf() failed: host_server_list is empty', self._room_id) 335 | return False 336 | return True 337 | 338 | @staticmethod 339 | def _make_packet(data: dict, operation: int) -> bytes: 340 | """ 341 | 创建一个要发送给服务器的包 342 | 343 | :param data: 包体JSON数据 344 | :param operation: 操作码,见Operation 345 | :return: 整个包的数据 346 | """ 347 | body = json.dumps(data).encode('utf-8') 348 | header = HEADER_STRUCT.pack(*HeaderTuple( 349 | pack_len=HEADER_STRUCT.size + len(body), 350 | raw_header_size=HEADER_STRUCT.size, 351 | ver=1, 352 | operation=operation, 353 | seq_id=1 354 | )) 355 | return header + body 356 | 357 | async def _network_coroutine_wrapper(self): 358 | """ 359 | 负责处理网络协程的异常,网络协程具体逻辑在_network_coroutine里 360 | """ 361 | try: 362 | await self._network_coroutine() 363 | except asyncio.CancelledError: 364 | # 正常停止 365 | pass 366 | except Exception: # noqa 367 | logger.exception('room=%s _network_coroutine() finished with exception:', self.room_id) 368 | finally: 369 | logger.debug('room=%s _network_coroutine() finished', self.room_id) 370 | self._network_future = None 371 | 372 | async def _network_coroutine(self): 373 | """ 374 | 网络协程,负责连接服务器、接收消息、解包 375 | """ 376 | # 如果之前未初始化则初始化 377 | if self._host_server_token is None: 378 | if not await self.init_room(): 379 | raise InitError('init_room() failed') 380 | 381 | retry_count = 0 382 | while True: 383 | try: 384 | # 连接 385 | host_server = self._host_server_list[retry_count % len(self._host_server_list)] 386 | async with self._session.ws_connect( 387 | f"wss://{host_server['host']}:{host_server['wss_port']}/sub", 388 | headers={ 389 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)' 390 | ' Chrome/102.0.0.0 Safari/537.36' 391 | }, 392 | receive_timeout=self._heartbeat_interval + 5, 393 | ssl=self._ssl 394 | ) as websocket: 395 | self._websocket = websocket 396 | await self._on_ws_connect() 397 | 398 | # 处理消息 399 | message: aiohttp.WSMessage 400 | async for message in websocket: 401 | await self._on_ws_message(message) 402 | # 至少成功处理1条消息 403 | retry_count = 0 404 | 405 | except (aiohttp.ClientConnectionError, asyncio.TimeoutError): 406 | # 掉线重连 407 | pass 408 | except AuthError: 409 | # 认证失败了,应该重新获取token再重连 410 | logger.exception('room=%d auth failed, trying init_room() again', self.room_id) 411 | if not await self.init_room(): 412 | raise InitError('init_room() failed') 413 | except ssl_.SSLError: 414 | logger.error('room=%d a SSLError happened, cannot reconnect', self.room_id) 415 | raise 416 | finally: 417 | self._websocket = None 418 | await self._on_ws_close() 419 | 420 | # 准备重连 421 | retry_count += 1 422 | logger.warning('room=%d is reconnecting, retry_count=%d', self.room_id, retry_count) 423 | await asyncio.sleep(1) 424 | 425 | async def _on_ws_connect(self): 426 | """ 427 | WebSocket连接成功 428 | """ 429 | await self._send_auth() 430 | self._heartbeat_timer_handle = asyncio.get_running_loop().call_later( 431 | self._heartbeat_interval, self._on_send_heartbeat 432 | ) 433 | 434 | async def _on_ws_close(self): 435 | """ 436 | WebSocket连接断开 437 | """ 438 | if self._heartbeat_timer_handle is not None: 439 | self._heartbeat_timer_handle.cancel() 440 | self._heartbeat_timer_handle = None 441 | 442 | async def _send_auth(self): 443 | """ 444 | 发送认证包 445 | """ 446 | auth_params = { 447 | 'uid': self._uid or self.room_owner_uid or 0, 448 | 'roomid': self._room_id, 449 | 'protover': 3, 450 | 'platform': 'web', 451 | 'type': 2 452 | } 453 | if self._host_server_token is not None: 454 | auth_params['key'] = self._host_server_token 455 | await self._websocket.send_bytes(self._make_packet(auth_params, Operation.AUTH)) 456 | 457 | def _on_send_heartbeat(self): 458 | """ 459 | 定时发送心跳包的回调 460 | """ 461 | if self._websocket is None or self._websocket.closed: 462 | self._heartbeat_timer_handle = None 463 | return 464 | 465 | self._heartbeat_timer_handle = asyncio.get_running_loop().call_later( 466 | self._heartbeat_interval, self._on_send_heartbeat 467 | ) 468 | asyncio.create_task(self._send_heartbeat()) 469 | 470 | async def _send_heartbeat(self): 471 | """ 472 | 发送心跳包 473 | """ 474 | if self._websocket is None or self._websocket.closed: 475 | return 476 | 477 | try: 478 | await self._websocket.send_bytes(self._make_packet({}, Operation.HEARTBEAT)) 479 | except (ConnectionResetError, aiohttp.ClientConnectionError) as e: 480 | logger.warning('room=%d _send_heartbeat() failed: %r', self.room_id, e) 481 | except Exception: # noqa 482 | logger.exception('room=%d _send_heartbeat() failed:', self.room_id) 483 | 484 | async def _on_ws_message(self, message: aiohttp.WSMessage): 485 | """ 486 | 收到WebSocket消息 487 | 488 | :param message: WebSocket消息 489 | """ 490 | if message.type != aiohttp.WSMsgType.BINARY: 491 | logger.warning('room=%d unknown websocket message type=%s, data=%s', self.room_id, 492 | message.type, message.data) 493 | return 494 | 495 | try: 496 | await self._parse_ws_message(message.data) 497 | except (asyncio.CancelledError, AuthError): 498 | # 正常停止、认证失败,让外层处理 499 | raise 500 | except Exception: # noqa 501 | logger.exception('room=%d _parse_ws_message() error:', self.room_id) 502 | 503 | async def _parse_ws_message(self, data: bytes): 504 | """ 505 | 解析WebSocket消息 506 | 507 | :param data: WebSocket消息数据 508 | """ 509 | offset = 0 510 | try: 511 | header = HeaderTuple(*HEADER_STRUCT.unpack_from(data, offset)) 512 | except struct.error: 513 | logger.exception('room=%d parsing header failed, offset=%d, data=%s', self.room_id, offset, data) 514 | return 515 | 516 | if header.operation in (Operation.SEND_MSG_REPLY, Operation.AUTH_REPLY): 517 | # 业务消息,可能有多个包一起发,需要分包 518 | while True: 519 | body = data[offset + header.raw_header_size: offset + header.pack_len] 520 | await self._parse_business_message(header, body) 521 | 522 | offset += header.pack_len 523 | if offset >= len(data): 524 | break 525 | 526 | try: 527 | header = HeaderTuple(*HEADER_STRUCT.unpack_from(data, offset)) 528 | except struct.error: 529 | logger.exception('room=%d parsing header failed, offset=%d, data=%s', self.room_id, offset, data) 530 | break 531 | 532 | elif header.operation == Operation.HEARTBEAT_REPLY: 533 | # 服务器心跳包,前4字节是人气值,后面是客户端发的心跳包内容 534 | # pack_len不包括客户端发的心跳包内容,不知道是不是服务器BUG 535 | body = data[offset + header.raw_header_size: offset + header.raw_header_size + 4] 536 | popularity = int.from_bytes(body, 'big') 537 | # 自己造个消息当成业务消息处理 538 | body = { 539 | 'cmd': '_HEARTBEAT', 540 | 'data': { 541 | 'popularity': popularity 542 | } 543 | } 544 | await self._handle_command(body) 545 | 546 | else: 547 | # 未知消息 548 | body = data[offset + header.raw_header_size: offset + header.pack_len] 549 | logger.warning('room=%d unknown message operation=%d, header=%s, body=%s', self.room_id, 550 | header.operation, header, body) 551 | 552 | async def _parse_business_message(self, header: HeaderTuple, body: bytes): 553 | """ 554 | 解析业务消息 555 | """ 556 | if header.operation == Operation.SEND_MSG_REPLY: 557 | # 业务消息 558 | if header.ver == ProtoVer.BROTLI: 559 | # 压缩过的先解压,为了避免阻塞网络线程,放在其他线程执行 560 | body = await asyncio.get_running_loop().run_in_executor(None, brotli.decompress, body) 561 | await self._parse_ws_message(body) 562 | elif header.ver == ProtoVer.NORMAL: 563 | # 没压缩过的直接反序列化,因为有万恶的GIL,这里不能并行避免阻塞 564 | if len(body) != 0: 565 | try: 566 | body = json.loads(body.decode('utf-8')) 567 | await self._handle_command(body) 568 | except asyncio.CancelledError: 569 | raise 570 | except Exception: 571 | logger.error('room=%d, body=%s', self.room_id, body) 572 | raise 573 | else: 574 | # 未知格式 575 | logger.warning('room=%d unknown protocol version=%d, header=%s, body=%s', self.room_id, 576 | header.ver, header, body) 577 | 578 | elif header.operation == Operation.AUTH_REPLY: 579 | # 认证响应 580 | body = json.loads(body.decode('utf-8')) 581 | if body['code'] != AuthReplyCode.OK: 582 | raise AuthError(f"auth reply error, code={body['code']}, body={body}") 583 | await self._websocket.send_bytes(self._make_packet({}, Operation.HEARTBEAT)) 584 | 585 | else: 586 | # 未知消息 587 | logger.warning('room=%d unknown message operation=%d, header=%s, body=%s', self.room_id, 588 | header.operation, header, body) 589 | 590 | async def _handle_command(self, command: dict): 591 | """ 592 | 解析并处理业务消息 593 | 594 | :param command: 业务消息 595 | """ 596 | # 外部代码可能不能正常处理取消,所以这里加shield 597 | results = await asyncio.shield( 598 | asyncio.gather( 599 | *(handler.handle(self, command) for handler in self._handlers), return_exceptions=True 600 | ) 601 | ) 602 | for res in results: 603 | if isinstance(res, Exception): 604 | logger.exception('room=%d _handle_command() failed, command=%s', self.room_id, command, exc_info=res) 605 | -------------------------------------------------------------------------------- /Barrage/blivedm/handlers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from typing import * 3 | from loguru import logger 4 | from . import client as client_ 5 | from . import models 6 | 7 | __all__ = ( 8 | 'HandlerInterface', 9 | 'BaseHandler', 10 | ) 11 | 12 | # logger = logging.getLogger('blivedm') 13 | 14 | 15 | IGNORED_CMDS = ( 16 | 'COMBO_SEND', 17 | 'ENTRY_EFFECT', 18 | 'HOT_RANK_CHANGED', 19 | 'HOT_RANK_CHANGED_V2', 20 | 'INTERACT_WORD', 21 | 'LIVE', 22 | 'LIVE_INTERACTIVE_GAME', 23 | 'NOTICE_MSG', 24 | 'ONLINE_RANK_COUNT', 25 | 'ONLINE_RANK_TOP3', 26 | 'ONLINE_RANK_V2', 27 | 'PK_BATTLE_END', 28 | 'PK_BATTLE_FINAL_PROCESS', 29 | 'PK_BATTLE_PROCESS', 30 | 'PK_BATTLE_PROCESS_NEW', 31 | 'PK_BATTLE_SETTLE', 32 | 'PK_BATTLE_SETTLE_USER', 33 | 'PK_BATTLE_SETTLE_V2', 34 | 'PREPARING', 35 | 'ROOM_REAL_TIME_MESSAGE_UPDATE', 36 | 'STOP_LIVE_ROOM_LIST', 37 | 'SUPER_CHAT_MESSAGE_JPN', 38 | 'WIDGET_BANNER', 39 | ) 40 | """常见可忽略的cmd""" 41 | 42 | logged_unknown_cmds = set() 43 | """已打日志的未知cmd""" 44 | 45 | 46 | class HandlerInterface: 47 | """ 48 | 直播消息处理器接口 49 | """ 50 | 51 | async def handle(self, client: client_.BLiveClient, command: dict): 52 | raise NotImplementedError 53 | 54 | 55 | class BaseHandler(HandlerInterface): 56 | """ 57 | 一个简单的消息处理器实现,带消息分发和消息类型转换。继承并重写_on_xxx方法即可实现自己的处理器 58 | """ 59 | 60 | def __heartbeat_callback(self, client: client_.BLiveClient, command: dict): 61 | return self._on_heartbeat(client, models.HeartbeatMessage.from_command(command['data'])) 62 | 63 | def __danmu_msg_callback(self, client: client_.BLiveClient, command: dict): 64 | return self._on_danmaku(client, models.DanmakuMessage.from_command(command['info'], command.get('dm_v2', ''))) 65 | 66 | def __send_gift_callback(self, client: client_.BLiveClient, command: dict): 67 | return self._on_gift(client, models.GiftMessage.from_command(command['data'])) 68 | 69 | def __guard_buy_callback(self, client: client_.BLiveClient, command: dict): 70 | return self._on_buy_guard(client, models.GuardBuyMessage.from_command(command['data'])) 71 | 72 | def __super_chat_message_callback(self, client: client_.BLiveClient, command: dict): 73 | return self._on_super_chat(client, models.SuperChatMessage.from_command(command['data'])) 74 | 75 | def __super_chat_message_delete_callback(self, client: client_.BLiveClient, command: dict): 76 | return self._on_super_chat_delete(client, models.SuperChatDeleteMessage.from_command(command['data'])) 77 | 78 | _CMD_CALLBACK_DICT: Dict[ 79 | str, 80 | Optional[Callable[ 81 | ['BaseHandler', client_.BLiveClient, dict], 82 | Awaitable 83 | ]] 84 | ] = { 85 | # 收到心跳包,这是blivedm自造的消息,原本的心跳包格式不一样 86 | '_HEARTBEAT': __heartbeat_callback, 87 | # 收到弹幕 88 | # go-common\app\service\live\live-dm\service\v1\send.go 89 | 'DANMU_MSG': __danmu_msg_callback, 90 | # 有人送礼 91 | 'SEND_GIFT': __send_gift_callback, 92 | # 有人上舰 93 | 'GUARD_BUY': __guard_buy_callback, 94 | # 醒目留言 95 | 'SUPER_CHAT_MESSAGE': __super_chat_message_callback, 96 | # 删除醒目留言 97 | 'SUPER_CHAT_MESSAGE_DELETE': __super_chat_message_delete_callback, 98 | } 99 | """cmd -> 处理回调""" 100 | # 忽略其他常见cmd 101 | for cmd in IGNORED_CMDS: 102 | _CMD_CALLBACK_DICT[cmd] = None 103 | del cmd 104 | 105 | async def handle(self, client: client_.BLiveClient, command: dict): 106 | cmd = command.get('cmd', '') 107 | pos = cmd.find(':') # 2019-5-29 B站弹幕升级新增了参数 108 | if pos != -1: 109 | cmd = cmd[:pos] 110 | 111 | if cmd not in self._CMD_CALLBACK_DICT: 112 | # 只有第一次遇到未知cmd时打日志 113 | if cmd not in logged_unknown_cmds: 114 | logger.warning(f'room={client.room_id} unknown cmd={cmd}, command={command}') 115 | logged_unknown_cmds.add(cmd) 116 | return 117 | 118 | callback = self._CMD_CALLBACK_DICT[cmd] 119 | if callback is not None: 120 | await callback(self, client, command) 121 | 122 | async def _on_heartbeat(self, client: client_.BLiveClient, message: models.HeartbeatMessage): 123 | """ 124 | 收到心跳包(人气值) 125 | """ 126 | 127 | async def _on_danmaku(self, client: client_.BLiveClient, message: models.DanmakuMessage): 128 | """ 129 | 收到弹幕 130 | """ 131 | 132 | async def _on_gift(self, client: client_.BLiveClient, message: models.GiftMessage): 133 | """ 134 | 收到礼物 135 | """ 136 | 137 | async def _on_buy_guard(self, client: client_.BLiveClient, message: models.GuardBuyMessage): 138 | """ 139 | 有人上舰 140 | """ 141 | 142 | async def _on_super_chat(self, client: client_.BLiveClient, message: models.SuperChatMessage): 143 | """ 144 | 醒目留言 145 | """ 146 | 147 | async def _on_super_chat_delete(self, client: client_.BLiveClient, message: models.SuperChatDeleteMessage): 148 | """ 149 | 删除醒目留言 150 | """ 151 | -------------------------------------------------------------------------------- /Barrage/blivedm/models/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import base64 3 | import binascii 4 | import dataclasses 5 | import json 6 | from typing import * 7 | 8 | from . import pb 9 | 10 | __all__ = ( 11 | 'HeartbeatMessage', 12 | 'DanmakuMessage', 13 | 'GiftMessage', 14 | 'GuardBuyMessage', 15 | 'SuperChatMessage', 16 | 'SuperChatDeleteMessage', 17 | ) 18 | 19 | 20 | @dataclasses.dataclass 21 | class HeartbeatMessage: 22 | """ 23 | 心跳消息 24 | """ 25 | 26 | popularity: int = 0 27 | """人气值""" 28 | 29 | @classmethod 30 | def from_command(cls, data: dict): 31 | return cls( 32 | popularity=data['popularity'], 33 | ) 34 | 35 | 36 | @dataclasses.dataclass 37 | class DanmakuMessage: 38 | """ 39 | 弹幕消息 40 | """ 41 | 42 | mode: int = 0 43 | """弹幕显示模式(滚动、顶部、底部)""" 44 | font_size: int = 0 45 | """字体尺寸""" 46 | color: int = 0 47 | """颜色""" 48 | timestamp: int = 0 49 | """时间戳(毫秒)""" 50 | rnd: int = 0 51 | """随机数,前端叫作弹幕ID,可能是去重用的""" 52 | uid_crc32: str = '' 53 | """用户ID文本的CRC32""" 54 | msg_type: int = 0 55 | """是否礼物弹幕(节奏风暴)""" 56 | bubble: int = 0 57 | """右侧评论栏气泡""" 58 | dm_type: int = 0 59 | """弹幕类型,0文本,1表情,2语音""" 60 | emoticon_options: Union[dict, str] = '' 61 | """表情参数""" 62 | voice_config: Union[dict, str] = '' 63 | """语音参数""" 64 | mode_info: dict = dataclasses.field(default_factory=dict) 65 | """一些附加参数""" 66 | 67 | msg: str = '' 68 | """弹幕内容""" 69 | 70 | uid: int = 0 71 | """用户ID""" 72 | uname: str = '' 73 | """用户名""" 74 | face: str = '' 75 | """用户头像URL""" 76 | admin: int = 0 77 | """是否房管""" 78 | vip: int = 0 79 | """是否月费老爷""" 80 | svip: int = 0 81 | """是否年费老爷""" 82 | urank: int = 0 83 | """用户身份,用来判断是否正式会员,猜测非正式会员为5000,正式会员为10000""" 84 | mobile_verify: int = 0 85 | """是否绑定手机""" 86 | uname_color: str = '' 87 | """用户名颜色""" 88 | 89 | medal_level: str = '' 90 | """勋章等级""" 91 | medal_name: str = '' 92 | """勋章名""" 93 | runame: str = '' 94 | """勋章房间主播名""" 95 | medal_room_id: int = 0 96 | """勋章房间ID""" 97 | mcolor: int = 0 98 | """勋章颜色""" 99 | special_medal: str = '' 100 | """特殊勋章""" 101 | 102 | user_level: int = 0 103 | """用户等级""" 104 | ulevel_color: int = 0 105 | """用户等级颜色""" 106 | ulevel_rank: str = '' 107 | """用户等级排名,>50000时为'>50000'""" 108 | 109 | old_title: str = '' 110 | """旧头衔""" 111 | title: str = '' 112 | """头衔""" 113 | 114 | privilege_type: int = 0 115 | """舰队类型,0非舰队,1总督,2提督,3舰长""" 116 | 117 | @classmethod 118 | def from_command(cls, info: list, dm_v2=''): 119 | proto: Optional[pb.SimpleDm] = None 120 | if dm_v2 != '': 121 | try: 122 | proto = pb.SimpleDm.loads(base64.b64decode(dm_v2)) 123 | except (binascii.Error, KeyError, TypeError, ValueError): 124 | pass 125 | if proto is not None: 126 | face = proto.user.face 127 | else: 128 | face = '' 129 | 130 | if len(info[3]) != 0: 131 | medal_level = info[3][0] 132 | medal_name = info[3][1] 133 | runame = info[3][2] 134 | room_id = info[3][3] 135 | mcolor = info[3][4] 136 | special_medal = info[3][5] 137 | else: 138 | medal_level = 0 139 | medal_name = '' 140 | runame = '' 141 | room_id = 0 142 | mcolor = 0 143 | special_medal = 0 144 | 145 | return cls( 146 | mode=info[0][1], 147 | font_size=info[0][2], 148 | color=info[0][3], 149 | timestamp=info[0][4], 150 | rnd=info[0][5], 151 | uid_crc32=info[0][7], 152 | msg_type=info[0][9], 153 | bubble=info[0][10], 154 | dm_type=info[0][12], 155 | emoticon_options=info[0][13], 156 | voice_config=info[0][14], 157 | mode_info=info[0][15], 158 | 159 | msg=info[1], 160 | 161 | uid=info[2][0], 162 | uname=info[2][1], 163 | face=face, 164 | admin=info[2][2], 165 | vip=info[2][3], 166 | svip=info[2][4], 167 | urank=info[2][5], 168 | mobile_verify=info[2][6], 169 | uname_color=info[2][7], 170 | 171 | medal_level=medal_level, 172 | medal_name=medal_name, 173 | runame=runame, 174 | medal_room_id=room_id, 175 | mcolor=mcolor, 176 | special_medal=special_medal, 177 | 178 | user_level=info[4][0], 179 | ulevel_color=info[4][2], 180 | ulevel_rank=info[4][3], 181 | 182 | old_title=info[5][0], 183 | title=info[5][1], 184 | 185 | privilege_type=info[7], 186 | ) 187 | 188 | @property 189 | def emoticon_options_dict(self) -> dict: 190 | """ 191 | 示例: 192 | {'bulge_display': 0, 'emoticon_unique': 'official_13', 'height': 60, 'in_player_area': 1, 'is_dynamic': 1, 193 | 'url': 'https://i0.hdslb.com/bfs/live/a98e35996545509188fe4d24bd1a56518ea5af48.png', 'width': 183} 194 | """ 195 | if isinstance(self.emoticon_options, dict): 196 | return self.emoticon_options 197 | try: 198 | return json.loads(self.emoticon_options) 199 | except (json.JSONDecodeError, TypeError): 200 | return {} 201 | 202 | @property 203 | def voice_config_dict(self) -> dict: 204 | """ 205 | 示例: 206 | {'voice_url': 'https%3A%2F%2Fboss.hdslb.com%2Flive-dm-voice%2Fb5b26e48b556915cbf3312a59d3bb2561627725945.wav 207 | %3FX-Amz-Algorithm%3DAWS4-HMAC-SHA256%26X-Amz-Credential%3D2663ba902868f12f%252F20210731%252Fshjd%252Fs3%25 208 | 2Faws4_request%26X-Amz-Date%3D20210731T100545Z%26X-Amz-Expires%3D600000%26X-Amz-SignedHeaders%3Dhost%26 209 | X-Amz-Signature%3D114e7cb5ac91c72e231c26d8ca211e53914722f36309b861a6409ffb20f07ab8', 210 | 'file_format': 'wav', 'text': '汤,下午好。', 'file_duration': 1} 211 | """ 212 | if isinstance(self.voice_config, dict): 213 | return self.voice_config 214 | try: 215 | return json.loads(self.voice_config) 216 | except (json.JSONDecodeError, TypeError): 217 | return {} 218 | 219 | 220 | @dataclasses.dataclass 221 | class GiftMessage: 222 | """ 223 | 礼物消息 224 | """ 225 | 226 | gift_name: str = '' 227 | """礼物名""" 228 | num: int = 0 229 | """数量""" 230 | uname: str = '' 231 | """用户名""" 232 | face: str = '' 233 | """用户头像URL""" 234 | guard_level: int = 0 235 | """舰队等级,0非舰队,1总督,2提督,3舰长""" 236 | uid: int = 0 237 | """用户ID""" 238 | timestamp: int = 0 239 | """时间戳""" 240 | gift_id: int = 0 241 | """礼物ID""" 242 | gift_type: int = 0 243 | """礼物类型(未知)""" 244 | action: str = '' 245 | """目前遇到的有'喂食'、'赠送'""" 246 | price: int = 0 247 | """礼物单价瓜子数""" 248 | rnd: str = '' 249 | """随机数,可能是去重用的。有时是时间戳+去重ID,有时是UUID""" 250 | coin_type: str = '' 251 | """瓜子类型,'silver'或'gold',1000金瓜子 = 1元""" 252 | total_coin: int = 0 253 | """总瓜子数""" 254 | tid: str = '' 255 | """可能是事务ID,有时和rnd相同""" 256 | 257 | @classmethod 258 | def from_command(cls, data: dict): 259 | return cls( 260 | gift_name=data['giftName'], 261 | num=data['num'], 262 | uname=data['uname'], 263 | face=data['face'], 264 | guard_level=data['guard_level'], 265 | uid=data['uid'], 266 | timestamp=data['timestamp'], 267 | gift_id=data['giftId'], 268 | gift_type=data['giftType'], 269 | action=data['action'], 270 | price=data['price'], 271 | rnd=data['rnd'], 272 | coin_type=data['coin_type'], 273 | total_coin=data['total_coin'], 274 | tid=data['tid'], 275 | ) 276 | 277 | 278 | @dataclasses.dataclass 279 | class GuardBuyMessage: 280 | """ 281 | 上舰消息 282 | """ 283 | 284 | uid: int = 0 285 | """用户ID""" 286 | username: str = '' 287 | """用户名""" 288 | guard_level: int = 0 289 | """舰队等级,0非舰队,1总督,2提督,3舰长""" 290 | num: int = 0 291 | """数量""" 292 | price: int = 0 293 | """单价金瓜子数""" 294 | gift_id: int = 0 295 | """礼物ID""" 296 | gift_name: str = '' 297 | """礼物名""" 298 | start_time: int = 0 299 | """开始时间戳,和结束时间戳相同""" 300 | end_time: int = 0 301 | """结束时间戳,和开始时间戳相同""" 302 | 303 | @classmethod 304 | def from_command(cls, data: dict): 305 | return cls( 306 | uid=data['uid'], 307 | username=data['username'], 308 | guard_level=data['guard_level'], 309 | num=data['num'], 310 | price=data['price'], 311 | gift_id=data['gift_id'], 312 | gift_name=data['gift_name'], 313 | start_time=data['start_time'], 314 | end_time=data['end_time'], 315 | ) 316 | 317 | 318 | @dataclasses.dataclass 319 | class SuperChatMessage: 320 | """ 321 | 醒目留言消息 322 | """ 323 | 324 | price: int = 0 325 | """价格(人民币)""" 326 | message: str = '' 327 | """消息""" 328 | message_trans: str = '' 329 | """消息日文翻译(目前只出现在SUPER_CHAT_MESSAGE_JPN)""" 330 | start_time: int = 0 331 | """开始时间戳""" 332 | end_time: int = 0 333 | """结束时间戳""" 334 | time: int = 0 335 | """剩余时间(约等于 结束时间戳 - 开始时间戳)""" 336 | id: int = 0 337 | """醒目留言ID,删除时用""" 338 | gift_id: int = 0 339 | """礼物ID""" 340 | gift_name: str = '' 341 | """礼物名""" 342 | uid: int = 0 343 | """用户ID""" 344 | uname: str = '' 345 | """用户名""" 346 | face: str = '' 347 | """用户头像URL""" 348 | guard_level: int = 0 349 | """舰队等级,0非舰队,1总督,2提督,3舰长""" 350 | user_level: int = 0 351 | """用户等级""" 352 | background_bottom_color: str = '' 353 | """底部背景色,'#rrggbb'""" 354 | background_color: str = '' 355 | """背景色,'#rrggbb'""" 356 | background_icon: str = '' 357 | """背景图标""" 358 | background_image: str = '' 359 | """背景图URL""" 360 | background_price_color: str = '' 361 | """背景价格颜色,'#rrggbb'""" 362 | 363 | @classmethod 364 | def from_command(cls, data: dict): 365 | return cls( 366 | price=data['price'], 367 | message=data['message'], 368 | message_trans=data['message_trans'], 369 | start_time=data['start_time'], 370 | end_time=data['end_time'], 371 | time=data['time'], 372 | id=data['id'], 373 | gift_id=data['gift']['gift_id'], 374 | gift_name=data['gift']['gift_name'], 375 | uid=data['uid'], 376 | uname=data['user_info']['uname'], 377 | face=data['user_info']['face'], 378 | guard_level=data['user_info']['guard_level'], 379 | user_level=data['user_info']['user_level'], 380 | background_bottom_color=data['background_bottom_color'], 381 | background_color=data['background_color'], 382 | background_icon=data['background_icon'], 383 | background_image=data['background_image'], 384 | background_price_color=data['background_price_color'], 385 | ) 386 | 387 | 388 | @dataclasses.dataclass 389 | class SuperChatDeleteMessage: 390 | """ 391 | 删除醒目留言消息 392 | """ 393 | 394 | ids: List[int] = dataclasses.field(default_factory=list) 395 | """醒目留言ID数组""" 396 | 397 | @classmethod 398 | def from_command(cls, data: dict): 399 | return cls( 400 | ids=data['ids'], 401 | ) 402 | -------------------------------------------------------------------------------- /Barrage/blivedm/models/pb.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import dataclasses 3 | import enum 4 | from typing import * 5 | 6 | import pure_protobuf.annotations as pb_anno 7 | import pure_protobuf.message as pb_msg 8 | 9 | try: 10 | Annotated 11 | except NameError: 12 | from typing_extensions import Annotated # Python < 3.9 13 | 14 | 15 | @dataclasses.dataclass 16 | class SimpleUser(pb_msg.BaseMessage): 17 | face: Annotated[str, pb_anno.Field(4)] = '' 18 | 19 | 20 | @dataclasses.dataclass 21 | class SimpleDm(pb_msg.BaseMessage): 22 | user: Annotated[SimpleUser, pb_anno.Field(20)] = dataclasses.field(default_factory=SimpleUser) 23 | 24 | 25 | # 26 | # 以下代码是预防以后全量使用Protobuf协议 27 | # 28 | 29 | class BizScene(enum.IntEnum): 30 | None_ = 0 31 | Lottery = 1 32 | Survive = 2 33 | VoiceConn = 3 34 | PlayBack = 4 35 | Vote = 5 36 | 37 | 38 | @dataclasses.dataclass 39 | class Bubble(pb_msg.BaseMessage): 40 | id: Annotated[int, pb_anno.Field(1)] = 0 41 | color: Annotated[str, pb_anno.Field(2)] = '' 42 | id_v2: Annotated[int, pb_anno.Field(3)] = 0 43 | 44 | 45 | class DmType(enum.IntEnum): 46 | Normal = 0 47 | Emoticon = 1 48 | Voice = 2 49 | 50 | 51 | @dataclasses.dataclass 52 | class Emoticon(pb_msg.BaseMessage): 53 | unique: Annotated[str, pb_anno.Field(1)] = '' 54 | url: Annotated[str, pb_anno.Field(2)] = '' 55 | is_dynamic: Annotated[bool, pb_anno.Field(3)] = False 56 | in_player_area: Annotated[int, pb_anno.Field(4)] = 0 57 | bulge_display: Annotated[int, pb_anno.Field(5)] = 0 58 | height: Annotated[int, pb_anno.Field(6)] = 0 59 | width: Annotated[int, pb_anno.Field(7)] = 0 60 | 61 | 62 | # pure_protobuf不支持map的临时解决方案 63 | @dataclasses.dataclass 64 | class EmoticonMapEntry(pb_msg.BaseMessage): 65 | key: Annotated[str, pb_anno.Field(1)] = '' 66 | value: Annotated[Emoticon, pb_anno.Field(2)] = dataclasses.field(default_factory=Emoticon) 67 | 68 | 69 | @dataclasses.dataclass 70 | class Voice(pb_msg.BaseMessage): 71 | url: Annotated[str, pb_anno.Field(1)] = '' 72 | file_format: Annotated[str, pb_anno.Field(2)] = '' 73 | text: Annotated[str, pb_anno.Field(3)] = '' 74 | file_duration: Annotated[int, pb_anno.Field(4)] = 0 75 | file_id: Annotated[str, pb_anno.Field(5)] = '' 76 | 77 | 78 | @dataclasses.dataclass 79 | class Aggregation(pb_msg.BaseMessage): 80 | is_aggregation: Annotated[bool, pb_anno.Field(1)] = False 81 | activity_source: Annotated[int, pb_anno.Field(2)] = 0 82 | activity_identity: Annotated[str, pb_anno.Field(3)] = '' 83 | not_show: Annotated[int, pb_anno.Field(4)] = 0 84 | 85 | 86 | @dataclasses.dataclass 87 | class Check(pb_msg.BaseMessage): 88 | token: Annotated[str, pb_anno.Field(1)] = '' 89 | ts: Annotated[int, pb_anno.Field(2)] = 0 90 | 91 | 92 | @dataclasses.dataclass 93 | class Medal(pb_msg.BaseMessage): 94 | level: Annotated[int, pb_anno.Field(1)] = 0 95 | name: Annotated[str, pb_anno.Field(2)] = '' 96 | special: Annotated[str, pb_anno.Field(3)] = '' 97 | color: Annotated[int, pb_anno.Field(4)] = 0 98 | icon_id: Annotated[int, pb_anno.Field(5)] = 0 99 | border_color: Annotated[int, pb_anno.Field(6)] = 0 100 | gradient_start_color: Annotated[int, pb_anno.Field(7)] = 0 101 | gradient_end_color: Annotated[int, pb_anno.Field(8)] = 0 102 | privilege: Annotated[int, pb_anno.Field(9)] = 0 103 | light: Annotated[int, pb_anno.Field(10)] = 0 104 | 105 | 106 | @dataclasses.dataclass 107 | class UserLevel(pb_msg.BaseMessage): 108 | level: Annotated[int, pb_anno.Field(1)] = 0 109 | color: Annotated[int, pb_anno.Field(2)] = 0 110 | rank: Annotated[str, pb_anno.Field(3)] = '' 111 | online_rank: Annotated[int, pb_anno.Field(4)] = 0 112 | 113 | 114 | @dataclasses.dataclass 115 | class Title(pb_msg.BaseMessage): 116 | title: Annotated[str, pb_anno.Field(1)] = '' 117 | old_title: Annotated[str, pb_anno.Field(2)] = '' 118 | 119 | 120 | @dataclasses.dataclass 121 | class Identify(pb_msg.BaseMessage): 122 | beginning_url: Annotated[str, pb_anno.Field(1)] = '' 123 | ending_url: Annotated[str, pb_anno.Field(2)] = '' 124 | jump_to_url: Annotated[str, pb_anno.Field(3)] = '' 125 | 126 | 127 | @dataclasses.dataclass 128 | class Wealth(pb_msg.BaseMessage): 129 | level: Annotated[int, pb_anno.Field(1)] = 0 130 | 131 | 132 | @dataclasses.dataclass 133 | class User(pb_msg.BaseMessage): 134 | uid: Annotated[int, pb_anno.Field(1)] = 0 135 | name: Annotated[str, pb_anno.Field(2)] = '' 136 | name_color: Annotated[str, pb_anno.Field(3)] = '' 137 | face: Annotated[str, pb_anno.Field(4)] = '' 138 | vip: Annotated[int, pb_anno.Field(5)] = 0 139 | svip: Annotated[int, pb_anno.Field(6)] = 0 140 | rank: Annotated[int, pb_anno.Field(7)] = 0 141 | mobile_verify: Annotated[int, pb_anno.Field(8)] = 0 142 | lpl_status: Annotated[int, pb_anno.Field(9)] = 0 143 | attr: Annotated[int, pb_anno.Field(10)] = 0 144 | medal: Annotated[Medal, pb_anno.Field(11)] = dataclasses.field(default_factory=Medal) 145 | level: Annotated[UserLevel, pb_anno.Field(12)] = dataclasses.field(default_factory=UserLevel) 146 | title: Annotated[Title, pb_anno.Field(13)] = dataclasses.field(default_factory=Title) 147 | identify: Annotated[Identify, pb_anno.Field(14)] = dataclasses.field(default_factory=Identify) 148 | wealth: Annotated[Wealth, pb_anno.Field(15)] = dataclasses.field(default_factory=Wealth) 149 | 150 | 151 | @dataclasses.dataclass 152 | class Room(pb_msg.BaseMessage): 153 | uid: Annotated[int, pb_anno.Field(1)] = 0 154 | name: Annotated[str, pb_anno.Field(2)] = '' 155 | 156 | 157 | @dataclasses.dataclass 158 | class Prefix(pb_msg.BaseMessage): 159 | type: Annotated[int, pb_anno.Field(1)] = 0 160 | resource: Annotated[str, pb_anno.Field(2)] = '' 161 | 162 | 163 | @dataclasses.dataclass 164 | class Icon(pb_msg.BaseMessage): 165 | prefix: Annotated[Prefix, pb_anno.Field(1)] = dataclasses.field(default_factory=Prefix) 166 | 167 | 168 | @dataclasses.dataclass 169 | class Dm(pb_msg.BaseMessage): 170 | id_str: Annotated[str, pb_anno.Field(1)] = '' 171 | mode: Annotated[int, pb_anno.Field(2)] = 0 172 | fontsize: Annotated[int, pb_anno.Field(3)] = 0 173 | color: Annotated[pb_anno.uint, pb_anno.Field(4)] = 0 174 | mid_hash: Annotated[str, pb_anno.Field(5)] = '' 175 | content: Annotated[str, pb_anno.Field(6)] = '' 176 | ctime: Annotated[int, pb_anno.Field(7)] = 0 177 | weight: Annotated[int, pb_anno.Field(8)] = 0 178 | rnd: Annotated[int, pb_anno.Field(9)] = 0 179 | attr: Annotated[int, pb_anno.Field(10)] = 0 180 | # 为了防止加新枚举后不兼容,还是用int了 181 | # biz_scene: Annotated[BizScene, pb_anno.Field(11)] = BizScene.None_ 182 | biz_scene: Annotated[int, pb_anno.Field(11)] = 0 183 | bubble: Annotated[Bubble, pb_anno.Field(12)] = dataclasses.field(default_factory=Bubble) 184 | # dm_type: Annotated[DmType, pb_anno.Field(13)] = DmType.Normal 185 | dm_type: Annotated[int, pb_anno.Field(13)] = 0 186 | emoticons: Annotated[List[EmoticonMapEntry], pb_anno.Field(14)] = dataclasses.field(default_factory=list) 187 | voice: Annotated[Voice, pb_anno.Field(15)] = dataclasses.field(default_factory=Voice) 188 | animation: Annotated[str, pb_anno.Field(16)] = '' 189 | aggregation: Annotated[Aggregation, pb_anno.Field(17)] = dataclasses.field(default_factory=Aggregation) 190 | send_from_me: Annotated[bool, pb_anno.Field(18)] = False 191 | check: Annotated[Check, pb_anno.Field(19)] = dataclasses.field(default_factory=Check) 192 | user: Annotated[User, pb_anno.Field(20)] = dataclasses.field(default_factory=User) 193 | room: Annotated[Room, pb_anno.Field(21)] = dataclasses.field(default_factory=Room) 194 | icon: Annotated[Icon, pb_anno.Field(22)] = dataclasses.field(default_factory=Icon) 195 | -------------------------------------------------------------------------------- /Dictionary/Tiktok/Gift.py: -------------------------------------------------------------------------------- 1 | class Gift: 2 | def __init__(self): 3 | self.TIKTOK_GIFTS = [{'name': '小心心', 'coin': 1}, {'name': '玫瑰', 'coin': 1}, {'name': '玫瑰花', 'coin': 1}, 4 | {'name': '抖音', 'coin': 1}, {'name': '人气TOP1', 'coin': 1}, {'name': '小皇冠', 'coin': 10}, 5 | {'name': '人气票', 'coin': 1}, {'name': '粉丝团灯牌', 'coin': 1}, 6 | {'name': '天鹅之梦', 'coin': 50}, {'name': '大啤酒', 'coin': 2}, {'name': '你最好看', 'coin': 2}, 7 | {'name': '棒棒糖', 'coin': 9}, {'name': '传承印记', 'coin': 9}, 8 | {'name': '为你点亮', 'coin': 9}, {'name': '鲜花', 'coin': 10}, 9 | {'name': '加油鸭', 'coin': 15}, {'name': '送你花花', 'coin': 49}, 10 | {'name': '爱你哟', 'coin': 52}, {'name': 'Thuglife', 'coin': 99}, 11 | {'name': '游戏手柄', 'coin': 99}, {'name': '爱的纸鹤', 'coin': 99}, 12 | {'name': '亲吻', 'coin': 99}, {'name': '闪耀星辰', 'coin': 99}, 13 | {'name': '荧光棒', 'coin': 99}, {'name': '捏捏喵脸', 'coin': 99}, 14 | {'name': '黄桃罐头', 'coin': 99}, {'name': '多喝热水', 'coin': 126}, 15 | {'name': '比心', 'coin': 199}, {'name': '礼花筒', 'coin': 199}, 16 | {'name': '为你举牌', 'coin': 199}, {'name': '浪漫寻蜜', 'coin': 255}, 17 | {'name': '星星点灯', 'coin': 268}, {'name': '蝶 · 连理枝', 'coin': 280}, 18 | {'name': '比心兔兔', 'coin': 299}, {'name': 'ONE礼挑一', 'coin': 299}, 19 | {'name': '爱的守护', 'coin': 299}, {'name': '猜猜我是谁', 'coin': 321}, 20 | {'name': '真爱玫瑰', 'coin': 366}, {'name': '一束花开', 'coin': 366}, 21 | {'name': '花开烂漫', 'coin': 466}, {'name': '热气球', 'coin': 520}, 22 | {'name': '真的爱你', 'coin': 520}, {'name': '一直陪伴你', 'coin': 520}, 23 | {'name': '浪漫花火', 'coin': 599}, {'name': '娶你回家', 'coin': 599}, 24 | {'name': '环球旅行车', 'coin': 650}, {'name': '万象烟花', 'coin': 688}, 25 | {'name': '日出相伴', 'coin': 726}, {'name': '蝶 · 书中情', 'coin': 750}, 26 | {'name': '掌上明珠', 'coin': 888}, {'name': '纸短情长', 'coin': 921}, 27 | {'name': '爱的发射', 'coin': 999}, {'name': '保时捷', 'coin': 1200}, 28 | {'name': '鱼你相伴', 'coin': 1388}, {'name': '花落长亭', 'coin': 1588}, 29 | {'name': '浪漫营地', 'coin': 1699}, {'name': '蝶 · 比翼鸟', 'coin': 1700}, 30 | {'name': '单车恋人', 'coin': 1899}, {'name': '浪漫恋人', 'coin': 1999}, 31 | {'name': '时尚代言', 'coin': 2188}, {'name': '太空喵喵', 'coin': 2333}, 32 | {'name': '爱的转圈圈', 'coin': 2333}, {'name': '奇幻八音盒', 'coin': 2399}, 33 | {'name': '花海泛舟', 'coin': 2800}, {'name': '直升机', 'coin': 2999}, 34 | {'name': '动次打次', 'coin': 2999}, {'name': '私人飞机', 'coin': 3000}, 35 | {'name': '薰衣草庄园', 'coin': 3300}, {'name': '奏响人生', 'coin': 3666}, 36 | {'name': '浪漫列车', 'coin': 3999}, {'name': '挖掘惊喜', 'coin': 3999}, 37 | {'name': '海上生明月', 'coin': 4166}, {'name': '告白惊喜', 'coin': 4214}, 38 | {'name': '心动丘比特', 'coin': 4321}, {'name': '星河相望', 'coin': 4520}, 39 | {'name': '奇幻花潮', 'coin': 4520}, {'name': 'Disco', 'coin': 4999}, 40 | {'name': '璧上飞仙', 'coin': 4999}, {'name': '天空之镜', 'coin': 6399}, 41 | {'name': '月下瀑布', 'coin': 6666}, {'name': '糖果大炮', 'coin': 6666}, 42 | {'name': '蝶 · 寄相思', 'coin': 6800}, {'name': '星际玫瑰', 'coin': 7500}, 43 | {'name': '云霄大厦', 'coin': 7888}, {'name': '摩天大厦', 'coin': 8222}, 44 | {'name': '月伴星辰', 'coin': 8666}, {'name': '甜蜜送达', 'coin': 8888}, 45 | {'name': '真爱永恒', 'coin': 8999}, {'name': '情定三生', 'coin': 9666}, 46 | {'name': '抖音1号', 'coin': 10001}, {'name': '为爱启航', 'coin': 10001}, 47 | {'name': '云中秘境', 'coin': 13140}, {'name': '糖果飞船', 'coin': 13140}, 48 | {'name': '陪你的季节', 'coin': 15999}, {'name': '宇宙之心', 'coin': 18888}, 49 | {'name': '梦幻城堡', 'coin': 28888}, {'name': '浪漫马车', 'coin': 28888}, 50 | {'name': '嘉年华', 'coin': 30000}] 51 | 52 | def is_gift_exist(self, gift_name): 53 | for gift in self.TIKTOK_GIFTS: 54 | if gift['name'] == gift_name: 55 | return True 56 | return False 57 | -------------------------------------------------------------------------------- /Dictionary/Tiktok/__init__.py: -------------------------------------------------------------------------------- 1 | from .Gift import Gift 2 | -------------------------------------------------------------------------------- /Dictionary/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwaggyMacro/BarrageGPT/4624fdaaf5358e676f411d545c0864805d12a628/Dictionary/__init__.py -------------------------------------------------------------------------------- /GPT/Chat.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | 4 | import openai 5 | 6 | from Utils import Config 7 | 8 | 9 | class Chat: 10 | 11 | def __init__(self, api_key: str): 12 | self.api_key = api_key 13 | openai.api_key = api_key 14 | 15 | if Config().is_api_proxy_on(): 16 | openai.api_base = Config().get_api_proxy_url() 17 | 18 | def chat(self, question: str, role: str = None, stream: bool = False, model: str = 'gpt-3.5-turbo') -> str: 19 | return stream and self.__chat_stream(question, model, role) or self.__chat_no_stream(question, model, role) 20 | 21 | def __chat_stream(self, question: str, model: str, role: str = None) -> str: 22 | """ 23 | :param question: what you want to ask for 24 | :param role: the role of the question, if None, the role will be none. 25 | """ 26 | start_time = time.time() 27 | response = openai.ChatCompletion.create( 28 | model=model, 29 | messages=role is not None and [ 30 | {"role": "system", "content": role}, 31 | {"role": "user", "content": question} 32 | ] or [ 33 | {"role": "user", "content": question} 34 | ], 35 | stream=True 36 | ) 37 | # create variables to collect the stream of chunks 38 | collected_chunks = [] 39 | collected_messages = [] 40 | # iterate through the stream of events 41 | try: 42 | for chunk in response: 43 | if chunk['choices'][0]['finish_reason'] == 'stop': 44 | break 45 | chunk_time = time.time() - start_time # calculate the time delay of the chunk 46 | collected_chunks.append(chunk) # save the event response 47 | chunk_message = chunk['choices'][0]['delta']['content'] # extract the message 48 | collected_messages.append(chunk_message) # save the message 49 | # print(f"Message received {chunk_time:.2f} seconds after request: {chunk_message}") 50 | # print the delay and text 51 | yield chunk_message 52 | except Exception as e: 53 | logging.error(e) 54 | logging.error(collected_chunks) 55 | logging.error(collected_messages) 56 | 57 | def __chat_no_stream(self, question: str, model: str, role: str = None) -> str: 58 | response = openai.ChatCompletion.create( 59 | model=model, 60 | messages=role is not None and [ 61 | {"role": "system", "content": role}, 62 | {"role": "user", "content": question} 63 | ] or [ 64 | {"role": "user", "content": question} 65 | ], 66 | stream=False 67 | ) 68 | return response['choices'][0]['message']['content'] 69 | -------------------------------------------------------------------------------- /GPT/__init__.py: -------------------------------------------------------------------------------- 1 | from .Chat import Chat 2 | -------------------------------------------------------------------------------- /Model/Huya/BarrageMessage.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | import json 5 | 6 | class BarrageMessage: 7 | def __init__(self, type, room_id, timestamp, uid, nickname, msg_content=None, face=None, gift_type=None, gfid=None, gfcnt=None, gift_name=None, gift_icon=None, price_big=None, price_total=None): 8 | self.type = type 9 | self.room_id = room_id 10 | self.timestamp = timestamp 11 | self.uid = uid 12 | self.nickname = nickname 13 | self.msg_content = msg_content 14 | self.face = face 15 | self.gift_type = gift_type 16 | self.gfid = gfid 17 | self.gfcnt = gfcnt 18 | self.gift_name = gift_name 19 | self.gift_icon = gift_icon 20 | self.price_big = price_big 21 | self.price_total = price_total 22 | 23 | @classmethod 24 | def from_json(cls, json_str): 25 | data = json.loads(json_str) 26 | if data["type"] == "chat": 27 | data = data['data'] 28 | return cls( 29 | "chat", 30 | data.get("room_id", None), 31 | data.get("timestamp", None), 32 | data.get("uid", None), 33 | data.get("nickname", None), 34 | data.get("msg_content", None), 35 | data.get("face", None) 36 | ) 37 | elif data["type"] == "gift": 38 | data = data['data'] 39 | return cls( 40 | "gift", 41 | data.get("room_id", None), 42 | data.get("timestamp", None), 43 | data.get("uid", None), 44 | data.get("nickname", None), 45 | None, # chat 类型没有这些属性,因此设置为 None 46 | data.get("face", None), 47 | data.get("gift_type", None), 48 | data.get("gfid", None), 49 | data.get("gfcnt", None), 50 | data.get("gift_name", None), 51 | data.get("gift_icon", None), 52 | data.get("price_big", None), 53 | data.get("price_total", None) 54 | ) 55 | 56 | def to_json(self): 57 | data = { 58 | "type": self.type, 59 | "room_id": self.room_id, 60 | "timestamp": self.timestamp, 61 | "uid": self.uid, 62 | "nickname": self.nickname, 63 | } 64 | if self.type == "chat": 65 | data["msg_content"] = self.msg_content 66 | data["face"] = self.face 67 | elif self.type == "gift": 68 | data["gfid"] = self.gfid 69 | data["gfcnt"] = self.gfcnt 70 | data["gift_name"] = self.gift_name 71 | data["gift_icon"] = self.gift_icon 72 | data["price_big"] = self.price_big 73 | data["price_total"] = self.price_total 74 | 75 | return json.dumps(data) 76 | 77 | def __str__(self): 78 | return self.to_json() 79 | -------------------------------------------------------------------------------- /Model/Huya/__init__.py: -------------------------------------------------------------------------------- 1 | from .BarrageMessage import BarrageMessage 2 | -------------------------------------------------------------------------------- /Model/TikTok/BarrageMessage.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | class BarrageMessage: 5 | def __init__(self, data): 6 | data = json.loads(data) 7 | self.action = data.get("action") 8 | message_data = data.get("message", {}) 9 | self.repeat_count = message_data.get("repeatCount") 10 | self.gift_id = message_data.get("gift_id") 11 | self.gift_name = message_data.get("gift_name") 12 | self.gift_number = message_data.get("gift_number") 13 | self.gift_image = message_data.get("gift_image") 14 | self.gift_diamond_count = message_data.get("gift_diamondCount") 15 | self.gift_describe = message_data.get("gift_describe") 16 | self.user_level = message_data.get("user_level") 17 | self.user_fans_level = message_data.get("user_fansLevel") 18 | self.user_id = message_data.get("user_id") 19 | self.user_nickname = message_data.get("user_nickName") 20 | self.user_avatar = message_data.get("user_avatar") 21 | self.user_gender = message_data.get("user_gender") 22 | self.user_isAdmin = message_data.get("user_isAdmin") 23 | self.user_fans_lightName = message_data.get("user_fansLightName") 24 | self.user_level_image = message_data.get("user_levelImage") 25 | self.msg_content = message_data.get("msg_content") 26 | self.isGift = message_data.get("isGift") 27 | 28 | def __str__(self): 29 | attributes = vars(self) 30 | attribute_str = ", ".join(f"{key}={value}" for key, value in attributes.items()) 31 | return f"BarrageMessage({attribute_str})" 32 | -------------------------------------------------------------------------------- /Model/TikTok/__init__.py: -------------------------------------------------------------------------------- 1 | from .BarrageMessage import BarrageMessage 2 | -------------------------------------------------------------------------------- /Model/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwaggyMacro/BarrageGPT/4624fdaaf5358e676f411d545c0864805d12a628/Model/__init__.py -------------------------------------------------------------------------------- /Readme/readme_bilibili.md: -------------------------------------------------------------------------------- 1 | ### 哔哩哔哩操作步骤 2 | *** 3 | 1. 项目根目录下执行 4 | ```shell 5 | python bilibili.py 房间号 6 | ``` 7 | 2. OBS推流选中项目软件窗口 8 | *** 9 | bilibili引用Repo: 10 | - blivedm: https://github.com/xfgryujk/blivedm 11 | -------------------------------------------------------------------------------- /Readme/readme_huya.md: -------------------------------------------------------------------------------- 1 | ### 虎牙操作步骤 2 | *** 3 | `cd`至项目根目录下`ThirdLib/huya_barrage`文件夹下 4 | 1. 打开项目跟目录 安装 node 依赖 并运行,指定参数为需要监听的虎牙直播房间号 5 | 6 | ```shell 7 | npm i // 或 cnpm i 8 | npm install huya-danmu --save 9 | npm run dev 9927 // 9927为虎牙直播房间号 10 | ``` 11 | 2. 项目根目录下执行 12 | ```shell 13 | python huya.py 14 | ``` 15 | 3. OBS推流选中项目软件窗口 16 | *** 17 | 虎牙引用Repo: 18 | - huya_barrage(无法接收礼物消息): https://github.com/Kain-90/huya-danmu 19 | - huya_barrage_bravo(部分直播间无法接收信息, 可能只能接收有热度的直播间): https://github.com/hwenjie/huya_danmu 20 | 21 | 本项目使用的是修改过后的huya_barrage,可以正常接收弹幕以及礼物消息。 -------------------------------------------------------------------------------- /Readme/readme_tiktok.md: -------------------------------------------------------------------------------- 1 | ### 抖音操作步骤 2 | *** 3 | `cd`至项目根目录下`ThirdLib/tiktok_barrage_nodejs`文件夹下 4 | 1. 打开项目跟目录 安装 node 依赖 并运行 5 | 6 | ```shell 7 | npm i // 或 cnpm i 8 | npm run dev 9 | ``` 10 | 11 | 2. 打开浏览器进入需要接受弹幕消息的直播间页面,按 F12 控制台 注入下面代码 12 | 13 | ```javascript 14 | var scriptElement = document.createElement('script') 15 | scriptElement.src = 'https://swaggymacro.github.io/tiktok_barrage_nodejs/client.js?t=' + Math.random() 16 | document.body.appendChild(scriptElement) 17 | ``` 18 | 19 | 3. 项目根目录下执行 20 | ```shell 21 | python tiktok.py 22 | ``` 23 | 24 | 4. OBS推流选中项目软件窗口 25 | 26 | *** 27 | 原Repo地址: https://github.com/jiansenc/tiktok_barrage_nodejs -------------------------------------------------------------------------------- /Resources/Images/ChatGPT_logo.svg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwaggyMacro/BarrageGPT/4624fdaaf5358e676f411d545c0864805d12a628/Resources/Images/ChatGPT_logo.svg.png -------------------------------------------------------------------------------- /Resources/Images/ai_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwaggyMacro/BarrageGPT/4624fdaaf5358e676f411d545c0864805d12a628/Resources/Images/ai_logo.png -------------------------------------------------------------------------------- /Resources/Images/bilibili_default_avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwaggyMacro/BarrageGPT/4624fdaaf5358e676f411d545c0864805d12a628/Resources/Images/bilibili_default_avatar.jpg -------------------------------------------------------------------------------- /Resources/Images/download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwaggyMacro/BarrageGPT/4624fdaaf5358e676f411d545c0864805d12a628/Resources/Images/download.png -------------------------------------------------------------------------------- /Resources/Images/huya_default_avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwaggyMacro/BarrageGPT/4624fdaaf5358e676f411d545c0864805d12a628/Resources/Images/huya_default_avatar.png -------------------------------------------------------------------------------- /Resources/Images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwaggyMacro/BarrageGPT/4624fdaaf5358e676f411d545c0864805d12a628/Resources/Images/logo.png -------------------------------------------------------------------------------- /Resources/Images/tiktok_default_avatar.jfif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwaggyMacro/BarrageGPT/4624fdaaf5358e676f411d545c0864805d12a628/Resources/Images/tiktok_default_avatar.jfif -------------------------------------------------------------------------------- /Screenshots/Pictures/bilibili.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwaggyMacro/BarrageGPT/4624fdaaf5358e676f411d545c0864805d12a628/Screenshots/Pictures/bilibili.png -------------------------------------------------------------------------------- /Screenshots/Pictures/huya.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwaggyMacro/BarrageGPT/4624fdaaf5358e676f411d545c0864805d12a628/Screenshots/Pictures/huya.png -------------------------------------------------------------------------------- /Screenshots/Videos/bilibili.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwaggyMacro/BarrageGPT/4624fdaaf5358e676f411d545c0864805d12a628/Screenshots/Videos/bilibili.mp4 -------------------------------------------------------------------------------- /Screenshots/Videos/huya.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwaggyMacro/BarrageGPT/4624fdaaf5358e676f411d545c0864805d12a628/Screenshots/Videos/huya.mp4 -------------------------------------------------------------------------------- /ThirdLib/huya_barrage/app.js: -------------------------------------------------------------------------------- 1 | const huya_danmu = require('./index') 2 | 3 | const roomid = process.argv[2]; 4 | 5 | if (!roomid) { 6 | console.error('Roomid is required'); 7 | process.exit(1); 8 | } 9 | 10 | const client = new huya_danmu(roomid) 11 | 12 | const WebSocket = require('ws'); 13 | const http = require('http'); 14 | 15 | const httpServer = http.createServer((req, res) => { 16 | res.writeHead(200, { 'Content-Type': 'text/plain' }); 17 | res.end('WebSocket server is running'); 18 | }); 19 | 20 | const wss = new WebSocket.Server({ server: httpServer }); 21 | 22 | // listen port from command line argument, default 9588 23 | const port = process.argv[3] || 9528; 24 | 25 | client.on('connect', () => { 26 | console.log(`已连接huya ${roomid}房间弹幕~`) 27 | }) 28 | 29 | client.on('chatRecv', msg => { 30 | console.log(`[${msg.nickname}]:${msg.msg_content}`) 31 | broadcastMessage('chat', msg); 32 | }) 33 | client.on('giftRecv', msg => { 34 | console.log(`[${msg.nickname}]->赠送${msg.gfcnt}个${msg.gift_name}`) 35 | broadcastMessage('gift', msg); 36 | }) 37 | 38 | client.on('onlineRecv', msg => { 39 | console.log(`[当前人气]:${msg.count}`) 40 | }) 41 | 42 | client.on('error', e => { 43 | console.log(e) 44 | broadcastMessage('error', e); 45 | }) 46 | 47 | client.on('close', e => { 48 | console.log('close,未找到直播间信息,可能是主播未开播或直播间不存在。') 49 | broadcastMessage('close', '未找到直播间信息,可能是主播未开播或直播间不存在。'); 50 | }) 51 | 52 | client.start() 53 | 54 | wss.on('connection', (ws) => { 55 | console.log('WebSocket client connected'); 56 | 57 | ws.on('message', (message) => { 58 | console.log(`Received message: ${message}`); 59 | }); 60 | 61 | ws.on('close', () => { 62 | console.log('WebSocket client disconnected'); 63 | }); 64 | }); 65 | 66 | function broadcastMessage(type, data) { 67 | const message = JSON.stringify({ type, data }); 68 | wss.clients.forEach(client => { 69 | if (client.readyState === WebSocket.OPEN) { 70 | client.send(message); 71 | } 72 | }); 73 | } 74 | 75 | httpServer.listen(port, () => { 76 | console.log(`WebSocket server is listening on ${port}`); 77 | }); 78 | 79 | -------------------------------------------------------------------------------- /ThirdLib/huya_barrage/index.js: -------------------------------------------------------------------------------- 1 | const ws = require('ws') 2 | const md5 = require('md5') 3 | const events = require('events') 4 | const request = require('request-promise') 5 | const to_arraybuffer = require('to-arraybuffer') 6 | const socks_agent = require('socks-proxy-agent') 7 | const { Taf, TafMx, HUYA, List } = require('./lib') 8 | 9 | const timeout = 30000 10 | const heartbeat_interval = 60000 11 | const fresh_gift_interval = 60 * 60 * 1000 12 | const r = request.defaults({ json: true, gzip: true, timeout: timeout, headers: { 'User-Agent': 'Mozilla/5.0 (Linux; Android 5.1.1; Nexus 6 Build/LYZ28E) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Mobile Safari/537.36' } }) 13 | 14 | class huya_danmu extends events { 15 | 16 | constructor(opt) { 17 | super() 18 | if (typeof opt === 'string') 19 | this._roomid = opt 20 | else if (typeof opt === 'object') { 21 | this._roomid = opt.roomid 22 | this.set_proxy(opt.proxy) 23 | } 24 | this._gift_info = {} 25 | this._chat_list = new List() 26 | this._emitter = new events.EventEmitter() 27 | } 28 | 29 | set_proxy(proxy) { 30 | this._agent = new socks_agent(proxy) 31 | } 32 | 33 | async _get_chat_info() { 34 | try { 35 | let body = await r({ 36 | url: `https://m.huya.com/${this._roomid}`, 37 | agent: this._agent 38 | }) 39 | let info = {} 40 | let subsid_array = body.match(/"lSubChannelId":(.*);/) 41 | let topsid_array = body.match(/"lChannelId":(.*);/) 42 | let yyuid_array = body.match(/"lYyid":(.*),/) 43 | if (!subsid_array || !topsid_array || !yyuid_array) return 44 | info.subsid = subsid_array[1] === '' ? 0 : parseInt(subsid_array[1]) 45 | info.topsid = topsid_array[1] === '' ? 0 : parseInt(topsid_array[1]) 46 | info.yyuid = parseInt(yyuid_array[1]) 47 | return info 48 | } catch (e) { 49 | this.emit('error', new Error('Fail to get info')) 50 | } 51 | } 52 | 53 | async start() { 54 | if (this._starting) return 55 | this._starting = true 56 | this._info = await this._get_chat_info() 57 | if (!this._info) return this.emit('close') 58 | this._main_user_id = new HUYA.UserId() 59 | this._main_user_id.lUid = this._info.yyuid 60 | this._main_user_id.sHuYaUA = "webh5&1.0.0&websocket" 61 | this._start_ws() 62 | } 63 | 64 | 65 | _start_ws() { 66 | this._client = new ws('ws://ws.api.huya.com', { 67 | perMessageDeflate: false, 68 | agent: this._agent 69 | }) 70 | this._client.on('open', () => { 71 | this._get_gift_list() 72 | this._bind_ws_info() 73 | this._heartbeat() 74 | this._heartbeat_timer = setInterval(this._heartbeat.bind(this), heartbeat_interval) 75 | this._fresh_gift_list_timer = setInterval(this._get_gift_list.bind(this), fresh_gift_interval) 76 | this.emit('connect') 77 | }) 78 | this._client.on('error', err => { 79 | this.emit('error', err) 80 | }) 81 | this._client.on('close', async () => { 82 | this._stop() 83 | this.emit('close') 84 | }) 85 | this._client.on('message', this._on_mes.bind(this)) 86 | 87 | this._emitter.on("8006", msg => { 88 | const msg_obj = { 89 | type: 'online', 90 | time: new Date().getTime(), 91 | count: msg.iAttendeeCount 92 | } 93 | this.emit('onlineRecv', msg_obj) 94 | }) 95 | this._emitter.on("1400", msg => { 96 | const msg_obj = { 97 | room_id: this._roomid, 98 | timestamp: new Date().getTime()+"", 99 | nickname: msg.tUserInfo.sNickName, 100 | uid: msg.tUserInfo.lUid + '', 101 | id: md5(JSON.stringify(msg)), 102 | msg_content: msg.sContent, 103 | face: msg.tUserInfo.sAvatarUrl 104 | } 105 | // const can_emit = this._chat_list.push(msg_obj.from.rid + msg_obj.content, msg_obj.time) 106 | // can_emit && this.emit('chatRecv', msg_obj) 107 | this.emit('chatRecv', msg_obj) 108 | }) 109 | this._emitter.on("onGift", msg => { 110 | // if (msg.lPresenterUid != this._info.yyuid) return 111 | // if dont anontion the line above, will not get the gift info. 112 | let gift = this._gift_info[msg.iItemType + ''] || { name: '未知礼物', price: 0 } 113 | let id = md5(JSON.stringify(msg)) 114 | let msg_obj = { 115 | room_id: this._roomid, 116 | timestamp: new Date().getTime()+"", 117 | uid: msg.lSenderUid+"", 118 | nickname: msg.sSenderNick, 119 | face: msg.iSenderIcon, 120 | gift_type: msg.mcs, 121 | gfid: msg.iItemType, 122 | gfcnt: msg.iItemCount, 123 | gift_name: gift.name, 124 | gift_icon: gift.icon, 125 | price_big: gift.price, 126 | price_total: msg.iItemCount * gift.price 127 | } 128 | this.emit('giftRecv', msg_obj) 129 | }) 130 | this._emitter.on("getPropsList", msg => { 131 | msg.vPropsItemList.value.forEach(item => { 132 | let name = item.sPropsName, icon = ''; 133 | try{ 134 | name = item.vPropView.value[0].name; 135 | icon = item.vPropsIdentity.value[0].sPropsWeb.split("&")[0]; 136 | this._gift_info[item.iPropsId + ''] = { 137 | name: item.vPropView.value[0].name, 138 | price: item.iPropsYb / 100, 139 | icon 140 | } 141 | }catch (e) { 142 | } 143 | }) 144 | }) 145 | } 146 | 147 | _get_gift_list() { 148 | let prop_req = new HUYA.GetPropsListReq() 149 | prop_req.tUserId = this._main_user_id 150 | prop_req.iTemplateType = HUYA.EClientTemplateType.TPL_MIRROR 151 | this._send_wup("PropsUIServer", "getPropsList", prop_req) 152 | } 153 | 154 | _bind_ws_info() { 155 | let ws_user_info = new HUYA.WSUserInfo; 156 | ws_user_info.lUid = this._info.yyuid 157 | ws_user_info.bAnonymous = 0 == this._info.yyuid 158 | ws_user_info.sGuid = this._main_user_id.sGuid 159 | ws_user_info.sToken = "" 160 | ws_user_info.lTid = this._info.topsid 161 | ws_user_info.lSid = this._info.subsid 162 | ws_user_info.lGroupId = this._info.yyuid 163 | ws_user_info.lGroupType = 3 164 | let jce_stream = new Taf.JceOutputStream() 165 | ws_user_info.writeTo(jce_stream) 166 | let ws_command = new HUYA.WebSocketCommand() 167 | ws_command.iCmdType = HUYA.EWebSocketCommandType.EWSCmd_RegisterReq 168 | ws_command.vData = jce_stream.getBinBuffer() 169 | jce_stream = new Taf.JceOutputStream() 170 | ws_command.writeTo(jce_stream) 171 | this._client.send(jce_stream.getBuffer()) 172 | } 173 | 174 | _heartbeat() { 175 | let heart_beat_req = new HUYA.UserHeartBeatReq() 176 | let user_id = new HUYA.UserId() 177 | user_id.sHuYaUA = "webh5&1.0.0&websocket" 178 | heart_beat_req.tId = user_id 179 | heart_beat_req.lTid = this._info.topsid 180 | heart_beat_req.lSid = this._info.subsid 181 | heart_beat_req.lPid = this._info.yyuid 182 | heart_beat_req.eLineType = 1 183 | heart_beat_req.lShortTid = 0; 184 | heart_beat_req.bWatchVideo = true; 185 | heart_beat_req.eLineType = HUYA.EStreamLineType.STREAM_LINE_AL; 186 | heart_beat_req.iFps = 0; 187 | heart_beat_req.iAttendee = 0; 188 | heart_beat_req.iLastHeartElapseTime = 0; 189 | this._send_wup("onlineui", "OnUserHeartBeat", heart_beat_req) 190 | } 191 | 192 | _on_mes(data) { 193 | try { 194 | data = to_arraybuffer(data) 195 | let stream = new Taf.JceInputStream(data) 196 | let command = new HUYA.WebSocketCommand() 197 | command.readFrom(stream) 198 | switch (command.iCmdType) { 199 | case HUYA.EWebSocketCommandType.EWSCmd_WupRsp: 200 | let wup = new Taf.Wup() 201 | wup.decode(command.vData.buffer) 202 | let map = new (TafMx.WupMapping[wup.sFuncName])() 203 | wup.readStruct('tRsp', map, TafMx.WupMapping[wup.sFuncName]) 204 | this._emitter.emit(wup.sFuncName, map) 205 | break 206 | case HUYA.EWebSocketCommandType.EWSCmdS2C_MsgPushReq: 207 | stream = new Taf.JceInputStream(command.vData.buffer) 208 | let msg = new HUYA.WSPushMessage() 209 | msg.readFrom(stream) 210 | let mcs = msg.iUri 211 | stream = new Taf.JceInputStream(msg.sMsg.buffer) 212 | if (TafMx.UriMapping[msg.iUri]) { 213 | let map = new (TafMx.UriMapping[msg.iUri])() 214 | map.readFrom(stream) 215 | if (mcs == 6501 || mcs == 6502 || mcs == 6507) { 216 | map.mcs = mcs 217 | this._emitter.emit("onGift", map) 218 | } 219 | this._emitter.emit(msg.iUri, map) 220 | } 221 | break 222 | default: 223 | break 224 | } 225 | } catch (e) { 226 | this.emit('error', e) 227 | } 228 | 229 | } 230 | 231 | _send_wup(action, callback, req) { 232 | try { 233 | let wup = new Taf.Wup() 234 | wup.setServant(action) 235 | wup.setFunc(callback) 236 | wup.writeStruct("tReq", req) 237 | let command = new HUYA.WebSocketCommand() 238 | command.iCmdType = HUYA.EWebSocketCommandType.EWSCmd_WupReq 239 | command.vData = wup.encode() 240 | let stream = new Taf.JceOutputStream() 241 | command.writeTo(stream) 242 | this._client.send(stream.getBuffer()) 243 | } catch (err) { 244 | this.emit('error', err) 245 | } 246 | } 247 | 248 | _stop() { 249 | this._starting = false 250 | this._emitter.removeAllListeners() 251 | clearInterval(this._heartbeat_timer) 252 | clearInterval(this._fresh_gift_list_timer) 253 | this._client && this._client.terminate() 254 | } 255 | 256 | stop() { 257 | this.removeAllListeners() 258 | this._stop() 259 | } 260 | } 261 | 262 | module.exports = huya_danmu -------------------------------------------------------------------------------- /ThirdLib/huya_barrage/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "huya-danmu", 3 | "version": "2.0.3", 4 | "description": "huya danmu module", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "node ./app.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/BacooTang/huya-danmu.git" 12 | }, 13 | "keywords": [ 14 | "huya", 15 | "danmu" 16 | ], 17 | "author": "BacooTang", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/BacooTang/huya-danmu/issues" 21 | }, 22 | "dependencies": { 23 | "axios": "^1.5.0", 24 | "huya-danmu": "^2.0.3", 25 | "md5": "^2.2.1", 26 | "request": "^2.83.0", 27 | "request-promise": "^4.2.2", 28 | "socks-proxy-agent": "^3.0.1", 29 | "to-arraybuffer": "^1.0.1", 30 | "ws": "^4.0.0" 31 | }, 32 | "homepage": "https://github.com/BacooTang/huya-danmu#readme" 33 | } 34 | -------------------------------------------------------------------------------- /ThirdLib/huya_barrage_bravo/app.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require('ws'); 2 | const http = require('http'); 3 | 4 | const httpServer = http.createServer((req, res) => { 5 | res.writeHead(200, { 'Content-Type': 'text/plain' }); 6 | res.end('WebSocket server is running'); 7 | }); 8 | 9 | const wss = new WebSocket.Server({ server: httpServer }); 10 | 11 | function onChat(data) { 12 | console.log('Received chat message:', data); 13 | broadcastMessage('chat', data); 14 | } 15 | 16 | function onGift(data) { 17 | console.log('Received gift message:', data); 18 | broadcastMessage('gift', data); 19 | } 20 | 21 | function onError(data) { 22 | console.error('Received error message:', data); 23 | broadcastMessage('error', data); 24 | } 25 | 26 | function broadcastMessage(type, data) { 27 | const message = JSON.stringify({ type, data }); 28 | wss.clients.forEach(client => { 29 | if (client.readyState === WebSocket.OPEN) { 30 | client.send(message); 31 | } 32 | }); 33 | } 34 | 35 | wss.on('connection', (ws) => { 36 | console.log('WebSocket client connected'); 37 | 38 | ws.on('message', (message) => { 39 | console.log(`Received message: ${message}`); 40 | }); 41 | 42 | ws.on('close', () => { 43 | console.log('WebSocket client disconnected'); 44 | }); 45 | }); 46 | 47 | // roomid from command line argument 48 | const roomid = process.argv[2]; 49 | 50 | if (!roomid) { 51 | console.error('Roomid is required'); 52 | process.exit(1); 53 | } 54 | 55 | // listen port from command line argument, default 9588 56 | const port = process.argv[3] || 9528; 57 | 58 | httpServer.listen(port, () => { 59 | // 字符串嵌套表达式 60 | console.log(`WebSocket server is listening on ${port}`); 61 | }); 62 | 63 | const HuyaDanmu = require('./src'); 64 | new HuyaDanmu({ 65 | roomid: roomid, 66 | onChat: (data) => onChat(JSON.stringify(data)), 67 | onGift: (data) => onGift(JSON.stringify(data)), 68 | onError: (data) => onError(JSON.stringify(data)) 69 | }).start(); 70 | -------------------------------------------------------------------------------- /ThirdLib/huya_barrage_bravo/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "huya_danmu", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "huya_danmu", 9 | "version": "1.0.0", 10 | "dependencies": { 11 | "axios": "^0.21.1", 12 | "ws": "^7.5.1" 13 | } 14 | }, 15 | "node_modules/axios": { 16 | "version": "0.21.1", 17 | "resolved": "https://registry.nlark.com/axios/download/axios-0.21.1.tgz", 18 | "integrity": "sha1-IlY0gZYvTWvemnbVFu8OXTwJsrg=", 19 | "license": "MIT", 20 | "dependencies": { 21 | "follow-redirects": "^1.10.0" 22 | } 23 | }, 24 | "node_modules/follow-redirects": { 25 | "version": "1.14.1", 26 | "resolved": "https://registry.nlark.com/follow-redirects/download/follow-redirects-1.14.1.tgz", 27 | "integrity": "sha1-2RFN7Qoc/dM04WTmZirQK/2R/0M=", 28 | "funding": [ 29 | { 30 | "type": "individual", 31 | "url": "https://github.com/sponsors/RubenVerborgh" 32 | } 33 | ], 34 | "license": "MIT", 35 | "engines": { 36 | "node": ">=4.0" 37 | }, 38 | "peerDependenciesMeta": { 39 | "debug": { 40 | "optional": true 41 | } 42 | } 43 | }, 44 | "node_modules/ws": { 45 | "version": "7.5.1", 46 | "resolved": "https://registry.nlark.com/ws/download/ws-7.5.1.tgz?cache=0&sync_timestamp=1624943994930&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fws%2Fdownload%2Fws-7.5.1.tgz", 47 | "integrity": "sha1-RPwADYftsdnFPlH7xpoKwfaHHWY=", 48 | "license": "MIT", 49 | "engines": { 50 | "node": ">=8.3.0" 51 | }, 52 | "peerDependencies": { 53 | "bufferutil": "^4.0.1", 54 | "utf-8-validate": "^5.0.2" 55 | }, 56 | "peerDependenciesMeta": { 57 | "bufferutil": { 58 | "optional": true 59 | }, 60 | "utf-8-validate": { 61 | "optional": true 62 | } 63 | } 64 | } 65 | }, 66 | "dependencies": { 67 | "axios": { 68 | "version": "0.21.1", 69 | "resolved": "https://registry.nlark.com/axios/download/axios-0.21.1.tgz", 70 | "integrity": "sha1-IlY0gZYvTWvemnbVFu8OXTwJsrg=", 71 | "requires": { 72 | "follow-redirects": "^1.10.0" 73 | } 74 | }, 75 | "follow-redirects": { 76 | "version": "1.14.1", 77 | "resolved": "https://registry.nlark.com/follow-redirects/download/follow-redirects-1.14.1.tgz", 78 | "integrity": "sha1-2RFN7Qoc/dM04WTmZirQK/2R/0M=" 79 | }, 80 | "ws": { 81 | "version": "7.5.1", 82 | "resolved": "https://registry.nlark.com/ws/download/ws-7.5.1.tgz?cache=0&sync_timestamp=1624943994930&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fws%2Fdownload%2Fws-7.5.1.tgz", 83 | "integrity": "sha1-RPwADYftsdnFPlH7xpoKwfaHHWY=", 84 | "requires": {} 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /ThirdLib/huya_barrage_bravo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "huya_danmu", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "node ./app.js" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "", 12 | "dependencies": { 13 | "axios": "^0.21.1", 14 | "ws": "^7.5.1" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ThirdLib/huya_barrage_bravo/src/index.js: -------------------------------------------------------------------------------- 1 | const events = require('events') 2 | const ws = require('ws') 3 | const axios = require('axios') 4 | const Taf = require('./lib/Taf') 5 | const HUYA = require('./lib/HUYA') 6 | const TafMx = require('./lib/TafMx') 7 | 8 | class HuyaDanmu extends events{ 9 | constructor(config){ 10 | super() 11 | this.config = { 12 | roomid: '', 13 | subsid: '', 14 | topsid: '', 15 | yyuid: '', 16 | sHuYaUA: 'webh5&2106011457&websocket', 17 | wsApi: 'wss://cdnws.api.huya.com', 18 | heartbeatTime: 60, 19 | timeout: 60, 20 | cookies: '', 21 | ...config 22 | } 23 | this._info = { 24 | presenterUid: '', 25 | lChannelId: '', 26 | lSubChannelId: '', 27 | yyuid: '', 28 | sGuid: '' 29 | } 30 | this.heartbeatTimer = null; 31 | this._client = null; 32 | this._gift_info = {} 33 | 34 | config.onChat && this.on('onChat', config.onChat) 35 | config.onGift && this.on('onGift', config.onGift) 36 | config.onError && this.on('onError', config.onError) 37 | } 38 | 39 | _get_user_id(){ 40 | var user = new HUYA.UserId(); 41 | user.sHuYaUA = this.config.sHuYaUA; 42 | user.lUid = this._info.presenterUid; 43 | user.sCookie = this.config.cookies; 44 | user.sGuid = this._info.sGuid; 45 | user.sToken = ''; 46 | return user; 47 | } 48 | 49 | async _get_room_info(){ 50 | let res = await axios.get(`https://m.huya.com/${this.config.roomid}`, { 51 | headers: { 52 | 'user-agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1 Edg/91.0.4472.124' 53 | }, 54 | timeout: this.config.timeout * 1000 55 | }) 56 | if(res.status === 200){ 57 | let info = {}; 58 | let presenterUid_array = res.data.match(/"lUid":(.*),"iIsProfile"/); 59 | let yyuid_array = res.data.match(/"lYyid":(.*?),"sNick"/); 60 | let topsid_array = res.data.match(/"lChannelId":(.*?),"lSubChannelId"/); 61 | let subsid_array = res.data.match(/"lSubChannelId":(.*?),"lPresenterUid"/); 62 | 63 | if (!presenterUid_array || !yyuid_array) { 64 | return; 65 | } 66 | if(!topsid_array){ 67 | this.emit('onError', { 68 | type: 0, 69 | error: `房间${ this.config.roomid }不存在或已停播` 70 | }) 71 | return 72 | } 73 | 74 | info.presenterUid = presenterUid_array[1] === '' ? 0 : parseInt(presenterUid_array[1]); 75 | info.lChannelId = topsid_array[1] === '' ? 0 : parseInt(topsid_array[1]); 76 | info.lSubChannelId = subsid_array[1] === '' ? 0 : parseInt(subsid_array[1]); 77 | info.yyuid = parseInt(yyuid_array[1]); 78 | info.sGuid = ""; 79 | return info 80 | }else{ 81 | this.emit('onError', { 82 | type: 0, 83 | error: `获取房间${ this.config.roomid }信息失败` 84 | }) 85 | return 86 | } 87 | } 88 | 89 | /** 获取礼物清单 */ 90 | _get_gift(){ 91 | let prop_req = new HUYA.GetPropsListReq() 92 | prop_req.tUserId = this._get_user_id() 93 | prop_req.iTemplateType = HUYA.EClientTemplateType.TPL_MIRROR 94 | this._send_wup("PropsUIServer", "getPropsList", prop_req) 95 | } 96 | 97 | /** 注册弹幕 */ 98 | _get_chat(){ 99 | let req = new HUYA.WSRegisterGroupReq(); 100 | req.vGroupId.value.push('live:' + this._info.presenterUid); 101 | req.vGroupId.value.push('chat:' + this._info.presenterUid); 102 | let stream = new Taf.JceOutputStream(); 103 | req.writeTo(stream); 104 | let webCommand = new HUYA.WebSocketCommand(); 105 | webCommand.iCmdType = HUYA.EWebSocketCommandType.EWSCmdC2S_RegisterGroupReq; 106 | webCommand.vData = stream.getBinBuffer(); 107 | stream = new Taf.JceOutputStream(); 108 | webCommand.writeTo(stream); 109 | this._sendMsg(stream.getBuffer()); 110 | } 111 | 112 | /** 心跳 */ 113 | _heartbeat(){ 114 | let heart_beat_req = new HUYA.UserHeartBeatReq() 115 | heart_beat_req.tId = this._get_user_id(); 116 | heart_beat_req.lTid = this._info.lChannelId 117 | heart_beat_req.lSid = this._info.lSubChannelId 118 | heart_beat_req.lPid = this._info.yyuid 119 | heart_beat_req.eLineType = 1; 120 | heart_beat_req.lShortTid = 0; 121 | heart_beat_req.bWatchVideo = true; 122 | heart_beat_req.eLineType = HUYA.EStreamLineType.STREAM_LINE_AL; 123 | heart_beat_req.iFps = 0; 124 | heart_beat_req.iAttendee = 0; 125 | heart_beat_req.iLastHeartElapseTime = 0; 126 | this._send_wup("onlineui", "OnUserHeartBeat", heart_beat_req) 127 | } 128 | 129 | _send_wup(servant, func, req){ 130 | try { 131 | var wup = new Taf.Wup(); 132 | wup.setServant(servant); 133 | wup.setFunc(func); 134 | wup.writeStruct('tReq', req); 135 | var webCommand = new HUYA.WebSocketCommand; 136 | webCommand.iCmdType = HUYA.EWebSocketCommandType.EWSCmd_WupReq; 137 | webCommand.vData = wup.encode(); 138 | var jceStream = new Taf.JceOutputStream; 139 | webCommand.writeTo(jceStream); 140 | this._sendMsg(jceStream.getBuffer()); 141 | }catch (err) { 142 | this.emit('onError', { 143 | type: 2, 144 | error: err 145 | }) 146 | } 147 | } 148 | 149 | _sendMsg(message){ 150 | this._client.send(message); 151 | } 152 | 153 | _on_message(message){ 154 | try{ 155 | var buffer = new Uint8Array(message).buffer; 156 | var from = new Taf.JceInputStream(buffer); 157 | var webSocketCOmmand = new HUYA.WebSocketCommand(); 158 | webSocketCOmmand.readFrom(from); 159 | switch (webSocketCOmmand.iCmdType) { 160 | case HUYA.EWebSocketCommandType.EWSCmd_WupRsp: // 回调 161 | try{ 162 | let wup = new Taf.Wup() 163 | wup.decode(webSocketCOmmand.vData.buffer) 164 | let map = new (TafMx.WupMapping[wup.sFuncName])() 165 | wup.readStruct('tRsp', map, TafMx.WupMapping[wup.sFuncName]) 166 | this.emit(wup.sFuncName, map) 167 | }catch (e) { 168 | console.log(e) 169 | } 170 | break; 171 | case HUYA.EWebSocketCommandType.EWSCmdS2C_MsgPushReq: // 系统下发 172 | from = new Taf.JceInputStream(webSocketCOmmand.vData.buffer); 173 | var pushMessage = new HUYA.WSPushMessage(); 174 | pushMessage.readFrom(from); 175 | var mcs = pushMessage.iUri; 176 | from = new Taf.JceInputStream(pushMessage.sMsg.buffer); 177 | var uriMapping = TafMx.UriMapping[pushMessage.iUri]; 178 | if (uriMapping) { 179 | var msg = new uriMapping(); 180 | msg.readFrom(from); 181 | // console.log(msg); 182 | if (mcs == 1400) { // 弹幕 183 | this.emit('onChat', { 184 | room_id: this.config.roomid, 185 | timestamp: new Date().getTime()+"", 186 | uid: msg.tUserInfo.lUid+"", 187 | nickname: msg.tUserInfo.sNickName, 188 | msg_content: msg.sContent, 189 | face: msg.tUserInfo.sAvatarUrl, 190 | }) 191 | } 192 | if (mcs == 6501 || mcs == 6502 || mcs == 6507) { 193 | let gift = this._gift_info[msg.iItemType + ''] || { price: 0 }; 194 | this.emit('onGift', { 195 | room_id: this.config.roomid, 196 | timestamp: new Date().getTime()+"", 197 | uid: msg.lSenderUid+"", 198 | nickname: msg.sSenderNick, 199 | type: mcs, 200 | gfid: msg.iItemType, 201 | gfcnt: msg.iItemCount, 202 | gift_name: gift.name, 203 | gift_icon: gift.icon, 204 | price_big: gift.price, 205 | price_total: msg.iItemCount * gift.price 206 | }); 207 | } 208 | } 209 | break; 210 | default: 211 | break 212 | } 213 | 214 | }catch(e){ 215 | this.emit('onError', { 216 | type: 1, 217 | error: e 218 | }) 219 | } 220 | } 221 | 222 | _start_ws(){ 223 | this._client = new ws(this.config.wsApi, { 224 | perMessageDeflate: false, 225 | handshakeTimeout: this.config.timeout * 1000 226 | }) 227 | 228 | this._client.on('open', () => { 229 | this._get_gift(); 230 | this._get_chat() 231 | this._heartbeat() 232 | this.heartbeatTimer = setInterval(this._heartbeat.bind(this), this.config.heartbeatTime * 1000) 233 | }) 234 | 235 | this._client.on('message', this._on_message.bind(this)) 236 | this._client.on('close', () => { 237 | this.emit('onError', { 238 | type: 1, 239 | error: this.config.roomid + "websocket断开" 240 | }) 241 | }) 242 | 243 | this.on("getPropsList", msg => { 244 | msg.vPropsItemList.value.forEach(item => { 245 | let name = item.sPropsName, icon = ''; 246 | try{ 247 | name = item.vPropView.value[0].name; 248 | icon = item.vPropsIdentity.value[0].sPropsWeb.split("&")[0]; 249 | this._gift_info[item.iPropsId + ''] = { 250 | name: item.vPropView.value[0].name, 251 | price: item.iPropsYb / 100, 252 | icon 253 | } 254 | }catch (e) { 255 | } 256 | }) 257 | }) 258 | } 259 | 260 | async start(){ 261 | this._info = await this._get_room_info() 262 | this._start_ws() 263 | } 264 | } 265 | 266 | module.exports = HuyaDanmu -------------------------------------------------------------------------------- /ThirdLib/huya_barrage_bravo/src/lib/TafMx.js: -------------------------------------------------------------------------------- 1 | const HUYA = require('./HUYA'); 2 | const _GUESS = require('./GUESS'); 3 | let GUESS = _GUESS.GUESS; 4 | let QAGuessWatchLive = _GUESS.QAGuessWatchLive; 5 | let MatchGuess = _GUESS.MatchGuess; 6 | let ActWatchTor = _GUESS.ActWatchTor; 7 | let ActTotalFinalPeak = _GUESS.ActTotalFinalPeak; 8 | let ActLiveCall = _GUESS.ActLiveCall; 9 | var TafMx = {}; 10 | TafMx['UriMapping'] = TafMx['UriMapping'] || {}; 11 | TafMx['WupMapping'] = TafMx['WupMapping'] || {}; 12 | TafMx['UriMapping'] = { 13 | 1400: HUYA.MessageNotice, 14 | 6110: HUYA.VipEnterBanner, 15 | 6210: HUYA.VipBarListRsp, 16 | 6501: HUYA.SendItemSubBroadcastPacket, 17 | 6502: HUYA.SendItemNoticeWordBroadcastPacket, 18 | 6507: HUYA.SendItemNoticeGameBroadcastPacket, 19 | 8006: HUYA.AttendeeCountNotice 20 | }; 21 | TafMx['WupMapping'] = { 22 | 'getPropsList': HUYA.GetPropsListRsp, 23 | 'OnUserHeartBeat': HUYA.UserHeartBeatRsp, 24 | 'OnUserEvent': HUYA.UserEventRsp, 25 | 'doLaunch': HUYA.LiveLaunchRsp, 26 | 'getLivingInfo': HUYA.GetLivingInfoRsp, 27 | 'getWebdbUserInfo': HUYA.GetWebdbUserInfoRsp, 28 | 'batchGetCdnTokenInfo': HUYA.BatchGetCdnTokenRsp, 29 | 'getCurWeekStarPropsIds': HUYA.WeekStarPropsIds, 30 | 'sendCardPackageItem': HUYA.SendCardPackageItemRsp, 31 | 'getVerificationStatus': HUYA.GetVerificationStatusResp, 32 | 'getFirstRechargePkgStatus': HUYA.GetFirstRechargePkgStatusResp, 33 | 'getPresenterDetail': HUYA.GetPresenterDetailRsp, 34 | 'getCdnTokenInfoEx': HUYA.GetCdnTokenExRsp, 35 | 'getSequence': HUYA.GetSequenceRsp, 36 | 'getBatchPropsItem': HUYA.GetBatchPropsItemRsp, 37 | 'getVipBarList': HUYA.VipBarListRsp, 38 | 'getWeekRankList': HUYA.WeekRankListRsp, 39 | 'muteRoomUser': HUYA.MuteRoomUserRsp, 40 | 'sendMessage': HUYA.SendMessageRsp, 41 | 'GetNobleInfo': HUYA.NobleInfoRsp, 42 | 'queryBadgeInfoList': HUYA.BadgeInfoListRsp, 43 | 'queryBadgeInfo': HUYA.BadgeInfo, 44 | 'useBadge': HUYA.BadgeInfo, 45 | 'getVipCard': HUYA.VipCardRsp, 46 | 'getScreenSkin': HUYA.getScreenSkinRsp, 47 | 'getRoomAuditConf': HUYA.GetRoomAuditConfRsp, 48 | 'getUserLevelInfo': HUYA.GetUserLevelInfoRsp, 49 | 'getViewerList': HUYA.ViewerListRsp, 50 | 'getFansSupportList': HUYA.FansSupportListRsp, 51 | 'sendReplayMessage': HUYA.SendReplayMessageRsp, 52 | 'getPresenterActivity': HUYA.PresenterActivityRsp, 53 | 'getRMessageList': HUYA.GetRMessageListRsp, 54 | 'getRMessageListWb': HUYA.GetRMessageListRsp, 55 | 'getDirectorProgramList': HUYA.GetDirectorProgramListRsp, 56 | 'reportMessage': HUYA.ReportMessageRsp, 57 | 'getUserBoxInfo': HUYA.GetUserBoxInfoRsp, 58 | 'finishTaskNotice': HUYA.FinishTaskNoticeRsp, 59 | 'awardBoxPrize': HUYA.AwardBoxPrizeRsp, 60 | 'getTreasureBoxInfo': HUYA.GetTreasureBoxInfoRsp, 61 | 'bet': HUYA.BetRsp, 62 | 'buyBet': HUYA.BuyBetRsp, 63 | 'getGameInfo': HUYA.GetGameInfoListRsp, 64 | 'getRemainBeanNum': HUYA.GetRemainBeanNumRsp, 65 | 'getAssistant': HUYA.GetAssistantRsp, 66 | 'queryCardPackage': HUYA.QueryCardPackageRsp, 67 | 'queryTreasure': HUYA.QueryTreasureInfoRsp, 68 | 'sendTreasureLotteryDraw': HUYA.TreasureLotteryDrawRsp, 69 | 'getLinkMicPresenterListByUid': HUYA.GetLinkMicPresenterInfoRsp, 70 | 'subscribe': HUYA.SubscribeResp, 71 | 'unsubscribe': HUYA.UnsubscribeResp, 72 | 'getSubscribeStatus': HUYA.SubscribeStatusResp, 73 | 'getRelation': HUYA.GetRelationRsp, 74 | 'addSubscribe': HUYA.ModRelationRsp, 75 | 'delSubscribe': HUYA.ModRelationRsp, 76 | 'getRelationBatch': HUYA.GetRelationBatchRsp, 77 | 'isPugcRoom': HUYA.IsPugcRoomRsp, 78 | 'getPugcVipList': HUYA.GetPugcVipListRsp, 79 | 'getGameLiveHisUpon': HUYA.GetGameLiveHisUponRsp, 80 | 'getVideoHisUpon': HUYA.GetGameLiveHisUponRsp, 81 | 'getBadgeName': HUYA.BadgeNameRsp, 82 | 'getBadgeNameV2': HUYA.BadgeNameRsp, 83 | 'getLiveAdInfo': HUYA.GetLiveAdInfoRsp, 84 | 'getAuditorRole': HUYA.AuditorEnterLiveNotice, 85 | 'getBadgeConfigInfo': HUYA.BadgeConfigInfoRsp, 86 | 'setBadgeV': HUYA.SetBadgeVRsp, 87 | 'setUserProfile': HUYA.SetUserProfileRsp, 88 | 'getMaiXuBySid': HUYA.MaiXuSearchRsp, 89 | 'getOnTVPanel': HUYA.OnTVPanel, 90 | 'getOnTVUserInfo': HUYA.OnTVUserInfoRsp, 91 | 'sendOnTVBarrage': HUYA.SendOnTVBarrageRsp, 92 | 'getBadgeItem': HUYA.BadgeItemRsp, 93 | 'getCurrentGameAd': HUYA.GameAdvertisement, 94 | 'getSupportCampInfo': HUYA.SupportCampInfoRsp, 95 | 'getUserSupportCamp': HUYA.UserSupportCampRsp, 96 | 'getUserSetting': HUYA.SettingFetchRsp, 97 | 'getLotteryPanel': HUYA.LotteryPanel, 98 | 'getLotteryUserInfo': HUYA.LotteryUserInfoRsp, 99 | 'buyTicket': HUYA.BuyTicketRsp, 100 | 'getPresenterLevelBase': HUYA.PresenterLevelBaseRsp, 101 | 'getPresenterLevelProgress': HUYA.PresenterLevelProgressRsp, 102 | 'getPresenterLiveScheduleInfo': HUYA.GetPresenterLiveScheduleInfoRsp, 103 | 'getActivityMsg': HUYA.ActivityMsgRsp, 104 | 'GetCurCheckRoomStatus': HUYA.CheckRoomStatus, 105 | 'CKRoomUserEnter': HUYA.CheckRoomRsp, 106 | 'getFansPrivilege': HUYA.FansPrivilegeRsp, 107 | 'queryBadgeInfoV2': HUYA.BadgeInfo, 108 | 'useBadgeV2': HUYA.BadgeInfo, 109 | 'getDragonInfo': GUESS.GetDragonRsp, 110 | 'getActivityTorMsg': HUYA.ActivityTorMsgRsp, 111 | 'getTorMemberTaskResult': ActWatchTor.MemberTaskResultRsp, 112 | 'startTorPublishTask': ActWatchTor.PublishTaskRsp, 113 | 'getTorPublishPanel': ActWatchTor.PublishPanelRsp, 114 | 'queryTorWeekHistory': ActWatchTor.PublishHistoryRsp, 115 | 'GetBattleTeamInfo': ActLiveCall.GetBattleTeamInfoRsp, 116 | 'getMeetingStatByPresenterUid': HUYA.GetMeetingStatByUidRsp, 117 | 'getUserPanel': QAGuessWatchLive.QAGuessGetUserPanelRsp, 118 | 'getRctTimedMessage': HUYA.GetRctTimedMessageRsp, 119 | 120 | getInfoFromVG: HUYA.GetInfoFromVGRsp, 121 | GetInfoFromVG: HUYA.GetInfoFromVGRsp, 122 | getInfoFromCdnVG: HUYA.GetInfoFromVGRsp, 123 | getCdnTokenInfoEx: HUYA.GetCdnTokenExRsp, 124 | getLivingStreamInfo: HUYA.GetLivingStreamInfoRsp, 125 | getP2PStreamInfo: HUYA.GetP2PStreamInfoRsp, 126 | getP2PStreamTokenInfoEx: HUYA.GetP2PStreamTokenExRsp, 127 | queryHttpDns: HUYA.QueryHttpDnsRsp, 128 | loginVerify: HUYA.LoginVerifyRsp, 129 | joinGroup: HUYA.JoinMediaGroupRsp, 130 | quitGroup: HUYA.QuitMediaGroupRsp, 131 | wsTimeSync: HUYA.WSTimeSyncRsp, 132 | onClientGetStunAndPcdnProxyReq: HUYA.ClientGetStunAndPcdnProxyRsp, 133 | clientQueryPcdnSchedule: HUYA.ClientQueryPcdnScheduleRsp, 134 | onClientGetPcdnFlvOutsideSdkFullstreamInfo: HUYA.ClientGetPcdnFlvOutsideSdkFullstreamInfoRsp, 135 | getMediaRec: HUYA.RecSysRsp 136 | }; 137 | module['exports'] = TafMx; -------------------------------------------------------------------------------- /ThirdLib/huya_barrage_bravo/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "axios@^0.21.1": 6 | "integrity" "sha1-IlY0gZYvTWvemnbVFu8OXTwJsrg=" 7 | "resolved" "https://registry.nlark.com/axios/download/axios-0.21.1.tgz" 8 | "version" "0.21.1" 9 | dependencies: 10 | "follow-redirects" "^1.10.0" 11 | 12 | "follow-redirects@^1.10.0": 13 | "integrity" "sha1-2RFN7Qoc/dM04WTmZirQK/2R/0M=" 14 | "resolved" "https://registry.nlark.com/follow-redirects/download/follow-redirects-1.14.1.tgz" 15 | "version" "1.14.1" 16 | 17 | "ws@^7.5.1": 18 | "integrity" "sha1-RPwADYftsdnFPlH7xpoKwfaHHWY=" 19 | "resolved" "https://registry.nlark.com/ws/download/ws-7.5.1.tgz?cache=0&sync_timestamp=1624943994930&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fws%2Fdownload%2Fws-7.5.1.tgz" 20 | "version" "7.5.1" 21 | -------------------------------------------------------------------------------- /ThirdLib/tiktok_barrage_nodejs/WSLINK.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwaggyMacro/BarrageGPT/4624fdaaf5358e676f411d545c0864805d12a628/ThirdLib/tiktok_barrage_nodejs/WSLINK.exe -------------------------------------------------------------------------------- /ThirdLib/tiktok_barrage_nodejs/ak.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |已读取的数字:
51 |已读取的数字:
55 |