├── 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 | 上传txt文件并读取第32行内容 6 | 7 | 46 | 47 | 48 | 49 | 50 |

已读取的数字:

51 | 52 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /ThirdLib/tiktok_barrage_nodejs/app.js: -------------------------------------------------------------------------------- 1 | import { WebSocketServer } from "ws"; 2 | const wss = new WebSocketServer({ 3 | port: 9527, 4 | }); 5 | 6 | wss.on("connection", function connection(ws) { 7 | console.log("客户端连接成功"); 8 | ws.on("message", function message(data) { 9 | let message = JSON.parse(data.toString()) 10 | switch (message.action) { 11 | case 'message': 12 | console.log(getTime(), message.message.user_nickName + ':' + message.message.msg_content) 13 | break 14 | case 'join': 15 | console.log(getTime(), message.message.user_nickName + ':' + message.message.msg_content) 16 | break 17 | } 18 | wss.clients.forEach(cen => { 19 | cen.send(JSON.stringify(message)) 20 | }) 21 | }); 22 | }); 23 | 24 | function getTime() { 25 | return `[${new Date().toLocaleTimeString()}]` 26 | } 27 | 28 | console.log('打开-> http://127.0.0.1:9527') -------------------------------------------------------------------------------- /ThirdLib/tiktok_barrage_nodejs/client.js: -------------------------------------------------------------------------------- 1 | window.onDouyinServer = function() { 2 | new Barrage() 3 | } 4 | console.clear() 5 | console.log(`[${new Date().toLocaleTimeString()}]`, '正在载入JS,请稍后..') 6 | console.log(`[${new Date().toLocaleTimeString()}]`, '如需删除直播画面,请在控制台输入: removeVideoLayer()') 7 | var scriptElement = document.createElement('script') 8 | scriptElement.src = 'https://swaggymacro.github.io/tiktok_barrage_nodejs/index.js?t=' + Math.random() 9 | document.body.appendChild(scriptElement) 10 | -------------------------------------------------------------------------------- /ThirdLib/tiktok_barrage_nodejs/index.js: -------------------------------------------------------------------------------- 1 | // 测试 2 | // window.onDouyinServer = function() { 3 | // new Barrage({ message: false }) 4 | // } 5 | 6 | const Barrage = class { 7 | wsurl = "ws://127.0.0.1:9527" 8 | timer = null 9 | timeinterval = 10 * 1000 // 断线重连轮询间隔 10 | propsId = null 11 | chatDom = null 12 | roomJoinDom = null 13 | ws = null 14 | observer = null 15 | chatObserverrom = null 16 | option = {} 17 | event = {} 18 | eventRegirst = {} 19 | constructor(option = { message: true }) { 20 | this.option = option 21 | let { link, removePlay } = option 22 | if (link) { 23 | this.wsurl = link 24 | } 25 | if (removePlay) { 26 | document.querySelector('.basicPlayer').remove() 27 | } 28 | this.propsId = Object.keys(document.querySelector('.webcast-chatroom___list'))[1] 29 | this.chatDom = document.querySelector('.webcast-chatroom___items').children[0] 30 | this.roomJoinDom = document.querySelector('.webcast-chatroom___bottom-message') 31 | this.ws = new WebSocket(this.wsurl) 32 | this.ws.onclose = this.wsClose 33 | this.ws.onopen = () => { 34 | this.openWs() 35 | } 36 | } 37 | 38 | // 消息事件 , join, message 39 | on(e, cb) { 40 | this.eventRegirst[e] = true 41 | this.event[e] = cb 42 | } 43 | openWs() { 44 | console.log(`[${new Date().toLocaleTimeString()}]`, '服务已经连接成功!') 45 | clearInterval(this.timer) 46 | this.runServer() 47 | } 48 | wsClose() { 49 | console.log('服务器断开') 50 | if (this.timer !== null) { 51 | return 52 | } 53 | this.observer && this.observer.disconnect(); 54 | this.chatObserverrom && this.chatObserverrom.disconnect(); 55 | this.timer = setInterval(() => { 56 | console.log('正在等待服务器启动..') 57 | this.ws = new WebSocket(wsurl); 58 | console.log('状态 ->', this.ws.readyState) 59 | setTimeout(() => { 60 | if (this.ws.readyState === 1) { 61 | openWs() 62 | } 63 | }, 2000) 64 | 65 | }, this.timeinterval) 66 | } 67 | runServer() { 68 | let _this = this 69 | if (this.option.join) { 70 | this.observer = new MutationObserver((mutationsList) => { 71 | for (let mutation of mutationsList) { 72 | if (mutation.type === 'childList' && mutation.addedNodes.length) { 73 | let dom = mutation.addedNodes[0] 74 | let user = dom[this.propsId].children.props.message.payload.user 75 | let msg = { 76 | ...this.getUser(user), 77 | ... { msg_content: `${user.nickname} 来了` } 78 | } 79 | if (this.eventRegirst.join) { 80 | this.event['join'](msg) 81 | } 82 | this.ws.send(JSON.stringify({ action: 'join', message: msg })); 83 | } 84 | } 85 | }); 86 | this.observer.observe(this.roomJoinDom, { childList: true }); 87 | 88 | } 89 | 90 | this.chatObserverrom = new MutationObserver((mutationsList, observer) => { 91 | for (let mutation of mutationsList) { 92 | if (mutation.type === 'childList' && mutation.addedNodes.length) { 93 | let b = mutation.addedNodes[0] 94 | if (b[this.propsId].children.props.message) { 95 | let message = this.messageParse(b) 96 | if (message) { 97 | if (this.eventRegirst.message) { 98 | this.event['join'](message) 99 | } 100 | if (_this.option.message === false && !message.isGift) { 101 | return 102 | } 103 | this.ws.send(JSON.stringify({ action: 'message', message: message })); 104 | } 105 | } 106 | } 107 | } 108 | }); 109 | this.chatObserverrom.observe(this.chatDom, { childList: true }); 110 | } 111 | getUser(user) { 112 | if (!user) { 113 | return 114 | } 115 | let msg = { 116 | user_level: this.getLevel(user.badgeImageList, 1), 117 | user_fansLevel: this.getLevel(user.badgeImageList, 7), 118 | user_id: user.id, 119 | user_nickName: user.nickname, 120 | user_avatar: user.avatar_thumb.url_list[0], 121 | user_gender: user.gender === 1 ? '男' : '女', 122 | user_fansLightName: "", 123 | user_levelImage: "" 124 | } 125 | return msg 126 | } 127 | getLevel(arr, type) { 128 | if (!arr || arr.length === 0) { 129 | return 0 130 | } 131 | let item = arr.find(i => { 132 | return i.imageType === type 133 | }) 134 | if (item) { 135 | return parseInt(item.content.level) 136 | } else { 137 | return 0 138 | } 139 | } 140 | messageParse(dom) { 141 | if (!dom[this.propsId].children.props.message) { 142 | return null 143 | } 144 | let msg = dom[this.propsId].children.props.message.payload 145 | let result = { 146 | repeatCount: null, 147 | gift_id: null, 148 | gift_name: null, 149 | gift_number: null, 150 | gift_image: null, 151 | gift_diamondCount: null, 152 | gift_describe: null, 153 | } 154 | 155 | result = Object.assign(result, this.getUser(msg.user)) 156 | switch (msg.common.method) { 157 | case 'WebcastGiftMessage': 158 | console.log(msg) 159 | result = Object.assign(result, { 160 | // repeatCount: parseInt(), 161 | msg_content: msg.common.describe, 162 | isGift: true, 163 | gift_id: msg.gift.id, 164 | gift_name: msg.gift.name, 165 | // gift_number: parseInt(msg.comboCount), 166 | gift_number: parseInt(msg.repeatCount), 167 | gift_image: msg.gift.icon.urlListList[0], 168 | gift_diamondCount: msg.gift.diamondCount, 169 | gift_describe: msg.gift.describe, 170 | }) 171 | break 172 | case 'WebcastChatMessage': 173 | result = Object.assign(result, { 174 | isGift: false, 175 | msg_content: msg.content 176 | }) 177 | break 178 | default: 179 | result = Object.assign(result, { 180 | isGift: false, 181 | msg_content: msg.content 182 | }) 183 | break 184 | } 185 | return result 186 | } 187 | } 188 | 189 | if (window.onDouyinServer) { 190 | window.onDouyinServer() 191 | } 192 | 193 | window.removeVideoLayer = function() { 194 | document.querySelector('.basicPlayer').remove() 195 | console.log('删除画面成功,不影响弹幕信息接收') 196 | } 197 | -------------------------------------------------------------------------------- /ThirdLib/tiktok_barrage_nodejs/made/d02.js: -------------------------------------------------------------------------------- 1 | window.onDouyinServer = function() { 2 | new Barrage({ join: true }) 3 | } 4 | console.log(`[${new Date().toLocaleTimeString()}]`, '正在载入JS,请稍后..') 5 | var scriptElement = document.createElement('script') 6 | scriptElement.src = 'https://jiansenc.github.io/tiktok_barrage_nodejs/index.js?t=' + Math.random() 7 | document.body.appendChild(scriptElement) -------------------------------------------------------------------------------- /ThirdLib/tiktok_barrage_nodejs/made/d03.js: -------------------------------------------------------------------------------- 1 | var bg = null 2 | window.onDouyinServer = function() { 3 | bg = new Barrage() 4 | } 5 | 6 | var videoTimes = null 7 | 8 | var go = () => { 9 | if (document.querySelector('video') === null) { 10 | bg = null 11 | setInterval(() => { 12 | if (document.querySelector('video') !== null) { 13 | clearInterval(videoTimes) 14 | go() 15 | } 16 | }, 60 * 1000); 17 | } else { 18 | console.log(`[${new Date().toLocaleTimeString()}]`, '正在载入JS,请稍后..') 19 | var scriptElement = document.createElement('script') 20 | scriptElement.src = 'https://jiansenc.github.io/tiktok_barrage_nodejs/index.js?t=' + Math.random() 21 | document.body.appendChild(scriptElement) 22 | } 23 | 24 | } 25 | 26 | go() -------------------------------------------------------------------------------- /ThirdLib/tiktok_barrage_nodejs/made/dgiftcount.js: -------------------------------------------------------------------------------- 1 | window.onDouyinServer = function() { 2 | new Barrage({ message: false }) 3 | } 4 | console.clear() 5 | console.log(`[${new Date().toLocaleTimeString()}]`, '正在载入JS,请稍后..') 6 | console.log(`[${new Date().toLocaleTimeString()}]`, '如需删除直播画面,请在控制台输入: removeVideoLayer()') 7 | var scriptElement = document.createElement('script') 8 | scriptElement.src = 'https://jiansenc.github.io/tiktok_barrage_nodejs/index.js?t=' + Math.random() 9 | document.body.appendChild(scriptElement) -------------------------------------------------------------------------------- /ThirdLib/tiktok_barrage_nodejs/made/gitfcount/EasyNet.ec: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwaggyMacro/BarrageGPT/4624fdaaf5358e676f411d545c0864805d12a628/ThirdLib/tiktok_barrage_nodejs/made/gitfcount/EasyNet.ec -------------------------------------------------------------------------------- /ThirdLib/tiktok_barrage_nodejs/made/gitfcount/HPSocket4C.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwaggyMacro/BarrageGPT/4624fdaaf5358e676f411d545c0864805d12a628/ThirdLib/tiktok_barrage_nodejs/made/gitfcount/HPSocket4C.dll -------------------------------------------------------------------------------- /ThirdLib/tiktok_barrage_nodejs/made/gitfcount/HP_Socket-20220501-无DLL.ec: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwaggyMacro/BarrageGPT/4624fdaaf5358e676f411d545c0864805d12a628/ThirdLib/tiktok_barrage_nodejs/made/gitfcount/HP_Socket-20220501-无DLL.ec -------------------------------------------------------------------------------- /ThirdLib/tiktok_barrage_nodejs/made/gitfcount/Ws_WebServer[公众版].ec: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwaggyMacro/BarrageGPT/4624fdaaf5358e676f411d545c0864805d12a628/ThirdLib/tiktok_barrage_nodejs/made/gitfcount/Ws_WebServer[公众版].ec -------------------------------------------------------------------------------- /ThirdLib/tiktok_barrage_nodejs/made/gitfcount/config.ini: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwaggyMacro/BarrageGPT/4624fdaaf5358e676f411d545c0864805d12a628/ThirdLib/tiktok_barrage_nodejs/made/gitfcount/config.ini -------------------------------------------------------------------------------- /ThirdLib/tiktok_barrage_nodejs/made/gitfcount/tft_dianzicheng.bak: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwaggyMacro/BarrageGPT/4624fdaaf5358e676f411d545c0864805d12a628/ThirdLib/tiktok_barrage_nodejs/made/gitfcount/tft_dianzicheng.bak -------------------------------------------------------------------------------- /ThirdLib/tiktok_barrage_nodejs/made/gitfcount/tft_dianzicheng.e: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwaggyMacro/BarrageGPT/4624fdaaf5358e676f411d545c0864805d12a628/ThirdLib/tiktok_barrage_nodejs/made/gitfcount/tft_dianzicheng.e -------------------------------------------------------------------------------- /ThirdLib/tiktok_barrage_nodejs/made/gitfcount/zyJson1.3.ec: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwaggyMacro/BarrageGPT/4624fdaaf5358e676f411d545c0864805d12a628/ThirdLib/tiktok_barrage_nodejs/made/gitfcount/zyJson1.3.ec -------------------------------------------------------------------------------- /ThirdLib/tiktok_barrage_nodejs/made/gitfcount/模拟消息发送.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | web socket test 8 | 25 | 26 | 27 | 28 | 29 | 30 |
31 | 昵称 32 | 33 | 34 | ID 35 | 36 | 37 | 礼物ID(可空) 38 | 39 |
40 | 内容 41 | 42 |
43 |
44 | 65 | 66 | 67 | 68 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /ThirdLib/tiktok_barrage_nodejs/made/gitfcount/精易模块v7.35.ec: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SwaggyMacro/BarrageGPT/4624fdaaf5358e676f411d545c0864805d12a628/ThirdLib/tiktok_barrage_nodejs/made/gitfcount/精易模块v7.35.ec -------------------------------------------------------------------------------- /ThirdLib/tiktok_barrage_nodejs/made/room.css: -------------------------------------------------------------------------------- 1 | ._chatDiv { 2 | position: fixed; 3 | top: 0; 4 | bottom: 0; 5 | left: 0; 6 | right: 0; 7 | background-color: #fff; 8 | z-index: 999; 9 | } 10 | 11 | #_chatDiv_list { 12 | margin: 0; 13 | } 14 | 15 | #_chatDiv_list li { 16 | padding: 10px; 17 | } -------------------------------------------------------------------------------- /ThirdLib/tiktok_barrage_nodejs/made/room.js: -------------------------------------------------------------------------------- 1 | let _box = null 2 | var dcount = 0 3 | const chatWindow = document.querySelector('#_chatDiv_list'); 4 | 5 | const Barrage = class { 6 | propsId = null 7 | chatDom = null 8 | roomJoinDom = null 9 | ws = null 10 | observer = null 11 | chatObserverrom = null 12 | option = {} 13 | constructor() { 14 | 15 | 16 | 17 | _box = document.getElementById('_chatDiv_list') 18 | this.propsId = Object.keys(document.querySelector('.webcast-chatroom___list'))[1] 19 | this.chatDom = document.querySelector('.webcast-chatroom___items').children[0] 20 | this.roomJoinDom = document.querySelector('.webcast-chatroom___bottom-message') 21 | } 22 | runServer() { 23 | let _this = this 24 | this.chatObserverrom = new MutationObserver((mutationsList, observer) => { 25 | for (let mutation of mutationsList) { 26 | if (mutation.type === 'childList' && mutation.addedNodes.length) { 27 | let b = mutation.addedNodes[0] 28 | if (b[this.propsId].children.props.message) { 29 | let message = this.messageParse(b) 30 | if (message && !message.isGift) { 31 | insertDom(message) 32 | } 33 | } 34 | } 35 | } 36 | }); 37 | this.chatObserverrom.observe(this.chatDom, { childList: true }); 38 | console.log('安装完成 √') 39 | setTimeout(() => { 40 | window.electronAPI.closeDevTools() 41 | }, 3000) 42 | } 43 | getUser(user) { 44 | if (!user) { 45 | return 46 | } 47 | let msg = { 48 | user_level: this.getLevel(user.badgeImageList, 1), 49 | user_fansLevel: this.getLevel(user.badgeImageList, 7), 50 | user_id: user.id, 51 | user_nickName: user.nickname, 52 | user_avatar: user.avatarThumb.urlList[0], 53 | user_gender: user.gender === 1 ? '男' : '女', 54 | user_isAdmin: user.userAttr.isAdmin, 55 | user_fansLightName: "", 56 | user_levelImage: "" 57 | } 58 | return msg 59 | } 60 | getLevel(arr, type) { 61 | if (!arr || arr.length === 0) { 62 | return 0 63 | } 64 | let item = arr.find(i => { 65 | return i.imageType === type 66 | }) 67 | if (item) { 68 | return parseInt(item.content.level) 69 | } else { 70 | return 0 71 | } 72 | } 73 | messageParse(dom) { 74 | if (!dom[this.propsId].children.props.message) { 75 | return null 76 | } 77 | let msg = dom[this.propsId].children.props.message.payload 78 | let result = { 79 | gift_id: null, 80 | gift_name: null, 81 | gift_number: null, 82 | gift_image: null, 83 | gift_diamondCount: null, 84 | gift_describe: null, 85 | } 86 | result = Object.assign(result, this.getUser(msg.user)) 87 | switch (msg.common.method) { 88 | case 'WebcastGiftMessage': 89 | result = Object.assign(result, { 90 | msg_content: msg.common.describe, 91 | isGift: true, 92 | gift_id: msg.gift.id, 93 | gift_name: msg.gift.name, 94 | gift_number: parseInt(msg.comboCount), 95 | gift_image: msg.gift.icon.urlListList[0], 96 | gift_diamondCount: msg.gift.diamondCount, 97 | gift_describe: msg.gift.describe, 98 | }) 99 | break 100 | case 'WebcastChatMessage': 101 | result = Object.assign(result, { 102 | isGift: false, 103 | msg_content: msg.content 104 | }) 105 | break 106 | default: 107 | result = Object.assign(result, { 108 | isGift: false, 109 | msg_content: msg.content 110 | }) 111 | break 112 | } 113 | return result 114 | } 115 | } 116 | document.querySelector('.basicPlayer').remove() 117 | 118 | console.log('脚本安装中..') 119 | let bar = new Barrage() 120 | 121 | bar.runServer() 122 | 123 | let chatTemplate = `
124 | Avatar 126 |
127 |
{nick}
128 |
130 |
{text}
131 | {time} 132 |
133 |
134 |
` 135 | 136 | function insertDom(msg) { 137 | dcount++ 138 | if (dcount.length > 100) { 139 | _box.innerHTML = '' 140 | } 141 | let item = document.createElement('li') 142 | let time = new Date().toLocaleTimeString() 143 | item.innerHTML = chatTemplate.replace('{src}', msg.user_avatar).replace('{nick}', msg.user_nickName).replace('{time}', time).replace('{text}', msg.msg_content) 144 | _box.appendChild(item) 145 | chatWindow.scrollTop = chatWindow.scrollHeight; 146 | } -------------------------------------------------------------------------------- /ThirdLib/tiktok_barrage_nodejs/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dy", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "dy", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "ws": "^8.12.1" 13 | } 14 | }, 15 | "node_modules/ws": { 16 | "version": "8.13.0", 17 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", 18 | "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", 19 | "engines": { 20 | "node": ">=10.0.0" 21 | }, 22 | "peerDependencies": { 23 | "bufferutil": "^4.0.1", 24 | "utf-8-validate": ">=5.0.2" 25 | }, 26 | "peerDependenciesMeta": { 27 | "bufferutil": { 28 | "optional": true 29 | }, 30 | "utf-8-validate": { 31 | "optional": true 32 | } 33 | } 34 | } 35 | }, 36 | "dependencies": { 37 | "ws": { 38 | "version": "8.13.0", 39 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", 40 | "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", 41 | "requires": {} 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /ThirdLib/tiktok_barrage_nodejs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dy", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "node ./app.js", 8 | "test": "node ./test/index.js" 9 | }, 10 | "type": "module", 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "ws": "^8.12.1" 15 | } 16 | } -------------------------------------------------------------------------------- /ThirdLib/tiktok_barrage_nodejs/template/ak.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 上传txt文件并读取第32行内容 7 | 8 | 9 | 10 | 11 | 50 | 51 | 52 | 53 | 54 |

已读取的数字:

55 | 56 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /ThirdLib/tiktok_barrage_nodejs/template/message.json: -------------------------------------------------------------------------------- 1 | { 2 | "common": { 3 | "method": "WebcastChatMessage", 4 | "msgId": "7210094747141936131", 5 | "roomId": "7210091933195176763", 6 | "createTime": "0", 7 | "monitor": 0, 8 | "isShowMsg": true, 9 | "describe": "", 10 | "foldType": "0", 11 | "anchorFoldType": "0", 12 | "priorityScore": "31000", 13 | "logId": "", 14 | "msgProcessFilterK": "", 15 | "msgProcessFilterV": "", 16 | "anchorFoldTypeV2": "0", 17 | "processAtSeiTimeMs": "0", 18 | "randomDispatchMs": "0", 19 | "isDispatch": false, 20 | "channelId": "0", 21 | "diffSei2absSecond": "0", 22 | "anchorFoldDuration": "0", 23 | "appId": "1128" 24 | }, 25 | "user": { 26 | "id": "3413316642287822", 27 | "shortId": "4161375834", 28 | "nickname": "。。。睡咯啊", 29 | "gender": 0, 30 | "signature": "", 31 | "level": 0, 32 | "birthday": "0", 33 | "telephone": "", 34 | "avatarThumb": { 35 | "urlListList": [ 36 | "https://p26.douyinpic.com/aweme/100x100/aweme-avatar/tos-cn-i-0813c001_1f12e0c1a9ea42ab84007a2fb4f894ea.jpeg?from=3067671334" 37 | ], 38 | "uri": "", 39 | "height": "0", 40 | "width": "0", 41 | "avgColor": "", 42 | "imageType": 0, 43 | "openWebUrl": "", 44 | "isAnimated": false, 45 | "flexSettingListList": [], 46 | "textSettingListList": [], 47 | "urlList": [ 48 | "https://p26.douyinpic.com/aweme/100x100/aweme-avatar/tos-cn-i-0813c001_1f12e0c1a9ea42ab84007a2fb4f894ea.jpeg?from=3067671334" 49 | ], 50 | "flexSettingList": [], 51 | "textSettingList": [] 52 | }, 53 | "verified": false, 54 | "experience": 0, 55 | "city": "", 56 | "status": 0, 57 | "createTime": "0", 58 | "modifyTime": "0", 59 | "secret": 0, 60 | "shareQrcodeUri": "", 61 | "incomeSharePercent": 0, 62 | "badgeImageListList": [{ 63 | "urlListList": [ 64 | "http://p6-webcast.douyinpic.com/img/webcast/user_grade_level_v5_3.png~tplv-obj.image" 65 | ], 66 | "uri": "", 67 | "height": "16", 68 | "width": "32", 69 | "avgColor": "", 70 | "imageType": 1, 71 | "openWebUrl": "", 72 | "content": { 73 | "name": "", 74 | "fontColor": "", 75 | "level": "3", 76 | "alternativeText": "荣誉等级3级勋章" 77 | }, 78 | "isAnimated": false, 79 | "flexSettingListList": [], 80 | "textSettingListList": [], 81 | "urlList": [ 82 | "http://p6-webcast.douyinpic.com/img/webcast/user_grade_level_v5_3.png~tplv-obj.image" 83 | ], 84 | "flexSettingList": [], 85 | "textSettingList": [] 86 | }], 87 | "followInfo": { 88 | "followingCount": "184", 89 | "followerCount": "39", 90 | "followStatus": "0", 91 | "pushStatus": "0", 92 | "remarkName": "", 93 | "followerCountStr": "39", 94 | "followingCountStr": "184" 95 | }, 96 | "payGrade": { 97 | "totalDiamondCount": "0", 98 | "name": "", 99 | "nextName": "", 100 | "level": "3", 101 | "nextDiamond": "0", 102 | "nowDiamond": "0", 103 | "thisGradeMinDiamond": "0", 104 | "thisGradeMaxDiamond": "0", 105 | "payDiamondBak": "0", 106 | "gradeDescribe": "", 107 | "gradeIconListList": [], 108 | "screenChatType": "0", 109 | "newImIconWithLevel": { 110 | "urlListList": [ 111 | "http://p6-webcast.douyinpic.com/img/webcast/user_grade_level_v5_3.png~tplv-obj.image" 112 | ], 113 | "uri": "", 114 | "height": "16", 115 | "width": "32", 116 | "avgColor": "", 117 | "imageType": 1, 118 | "openWebUrl": "", 119 | "isAnimated": false, 120 | "flexSettingListList": [], 121 | "textSettingListList": [], 122 | "urlList": [ 123 | "http://p6-webcast.douyinpic.com/img/webcast/user_grade_level_v5_3.png~tplv-obj.image" 124 | ], 125 | "flexSettingList": [], 126 | "textSettingList": [] 127 | }, 128 | "newLiveIcon": { 129 | "urlListList": [ 130 | "http://p6-webcast.douyinpic.com/img/webcast/aweme_pay_grade_2x_1_4.png~tplv-obj.image" 131 | ], 132 | "uri": "", 133 | "height": "12", 134 | "width": "12", 135 | "avgColor": "", 136 | "imageType": 1, 137 | "openWebUrl": "", 138 | "isAnimated": false, 139 | "flexSettingListList": [], 140 | "textSettingListList": [], 141 | "urlList": [ 142 | "http://p6-webcast.douyinpic.com/img/webcast/aweme_pay_grade_2x_1_4.png~tplv-obj.image" 143 | ], 144 | "flexSettingList": [], 145 | "textSettingList": [] 146 | }, 147 | "upgradeNeedConsume": "0", 148 | "nextPrivileges": "", 149 | "score": "0", 150 | "gradeBanner": "", 151 | "gradeIconList": [] 152 | }, 153 | "fansClub": { 154 | "data": { 155 | "clubName": "", 156 | "level": 0, 157 | "userFansClubStatus": 0, 158 | "badge": { 159 | "iconsMap": [ 160 | [ 161 | 0, 162 | { 163 | "urlListList": [], 164 | "uri": "", 165 | "height": "0", 166 | "width": "0", 167 | "avgColor": "", 168 | "imageType": 0, 169 | "openWebUrl": "", 170 | "isAnimated": false, 171 | "flexSettingListList": [], 172 | "textSettingListList": [], 173 | "urlList": [], 174 | "flexSettingList": [], 175 | "textSettingList": [] 176 | } 177 | ] 178 | ], 179 | "title": "" 180 | }, 181 | "availableGiftIdsList": [], 182 | "anchorId": "0", 183 | "badgeType": 0, 184 | "availableGiftIds": [] 185 | }, 186 | "preferDataMap": [] 187 | }, 188 | "specialId": "", 189 | "realTimeIconsList": [], 190 | "newRealTimeIconsList": [], 191 | "topVipNo": "0", 192 | "userAttr": { 193 | "isMuted": false, 194 | "isAdmin": false, 195 | "isSuperAdmin": false, 196 | "adminPrivilegesList": [], 197 | "adminPrivileges": [] 198 | }, 199 | "payScore": "0", 200 | "ticketCount": "0", 201 | "linkMicStats": 0, 202 | "displayId": "dy1fvvffm7t1q", 203 | "withCommercePermission": false, 204 | "withFusionShopEntry": false, 205 | "totalRechargeDiamondCount": "0", 206 | "verifiedContent": "", 207 | "topFansList": [], 208 | "secUid": "MS4wLjABAAAA8rFE7QOyWveakw6ndoai8gh3dkzPPCH6Ckkxv__ft5qJRddQVnXxme-t2FgRYLOV", 209 | "userRole": 0, 210 | "authorizationInfo": 3, 211 | "adversaryAuthorizationInfo": 0, 212 | "mediaBadgeImageListList": [], 213 | "adversaryUserStatus": 0, 214 | "commerceWebcastConfigIdsList": [], 215 | "badgeImageListV2List": [{ 216 | "urlListList": [ 217 | "http://p6-webcast.douyinpic.com/img/webcast/user_grade_level_v5_3.png~tplv-obj.image" 218 | ], 219 | "uri": "", 220 | "height": "16", 221 | "width": "32", 222 | "avgColor": "", 223 | "imageType": 1, 224 | "openWebUrl": "", 225 | "content": { 226 | "name": "", 227 | "fontColor": "", 228 | "level": "3", 229 | "alternativeText": "荣誉等级3级勋章" 230 | }, 231 | "isAnimated": false, 232 | "flexSettingListList": [], 233 | "textSettingListList": [], 234 | "urlList": [ 235 | "http://p6-webcast.douyinpic.com/img/webcast/user_grade_level_v5_3.png~tplv-obj.image" 236 | ], 237 | "flexSettingList": [], 238 | "textSettingList": [] 239 | }], 240 | "locationCity": "", 241 | "remarkName": "", 242 | "mysteryMan": 1, 243 | "webRid": "", 244 | "desensitizedNickname": "。。。睡咯啊", 245 | "isAnonymous": false, 246 | "consumeDiamondLevel": 0, 247 | "webcastUid": "", 248 | "allowBeLocated": false, 249 | "allowFindByContacts": false, 250 | "allowOthersDownloadVideo": false, 251 | "allowOthersDownloadWhenSharingVideo": false, 252 | "allowShareShowProfile": false, 253 | "allowShowInGossip": false, 254 | "allowShowMyAction": false, 255 | "allowStrangeComment": false, 256 | "allowUnfollowerComment": false, 257 | "allowUseLinkmic": false, 258 | "bgImgUrl": "", 259 | "birthdayDescription": "", 260 | "birthdayValid": false, 261 | "blockStatus": 0, 262 | "commentRestrict": 0, 263 | "constellation": "", 264 | "disableIchat": 0, 265 | "enableIchatImg": "0", 266 | "exp": 0, 267 | "fanTicketCount": "0", 268 | "foldStrangerChat": false, 269 | "followStatus": "0", 270 | "hotsoonVerified": false, 271 | "hotsoonVerifiedReason": "", 272 | "ichatRestrictType": 0, 273 | "idStr": "", 274 | "isFollower": false, 275 | "isFollowing": false, 276 | "needProfileGuide": false, 277 | "payScores": "0", 278 | "pushCommentStatus": false, 279 | "pushDigg": false, 280 | "pushFollow": false, 281 | "pushFriendAction": false, 282 | "pushIchat": false, 283 | "pushStatus": false, 284 | "pushVideoPost": false, 285 | "pushVideoRecommend": false, 286 | "verifiedMobile": false, 287 | "verifiedReason": "", 288 | "withCarManagementPermission": false, 289 | "ageRange": 0, 290 | "watchDurationMonth": "0", 291 | "badgeImageList": [{ 292 | "urlListList": [ 293 | "http://p6-webcast.douyinpic.com/img/webcast/user_grade_level_v5_3.png~tplv-obj.image" 294 | ], 295 | "uri": "", 296 | "height": "16", 297 | "width": "32", 298 | "avgColor": "", 299 | "imageType": 1, 300 | "openWebUrl": "", 301 | "content": { 302 | "name": "", 303 | "fontColor": "", 304 | "level": "3", 305 | "alternativeText": "荣誉等级3级勋章" 306 | }, 307 | "isAnimated": false, 308 | "flexSettingListList": [], 309 | "textSettingListList": [], 310 | "urlList": [ 311 | "http://p6-webcast.douyinpic.com/img/webcast/user_grade_level_v5_3.png~tplv-obj.image" 312 | ], 313 | "flexSettingList": [], 314 | "textSettingList": [] 315 | }], 316 | "realTimeIcons": [], 317 | "newRealTimeIcons": [], 318 | "topFans": [], 319 | "mediaBadgeImageList": [], 320 | "commerceWebcastConfigIds": [], 321 | "badgeImageListV2": [{ 322 | "urlListList": [ 323 | "http://p6-webcast.douyinpic.com/img/webcast/user_grade_level_v5_3.png~tplv-obj.image" 324 | ], 325 | "uri": "", 326 | "height": "16", 327 | "width": "32", 328 | "avgColor": "", 329 | "imageType": 1, 330 | "openWebUrl": "", 331 | "content": { 332 | "name": "", 333 | "fontColor": "", 334 | "level": "3", 335 | "alternativeText": "荣誉等级3级勋章" 336 | }, 337 | "isAnimated": false, 338 | "flexSettingListList": [], 339 | "textSettingListList": [], 340 | "urlList": [ 341 | "http://p6-webcast.douyinpic.com/img/webcast/user_grade_level_v5_3.png~tplv-obj.image" 342 | ], 343 | "flexSettingList": [], 344 | "textSettingList": [] 345 | }] 346 | }, 347 | "content": "来个火车摇", 348 | "visibleToSender": false, 349 | "fullScreenTextColor": "", 350 | "publicAreaCommon": { 351 | "userLabel": { 352 | "urlListList": [ 353 | "http://p6-webcast.douyinpic.com/img/webcast/userlabel_new_watch.png~tplv-obj.image" 354 | ], 355 | "uri": "", 356 | "height": "0", 357 | "width": "0", 358 | "avgColor": "#BCD9E0", 359 | "imageType": 0, 360 | "openWebUrl": "", 361 | "isAnimated": false, 362 | "flexSettingListList": [], 363 | "textSettingListList": [], 364 | "urlList": [ 365 | "http://p6-webcast.douyinpic.com/img/webcast/userlabel_new_watch.png~tplv-obj.image" 366 | ], 367 | "flexSettingList": [], 368 | "textSettingList": [] 369 | }, 370 | "userConsumeInRoom": "0", 371 | "userSendGiftCntInRoom": "0", 372 | "individualPriority": "0", 373 | "individualStrategyResultMap": [] 374 | }, 375 | "agreeMsgId": "0", 376 | "priorityLevel": 0, 377 | "eventTime": "1678731024", 378 | "sendReview": false, 379 | "fromIntercom": false, 380 | "intercomHideUserCard": false, 381 | "chatTagsList": [], 382 | "chatBy": "0", 383 | "individualChatPriority": 0, 384 | "chatTags": [] 385 | } -------------------------------------------------------------------------------- /ThirdLib/tiktok_barrage_nodejs/test/index.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path, { parse } from 'path' 3 | 4 | let filepath = process.cwd() + '/template/message.json' 5 | let b = fs.readFileSync(filepath) 6 | 7 | 8 | function parseMesg() { 9 | let msg = JSON.parse(b.toString()) 10 | let result = { 11 | user_nickName: msg.user.nickname, 12 | user_id: msg.user.id, 13 | user_gender: msg.user.gender === 1 ? '男' : '女', 14 | user_level: msg.user.level, 15 | user_levelImage: msg.user.badgeImageListList[0].urlListList[0], 16 | user_avatar: msg.user.avatarThumb.urlListList[0], 17 | user_isAdmin: msg.user.userAttr.isAdmin, 18 | user_fansLevel: parseInt(msg.user.badgeImageListV2List[0].content.level), 19 | user_fansLightName: msg.user.badgeImageListV2List[0].content.alternativeText, 20 | } 21 | switch (msg.common.method) { 22 | case 'WebcastGiftMessage': 23 | result = Object.assign(result, { 24 | message: msg.common.describe, 25 | isGift: true, 26 | gift_id: msg.gift.id, 27 | gift_name: msg.gift.name, 28 | gift_number: parseInt(msg.groupCount), 29 | gift_image: msg.gift.icon.urlListList[0], 30 | gift_diamondCount: msg.gift.diamondCount, 31 | gift_describe: msg.gift.describe, 32 | }) 33 | break 34 | case 'WebcastChatMessage': 35 | result = Object.assign(result, { 36 | isGift: false, 37 | message: msg.content 38 | }) 39 | break 40 | } 41 | return result 42 | } 43 | 44 | parseMesg() -------------------------------------------------------------------------------- /Utils/Common.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import Qt 2 | from PyQt5.QtGui import QImage, QPainter, QBrush, QColor, QPixmap 3 | 4 | 5 | class Common: 6 | 7 | @staticmethod 8 | def round_avatar(pixmap: QPixmap): 9 | # round avatar 10 | size = pixmap.size() 11 | image = QImage(size, QImage.Format_ARGB32) 12 | image.fill(Qt.transparent) 13 | painter = QPainter(image) 14 | painter.setRenderHint(QPainter.Antialiasing) 15 | painter.setBrush(QBrush(QColor(0, 0, 0, 0))) 16 | painter.drawEllipse(0, 0, size.width(), size.height()) 17 | painter.setBrush(QBrush(pixmap)) 18 | painter.drawEllipse(0, 0, size.width(), size.height()) 19 | painter.end() 20 | return QPixmap.fromImage(image) 21 | -------------------------------------------------------------------------------- /Utils/Config.py: -------------------------------------------------------------------------------- 1 | import json 2 | import random 3 | 4 | 5 | class Config: 6 | 7 | def get_api_key(self): 8 | with open("config.json", "r") as f: 9 | data = json.loads(f.read()) 10 | # randomly generate a number 0 to 100 11 | return data['api_keys'][random.randint(0, len(data['api_keys']) - 1)] 12 | 13 | def get_proxy_url(self): 14 | with open("config.json", "r") as f: 15 | data = json.loads(f.read()) 16 | return data['proxy']['url'] 17 | 18 | def is_proxy_on(self): 19 | with open("config.json", "r") as f: 20 | data = json.loads(f.read()) 21 | return data['proxy']['on'] 22 | 23 | def is_api_proxy_on(self): 24 | with open("config.json", "r") as f: 25 | data = json.loads(f.read()) 26 | return data['api_proxy']['on'] 27 | 28 | def get_api_proxy_url(self): 29 | with open("config.json", "r") as f: 30 | data = json.loads(f.read()) 31 | return data['api_proxy']['url'] 32 | -------------------------------------------------------------------------------- /Utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .Config import Config 2 | from .Common import Common -------------------------------------------------------------------------------- /Windows/UI/MainWindow/MainWindow.py: -------------------------------------------------------------------------------- 1 | # extends UI_MainWindow 2 | from PyQt5.QtCore import pyqtSignal 3 | 4 | from Barrage import blivedm 5 | from Model import Huya 6 | from Model.TikTok import BarrageMessage 7 | from .MainWindow_UI import Ui_MainWindow 8 | 9 | 10 | class MainWindowUI(Ui_MainWindow): 11 | update_ask_signal = pyqtSignal(BarrageMessage) 12 | update_ask_bilibili_signal = pyqtSignal(blivedm.DanmakuMessage) 13 | update_ask_huya_signal = pyqtSignal(Huya.BarrageMessage) 14 | update_answer_signal = pyqtSignal(str) 15 | update_wait_list_signal = pyqtSignal(list) 16 | 17 | def __init__(self): 18 | super().__init__() 19 | -------------------------------------------------------------------------------- /Windows/UI/MainWindow/MainWindow.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | ../../../Resources/Images/bilibili_default_avatar.jpg 4 | ../../../Resources/Images/huya_default_avatar.png 5 | ../../../Resources/Images/tiktok_default_avatar.jfif 6 | ../../../Resources/Images/ai_logo.png 7 | ../../../Resources/Images/download.png 8 | ../../../Resources/Images/ChatGPT_logo.svg.png 9 | 10 | 11 | -------------------------------------------------------------------------------- /Windows/UI/MainWindow/MainWindow_UI.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file 'MainWindow_UI.ui' 4 | # 5 | # Created by: PyQt5 UI code generator 5.15.9 6 | # 7 | # WARNING: Any manual changes made to this file will be lost when pyuic5 is 8 | # run again. Do not edit this file unless you know what you are doing. 9 | 10 | 11 | from PyQt5 import QtCore, QtGui, QtWidgets 12 | from PyQt5.QtCore import QObject 13 | 14 | 15 | class Ui_MainWindow(QObject): 16 | def setupUi(self, MainWindow): 17 | MainWindow.setObjectName("MainWindow") 18 | MainWindow.resize(1101, 612) 19 | MainWindow.setStyleSheet("*{\n" 20 | "background-color: rgb(0, 0, 0);\n" 21 | "}\n" 22 | "QScrollBar:vertical {\n" 23 | " border: 2px solid #999999;\n" 24 | " background: #F0F0F0;\n" 25 | " width: 10px;\n" 26 | " margin: 0px 0px 0px 0px;\n" 27 | "}\n" 28 | "\n" 29 | "QScrollBar::handle:vertical {\n" 30 | " background: #888888;\n" 31 | " min-height: 20px;\n" 32 | "}\n" 33 | "\n" 34 | "QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {\n" 35 | " background: none;\n" 36 | "}\n" 37 | "") 38 | self.centralwidget = QtWidgets.QWidget(MainWindow) 39 | self.centralwidget.setObjectName("centralwidget") 40 | self.labelBotLogo = QtWidgets.QLabel(self.centralwidget) 41 | self.labelBotLogo.setGeometry(QtCore.QRect(40, 60, 51, 51)) 42 | self.labelBotLogo.setStyleSheet("border-image: url(:/Images/Resources/Images/ai_logo.png);\n" 43 | "background-repeat: no-repeat;\n" 44 | "background-attachment: fixed;\n" 45 | "background-size: cover;\n" 46 | "border-radius: 25px;") 47 | self.labelBotLogo.setText("") 48 | self.labelBotLogo.setObjectName("labelBotLogo") 49 | self.textEdit = QtWidgets.QTextEdit(self.centralwidget) 50 | self.textEdit.setGeometry(QtCore.QRect(150, 20, 651, 391)) 51 | font = QtGui.QFont() 52 | font.setFamily("Microsoft YaHei UI") 53 | font.setPointSize(16) 54 | self.textEdit.setFont(font) 55 | self.textEdit.setStyleSheet("QTextEdit{\n" 56 | "background-color: rgba(43, 43, 43, 180);\n" 57 | "border: 1px solid #2b2b2b;\n" 58 | "border-radius: 20px;\n" 59 | "padding: 10px;\n" 60 | "color: white;\n" 61 | "}\n" 62 | "QScrollBar:vertical {\n" 63 | " border: none;\n" 64 | " background-color: #333333;\n" 65 | " width: 10px;\n" 66 | " margin: 0px 0px 0px 0px;\n" 67 | " border-radius: 5px;\n" 68 | "}\n" 69 | "\n" 70 | "QScrollBar::handle:vertical {\n" 71 | " background-color: #666666;\n" 72 | " min-height: 20px;\n" 73 | " border-radius: 5px;\n" 74 | "}\n" 75 | "\n" 76 | "QScrollBar::add-line:vertical {\n" 77 | " border: none;\n" 78 | " background-color: #333333;\n" 79 | " height: 10px;\n" 80 | " subcontrol-position: bottom;\n" 81 | " subcontrol-origin: margin;\n" 82 | " border-radius: 5px;\n" 83 | "}\n" 84 | "\n" 85 | "QScrollBar::sub-line:vertical {\n" 86 | " border: none;\n" 87 | " background-color: #333333;\n" 88 | " height: 10px;\n" 89 | " subcontrol-position: top;\n" 90 | " subcontrol-origin: margin;\n" 91 | " border-radius: 5px;\n" 92 | "}\n" 93 | "\n" 94 | "QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {\n" 95 | " background-color: none;\n" 96 | " border-radius: 5px;\n" 97 | "}") 98 | self.textEdit.setMarkdown("") 99 | self.textEdit.setObjectName("textEdit") 100 | self.labelUserLogo = QtWidgets.QLabel(self.centralwidget) 101 | self.labelUserLogo.setGeometry(QtCore.QRect(30, 510, 51, 51)) 102 | self.labelUserLogo.setStyleSheet("background-repeat: no-repeat;\n" 103 | "background-attachment: fixed;\n" 104 | "background-size: cover;") 105 | self.labelUserLogo.setText("") 106 | self.labelUserLogo.setObjectName("labelUserLogo") 107 | self.textEdit_2 = QtWidgets.QTextEdit(self.centralwidget) 108 | self.textEdit_2.setGeometry(QtCore.QRect(150, 440, 651, 121)) 109 | font = QtGui.QFont() 110 | font.setFamily("Microsoft YaHei UI") 111 | font.setPointSize(16) 112 | self.textEdit_2.setFont(font) 113 | self.textEdit_2.setStyleSheet("QTextEdit{\n" 114 | "background-color: rgba(43, 43, 43, 180);\n" 115 | "border: 1px solid #2b2b2b;\n" 116 | "border-radius: 20px;\n" 117 | "padding: 10px;\n" 118 | "color: white;\n" 119 | "}\n" 120 | "QScrollBar:vertical {\n" 121 | " border: 2px solid #999999;\n" 122 | " background: #F0F0F0;\n" 123 | " width: 10px;\n" 124 | " margin: 0px 0px 0px 0px;\n" 125 | "}\n" 126 | "\n" 127 | "QScrollBar::handle:vertical {\n" 128 | " background: #888888;\n" 129 | " min-height: 20px;\n" 130 | "}\n" 131 | "\n" 132 | "QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {\n" 133 | " background: none;\n" 134 | "}\n" 135 | "") 136 | self.textEdit_2.setObjectName("textEdit_2") 137 | self.label = QtWidgets.QLabel(self.centralwidget) 138 | self.label.setGeometry(QtCore.QRect(820, 20, 161, 21)) 139 | font = QtGui.QFont() 140 | font.setFamily("Microsoft YaHei UI") 141 | font.setPointSize(14) 142 | self.label.setFont(font) 143 | self.label.setStyleSheet("color: white;") 144 | self.label.setObjectName("label") 145 | self.labelWaitList = QtWidgets.QLabel(self.centralwidget) 146 | self.labelWaitList.setGeometry(QtCore.QRect(820, 45, 271, 511)) 147 | font = QtGui.QFont() 148 | font.setFamily("Microsoft YaHei UI") 149 | font.setPointSize(12) 150 | self.labelWaitList.setFont(font) 151 | self.labelWaitList.setStyleSheet("color: rgb(161, 161, 161);") 152 | self.labelWaitList.setText("") 153 | self.labelWaitList.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop) 154 | self.labelWaitList.setWordWrap(False) 155 | self.labelWaitList.setObjectName("labelWaitList") 156 | self.labelUsername = QtWidgets.QLabel(self.centralwidget) 157 | self.labelUsername.setGeometry(QtCore.QRect(10, 446, 131, 51)) 158 | font = QtGui.QFont() 159 | font.setFamily("Microsoft YaHei UI") 160 | font.setPointSize(14) 161 | self.labelUsername.setFont(font) 162 | self.labelUsername.setStyleSheet("color: rgb(255, 255, 255);") 163 | self.labelUsername.setText("") 164 | self.labelUsername.setAlignment(QtCore.Qt.AlignCenter) 165 | self.labelUsername.setObjectName("labelUsername") 166 | self.labelUsername_2 = QtWidgets.QLabel(self.centralwidget) 167 | self.labelUsername_2.setGeometry(QtCore.QRect(10, 10, 121, 51)) 168 | font = QtGui.QFont() 169 | font.setFamily("Microsoft YaHei UI") 170 | font.setPointSize(14) 171 | self.labelUsername_2.setFont(font) 172 | self.labelUsername_2.setStyleSheet("color: rgb(255, 255, 255);") 173 | self.labelUsername_2.setAlignment(QtCore.Qt.AlignCenter) 174 | self.labelUsername_2.setObjectName("labelUsername_2") 175 | MainWindow.setCentralWidget(self.centralwidget) 176 | self.menubar = QtWidgets.QMenuBar(MainWindow) 177 | self.menubar.setGeometry(QtCore.QRect(0, 0, 1101, 21)) 178 | self.menubar.setObjectName("menubar") 179 | MainWindow.setMenuBar(self.menubar) 180 | self.statusbar = QtWidgets.QStatusBar(MainWindow) 181 | self.statusbar.setObjectName("statusbar") 182 | MainWindow.setStatusBar(self.statusbar) 183 | 184 | self.retranslateUi(MainWindow) 185 | QtCore.QMetaObject.connectSlotsByName(MainWindow) 186 | 187 | def retranslateUi(self, MainWindow): 188 | _translate = QtCore.QCoreApplication.translate 189 | MainWindow.setWindowTitle(_translate("MainWindow", "智能AI对答 - ChatGPT引擎")) 190 | self.textEdit.setHtml(_translate("MainWindow", "\n" 191 | "\n" 194 | "


")) 195 | self.textEdit_2.setHtml(_translate("MainWindow", "\n" 196 | "\n" 199 | "


")) 200 | self.label.setText(_translate("MainWindow", "等待列表:")) 201 | self.labelUsername_2.setText(_translate("MainWindow", "@智能AI答复")) 202 | from . import MainWindow_rc 203 | -------------------------------------------------------------------------------- /Windows/UI/MainWindow/MainWindow_UI.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 1101 10 | 612 11 | 12 | 13 | 14 | 智能AI对答 - ChatGPT引擎 15 | 16 | 17 | *{ 18 | background-color: rgb(0, 0, 0); 19 | } 20 | QScrollBar:vertical { 21 | border: 2px solid #999999; 22 | background: #F0F0F0; 23 | width: 10px; 24 | margin: 0px 0px 0px 0px; 25 | } 26 | 27 | QScrollBar::handle:vertical { 28 | background: #888888; 29 | min-height: 20px; 30 | } 31 | 32 | QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { 33 | background: none; 34 | } 35 | 36 | 37 | 38 | 39 | 40 | 41 | 40 42 | 60 43 | 51 44 | 51 45 | 46 | 47 | 48 | border-image: url(:/Images/Resources/Images/ai_logo.png); 49 | background-repeat: no-repeat; 50 | background-attachment: fixed; 51 | background-size: cover; 52 | border-radius: 25px; 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 150 62 | 20 63 | 651 64 | 391 65 | 66 | 67 | 68 | 69 | Microsoft YaHei UI 70 | 16 71 | 72 | 73 | 74 | QTextEdit{ 75 | background-color: rgba(43, 43, 43, 180); 76 | border: 1px solid #2b2b2b; 77 | border-radius: 20px; 78 | padding: 10px; 79 | color: white; 80 | } 81 | QScrollBar:vertical { 82 | border: none; 83 | background-color: #333333; 84 | width: 10px; 85 | margin: 0px 0px 0px 0px; 86 | border-radius: 5px; 87 | } 88 | 89 | QScrollBar::handle:vertical { 90 | background-color: #666666; 91 | min-height: 20px; 92 | border-radius: 5px; 93 | } 94 | 95 | QScrollBar::add-line:vertical { 96 | border: none; 97 | background-color: #333333; 98 | height: 10px; 99 | subcontrol-position: bottom; 100 | subcontrol-origin: margin; 101 | border-radius: 5px; 102 | } 103 | 104 | QScrollBar::sub-line:vertical { 105 | border: none; 106 | background-color: #333333; 107 | height: 10px; 108 | subcontrol-position: top; 109 | subcontrol-origin: margin; 110 | border-radius: 5px; 111 | } 112 | 113 | QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { 114 | background-color: none; 115 | border-radius: 5px; 116 | } 117 | 118 | 119 | 120 | 121 | 122 | <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> 123 | <html><head><meta name="qrichtext" content="1" /><style type="text/css"> 124 | p, li { white-space: pre-wrap; } 125 | </style></head><body style=" font-family:'Microsoft YaHei UI'; font-size:16pt; font-weight:400; font-style:normal;"> 126 | <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:14pt;"><br /></p></body></html> 127 | 128 | 129 | 130 | 131 | 132 | 30 133 | 510 134 | 51 135 | 51 136 | 137 | 138 | 139 | background-repeat: no-repeat; 140 | background-attachment: fixed; 141 | background-size: cover; 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 150 151 | 440 152 | 651 153 | 121 154 | 155 | 156 | 157 | 158 | Microsoft YaHei UI 159 | 16 160 | 161 | 162 | 163 | QTextEdit{ 164 | background-color: rgba(43, 43, 43, 180); 165 | border: 1px solid #2b2b2b; 166 | border-radius: 20px; 167 | padding: 10px; 168 | color: white; 169 | } 170 | QScrollBar:vertical { 171 | border: 2px solid #999999; 172 | background: #F0F0F0; 173 | width: 10px; 174 | margin: 0px 0px 0px 0px; 175 | } 176 | 177 | QScrollBar::handle:vertical { 178 | background: #888888; 179 | min-height: 20px; 180 | } 181 | 182 | QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { 183 | background: none; 184 | } 185 | 186 | 187 | 188 | <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> 189 | <html><head><meta name="qrichtext" content="1" /><style type="text/css"> 190 | p, li { white-space: pre-wrap; } 191 | </style></head><body style=" font-family:'Microsoft YaHei UI'; font-size:16pt; font-weight:400; font-style:normal;"> 192 | <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:11pt;"><br /></p></body></html> 193 | 194 | 195 | 196 | 197 | 198 | 820 199 | 20 200 | 161 201 | 21 202 | 203 | 204 | 205 | 206 | Microsoft YaHei UI 207 | 14 208 | 209 | 210 | 211 | color: white; 212 | 213 | 214 | 等待列表: 215 | 216 | 217 | 218 | 219 | 220 | 820 221 | 45 222 | 271 223 | 511 224 | 225 | 226 | 227 | 228 | Microsoft YaHei UI 229 | 12 230 | 231 | 232 | 233 | color: rgb(161, 161, 161); 234 | 235 | 236 | 237 | 238 | 239 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 240 | 241 | 242 | false 243 | 244 | 245 | 246 | 247 | 248 | 10 249 | 446 250 | 131 251 | 51 252 | 253 | 254 | 255 | 256 | Microsoft YaHei UI 257 | 14 258 | 259 | 260 | 261 | color: rgb(255, 255, 255); 262 | 263 | 264 | 265 | 266 | 267 | Qt::AlignCenter 268 | 269 | 270 | 271 | 272 | 273 | 10 274 | 10 275 | 121 276 | 51 277 | 278 | 279 | 280 | 281 | Microsoft YaHei UI 282 | 14 283 | 284 | 285 | 286 | color: rgb(255, 255, 255); 287 | 288 | 289 | @智能AI答复 290 | 291 | 292 | Qt::AlignCenter 293 | 294 | 295 | 296 | 297 | 298 | 299 | 0 300 | 0 301 | 1101 302 | 21 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | -------------------------------------------------------------------------------- /Windows/UI/MainWindow/__init__.py: -------------------------------------------------------------------------------- 1 | from .MainWindow import MainWindowUI 2 | -------------------------------------------------------------------------------- /Windows/UI/__init__.py: -------------------------------------------------------------------------------- 1 | from .MainWindow import MainWindowUI 2 | -------------------------------------------------------------------------------- /Windows/__init__.py: -------------------------------------------------------------------------------- 1 | from .UI import MainWindowUI 2 | -------------------------------------------------------------------------------- /bilibili.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import asyncio 3 | import os 4 | import sys 5 | import threading 6 | import gc 7 | 8 | import requests 9 | from PyQt5 import QtWidgets 10 | from PyQt5.QtGui import QPixmap 11 | from loguru import logger 12 | 13 | from Barrage import blivedm 14 | from GPT import Chat 15 | from Utils import Config, Common 16 | from Windows import MainWindowUI 17 | 18 | # TEST_ROOM_IDS from command line arguments 19 | if len(sys.argv) > 1: 20 | TEST_ROOM_IDS = [] 21 | for i in range(1, len(sys.argv)): 22 | TEST_ROOM_IDS.append(int(sys.argv[i])) 23 | else: 24 | TEST_ROOM_IDS = [ 25 | 9519547 26 | ] 27 | logger.warning("No room id specified, using default room id: 9519547") 28 | 29 | MAIN_WINDOW = MainWindowUI() 30 | LIVE_MSG_CONTENT = "" 31 | PENDING_LIST = [] 32 | 33 | 34 | async def main(): 35 | await run_multi_clients() 36 | 37 | 38 | async def run_multi_clients(): 39 | clients = [blivedm.BLiveClient(room_id) for room_id in TEST_ROOM_IDS] 40 | handler = MyHandler() 41 | for client in clients: 42 | client.add_handler(handler) 43 | client.start() 44 | 45 | try: 46 | await asyncio.gather(*( 47 | client.join() for client in clients 48 | )) 49 | finally: 50 | await asyncio.gather(*( 51 | client.stop_and_close() for client in clients 52 | )) 53 | 54 | 55 | class MyHandler(blivedm.BaseHandler): 56 | _CMD_CALLBACK_DICT = blivedm.BaseHandler._CMD_CALLBACK_DICT.copy() 57 | 58 | async def __interact_word_callback(self, client: blivedm.BLiveClient, command: dict): 59 | # print(f"[{client.room_id}] INTERACT_WORD: self_type={type(self).__name__}, room_id={client.room_id}," 60 | # f" uname={command['data']['uname']}") 61 | pass 62 | 63 | _CMD_CALLBACK_DICT['INTERACT_WORD'] = __interact_word_callback # noqa 64 | 65 | async def _on_heartbeat(self, client: blivedm.BLiveClient, message: blivedm.HeartbeatMessage): 66 | # print(f'[{client.room_id}] 当前人气值:{message.popularity}') 67 | # not working, always 1 68 | pass 69 | 70 | async def _on_danmaku(self, client: blivedm.BLiveClient, message: blivedm.DanmakuMessage): 71 | global PENDING_LIST 72 | logger.info(f"[{client.room_id}] {message.uname}: {message.msg}") 73 | PENDING_LIST.append(message) 74 | MAIN_WINDOW.update_wait_list_signal.emit(PENDING_LIST) 75 | 76 | async def _on_gift(self, client: blivedm.BLiveClient, message: blivedm.GiftMessage): 77 | logger.info(f"[{client.room_id}] {message.uname} 送出了 {message.gift_name} x {message.num}" 78 | f"({message.coin_type}瓜子x{message.total_coin})") 79 | 80 | async def _on_buy_guard(self, client: blivedm.BLiveClient, message: blivedm.GuardBuyMessage): 81 | logger.info(f"[{client.room_id}] {message.username} 购买了 {message.gift_name}") 82 | 83 | async def _on_super_chat(self, client: blivedm.BLiveClient, message: blivedm.SuperChatMessage): 84 | logger.info(f"[{client.room_id}] ¥{message.price} {message.uname} 超级留言:{message.message} ") 85 | 86 | 87 | def update_answer_msg(msg: str): 88 | MAIN_WINDOW.textEdit.setMarkdown(msg) 89 | # scroll to bottom 90 | MAIN_WINDOW.textEdit.moveCursor(MAIN_WINDOW.textEdit.textCursor().End) 91 | 92 | 93 | def get_user_avatar(user_id: int): 94 | rep = requests.get(f"https://api.bilibili.com/x/space/wbi/acc/info?mid={user_id}", 95 | headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' 96 | 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36'}) 97 | try: 98 | if rep.status_code == 200 and rep.json().get('data') is not None: 99 | # if key 'data' exists 100 | return rep.json()['data']['face'] 101 | except: 102 | pass 103 | return "https://i0.hdslb.com/bfs/face/member/noface.jpg" 104 | 105 | 106 | def process_pending_list(): 107 | global PENDING_LIST 108 | while True: 109 | if len(PENDING_LIST) > 0: 110 | message = PENDING_LIST.pop(0) 111 | MAIN_WINDOW.update_ask_bilibili_signal.emit(message) 112 | chat = Chat(Config().get_api_key()) 113 | answer = chat.chat(message.msg, stream=True) 114 | answer_content = '' 115 | for char in answer: 116 | answer_content += char 117 | MAIN_WINDOW.update_answer_signal.emit(answer_content) 118 | MAIN_WINDOW.update_wait_list_signal.emit(PENDING_LIST) 119 | gc.collect() 120 | 121 | 122 | def process_wait_list(wait_list: list): 123 | # convert list to string and update 124 | wait_list_str = '' 125 | for i in wait_list: 126 | wait_list_str += f"{i.uname}: {i.msg}\n\n" 127 | MAIN_WINDOW.labelWaitList.setText(wait_list_str) 128 | 129 | 130 | def update_ask_msg(msg: blivedm.DanmakuMessage): 131 | MAIN_WINDOW.textEdit_2.setText(msg.msg) 132 | MAIN_WINDOW.labelUsername.setText(f"@{msg.uname}") 133 | # rep = requests.get(get_user_avatar(msg.uid)) 134 | rep = requests.get(msg.face) 135 | try: 136 | if rep.status_code == 200: 137 | pixmap = QPixmap() 138 | pixmap.loadFromData(rep.content) 139 | 140 | # round avatar 141 | MAIN_WINDOW.labelUserLogo.setPixmap(Common.round_avatar(pixmap)) 142 | except: 143 | # load default avatar 144 | MAIN_WINDOW.labelUserLogo.setPixmap(Common.round_avatar(QPixmap("Resources/bilibili_default_avatar.jpg"))) 145 | MAIN_WINDOW.labelUserLogo.setScaledContents(True) 146 | 147 | 148 | def run_main_window(): 149 | app = QtWidgets.QApplication(sys.argv) 150 | MainWindow = QtWidgets.QMainWindow() 151 | MAIN_WINDOW.setupUi(MainWindow) 152 | MAIN_WINDOW.update_ask_bilibili_signal.connect(update_ask_msg) 153 | MAIN_WINDOW.update_answer_signal.connect(update_answer_msg) 154 | MAIN_WINDOW.update_wait_list_signal.connect(process_wait_list) 155 | MainWindow.setWindowTitle("Bilibili Chatbot") 156 | MainWindow.show() 157 | sys.exit(app.exec_()) 158 | 159 | 160 | if __name__ == '__main__': 161 | # set proxy for openai API 162 | os.environ["HTTP_PROXY"] = "http://127.0.0.1:10801" 163 | os.environ["HTTPS_PROXY"] = "http://127.0.0.1:10801" 164 | 165 | # thread run main window 166 | thread_main_window = threading.Thread(target=run_main_window) 167 | thread_main_window.start() 168 | 169 | # thread process pending list 170 | thread_process_pending_list = threading.Thread(target=process_pending_list) 171 | thread_process_pending_list.start() 172 | asyncio.run(main()) 173 | -------------------------------------------------------------------------------- /config.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "api_keys": [ 3 | "api key from openai." 4 | ], 5 | "api_proxy": { 6 | "url": "api proxy url", 7 | "on": true 8 | }, 9 | "proxy": { 10 | "url": "http://127.0.0.1:10801", 11 | "on": false 12 | } 13 | } -------------------------------------------------------------------------------- /huya.py: -------------------------------------------------------------------------------- 1 | from Barrage import Huya 2 | from Model.Huya import BarrageMessage 3 | import asyncio 4 | import os 5 | import threading 6 | 7 | import requests 8 | from PyQt5.QtGui import QPixmap 9 | 10 | import sys 11 | from PyQt5 import QtWidgets 12 | from Windows import MainWindowUI 13 | from GPT import Chat 14 | from Utils import Config, Common 15 | 16 | MAIN_WINDOW = MainWindowUI() 17 | LIVE_MSG_CONTENT = "" 18 | PENDING_LIST = [] 19 | GIFT_LIST = [] 20 | 21 | 22 | def update_answer_msg(msg: str): 23 | MAIN_WINDOW.textEdit.setMarkdown(msg) 24 | # scroll to bottom 25 | MAIN_WINDOW.textEdit.moveCursor(MAIN_WINDOW.textEdit.textCursor().End) 26 | 27 | 28 | def get_user_face(uid: str): 29 | return requests.get('https://v.huya.com/index.php?r=user%2Fliveinfo&uid=' + uid).json()['user_avatar'] 30 | 31 | 32 | def update_ask_msg(msg: BarrageMessage): 33 | MAIN_WINDOW.textEdit_2.setText(msg.msg_content) 34 | MAIN_WINDOW.labelUsername.setText(f"@{msg.nickname}") 35 | msg.face = get_user_face(msg.uid) 36 | try: 37 | rep = requests.get(msg.face) 38 | if rep.status_code == 200: 39 | pixmap = QPixmap() 40 | pixmap.loadFromData(rep.content) 41 | 42 | # round avatar 43 | MAIN_WINDOW.labelUserLogo.setPixmap(Common.round_avatar(pixmap)) 44 | except: 45 | MAIN_WINDOW.labelUserLogo.setPixmap(Common.round_avatar(QPixmap("Resources/huya_default_avatar.png"))) 46 | MAIN_WINDOW.labelUserLogo.setScaledContents(True) 47 | 48 | 49 | def process_pending_list(): 50 | global PENDING_LIST 51 | while True: 52 | if len(PENDING_LIST) > 0: 53 | message = PENDING_LIST.pop(0) 54 | if message.type == "gift": 55 | MAIN_WINDOW.update_wait_list_signal.emit(PENDING_LIST) 56 | continue 57 | MAIN_WINDOW.update_ask_huya_signal.emit(message) 58 | chat = Chat(Config().get_api_key()) 59 | answer = chat.chat(message.msg_content, stream=True) 60 | answer_content = '' 61 | for char in answer: 62 | answer_content += char 63 | MAIN_WINDOW.update_answer_signal.emit(answer_content) 64 | MAIN_WINDOW.update_wait_list_signal.emit(PENDING_LIST) 65 | 66 | 67 | def process_wait_list(wait_list: list): 68 | # convert list to string and update 69 | wait_list_str = '' 70 | for i in wait_list: 71 | wait_list_str += f"{i.nickname}: {i.msg_content}\n\n" 72 | MAIN_WINDOW.labelWaitList.setText(wait_list_str) 73 | 74 | 75 | def run_main_window(): 76 | app = QtWidgets.QApplication(sys.argv) 77 | MainWindow = QtWidgets.QMainWindow() 78 | MAIN_WINDOW.setupUi(MainWindow) 79 | MAIN_WINDOW.update_ask_huya_signal.connect(update_ask_msg) 80 | MAIN_WINDOW.update_answer_signal.connect(update_answer_msg) 81 | MAIN_WINDOW.update_wait_list_signal.connect(process_wait_list) 82 | MainWindow.setWindowTitle("Huya Chatbot") 83 | MainWindow.show() 84 | sys.exit(app.exec_()) 85 | 86 | 87 | def huya_process_message(message: BarrageMessage): 88 | global PENDING_LIST, GIFT_LIST 89 | # TODO: code gift logic here 90 | print(message) 91 | PENDING_LIST.append(message) 92 | MAIN_WINDOW.update_wait_list_signal.emit(PENDING_LIST) 93 | 94 | 95 | if __name__ == '__main__': 96 | # set proxy for openai API 97 | os.environ["HTTP_PROXY"] = "http://127.0.0.1:10801" 98 | os.environ["HTTPS_PROXY"] = "http://127.0.0.1:10801" 99 | 100 | # thread run main window 101 | thread_main_window = threading.Thread(target=run_main_window) 102 | thread_main_window.start() 103 | 104 | # thread process pending list 105 | thread_process_pending_list = threading.Thread(target=process_pending_list) 106 | thread_process_pending_list.start() 107 | 108 | client = Huya() 109 | asyncio.get_event_loop().run_until_complete(client.connect()) 110 | asyncio.ensure_future(client.receive_messages()) 111 | asyncio.ensure_future(client.process_messages(huya_process_message)) 112 | asyncio.get_event_loop().run_forever() 113 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |
2 | logo 3 |
4 |
5 | 🇺🇸English | 🇨🇳简体中文 6 |
7 | 8 | 9 | ### 🤖BarrageGPT 10 | *** 11 | **当我快完成这个项目的时候,我会完善这份readme。** 12 | 13 | ### 🖥️支持平台 14 | *** 15 | - [x] 哔哩哔哩直播 16 | - [x] 虎牙 17 | - [x] 抖音 18 | 19 | ### ❓如何使用 20 | *** 21 | 1. 拉取项目文件 22 | ```shell 23 | git clone https://github.com/SwaggyMacro/BarrageGPT.git 24 | ``` 25 | 2. 安装项目依赖 26 | ```shell 27 | pip install -r requirements.txt 28 | ``` 29 | 3. 复制配置例子文件 `config.example.json` 为 `config.json` 然后修改其中的apikey为你自己的key。 30 | - 其中`api_proxy`用于填入第三方中转OpenAI API的地址,如果没有则在`on`属性填入false即可。 31 | - `proxy`则为代理地址(http代理),如果不需要则在`on`属性填入false即可。 32 | 4. 以下几个平台的使用教程readme 33 | - [Bilibili](./Readme/readme_bilibili.md) 34 | - [Huya](./Readme/readme_huya.md) 35 | - [TikTok(Douyin)](./Readme/readme_tiktok.md) 36 | 5. 打开OBS,然后截取本项目运行的软件进行推流。 37 | 38 | ### 🖼️截图 39 | *** 40 | #### `截图:` 41 | ![bilibili](https://raw.githubusercontent.com/SwaggyMacro/BarrageGPT/master/Screenshots/Pictures/bilibili.png) 42 | ![huya](https://raw.githubusercontent.com/SwaggyMacro/BarrageGPT/master/Screenshots/Pictures/huya.png) 43 | 44 | #### `录屏:` 45 | *** 46 | - huya: [查看视频](https://raw.githubusercontent.com/SwaggyMacro/BarrageGPT/master/Screenshots/Videos/huya.mp4) 47 | - bilibili: [查看视频](https://raw.githubusercontent.com/SwaggyMacro/BarrageGPT/master/Screenshots/Videos/bilibili.mp4) 48 | 49 | ### 🤬吐槽 50 | *** 51 | **关于抖音平台直播:** 52 | 53 | 1. 首先,你必须要有1000个粉丝才能在电脑上直播。如果你不在电脑上直播,那么你就不能使用OBS推流,并且通过抓包获取抖音直播推流码的方法也不再有效。 54 | 2. 手机直播的时候,你不能静音麦克风。 55 | 3. 在我尝试了几次抖音直播后,我原本想用手机录屏直播,然后通过远程工具展示我的电脑屏幕(运行本项目的电脑),但是抖音不知道为什么连续两次无理由的封禁了我的直播(第一次我甚至都没有开播成功,只是在尝试开播,第二次我刚直播出项目的画面就被封禁了)。 56 | 57 | 所以这就是为什么本项目中没有抖音平台的使用截图。 58 | **脑瘫抖音** 59 | 60 | ### 📋任务列表 61 | *** 62 | - [ ] 礼物触发AI问答(只有送礼物的用户才能通过发送弹幕进行AI问答,比如送玫瑰花礼物可以开启AI问答) 63 | - [ ] 虚拟AI角色(在旁边显示的虚拟角色,可以进行语音朗读回复) 64 | - [x] 第三方中转OpenAI API 65 | -------------------------------------------------------------------------------- /readme_en.md: -------------------------------------------------------------------------------- 1 | ### 🤖BarrageGPT 2 | *** 3 | **Once I'm nearing completion of the project, I might plan to write a readme.md file.** 4 | 5 | ### 🖥️Support platforms 6 | *** 7 | - [x] Bilibili 8 | - [x] Huya 9 | - [x] Douyin 10 | 11 | ### ❓How to use 12 | *** 13 | 1. Pull the project files from the repository 14 | ```shell 15 | git clone https://github.com/SwaggyMacro/BarrageGPT.git 16 | ``` 17 | 2. Install the project's python dependencies 18 | ```shell 19 | pip install -r requirements.txt 20 | ``` 21 | 3. Copy the `config.example.json` to `config.json` and modify the configuration file to configure the corresponding apikey. 22 | - The `api_proxy` is used to fill in the address of the third-party intermediary OpenAI API. If there is no such address, fill in `false` in the `on` attribute. 23 | - `proxy` is the proxy address (http proxy). If you don't need it, fill in `false` in the `on` attribute. 24 | 4. Check the platform you want to live-stream on, and follow the readme below to run the corresponding script. 25 | - [Bilibili](./Readme/readme_bilibili.md) 26 | - [Huya](./Readme/readme_huya.md) 27 | - [TikTok(Douyin)](./Readme/readme_tiktok.md) 28 | 5. Open OBS and select the project software window to start streaming. 29 | 30 | ### 🖼️Screenshots 31 | *** 32 | #### `Pictures here:` 33 | ![bilibili](https://raw.githubusercontent.com/SwaggyMacro/BarrageGPT/master/Screenshots/Pictures/bilibili.png) 34 | ![huya](https://raw.githubusercontent.com/SwaggyMacro/BarrageGPT/master/Screenshots/Pictures/huya.png) 35 | 36 | #### `Videos here:` 37 | - huya: [Check the video](https://raw.githubusercontent.com/SwaggyMacro/BarrageGPT/master/Screenshots/Videos/huya.mp4) 38 | - bilibili: [Check the video](https://raw.githubusercontent.com/SwaggyMacro/BarrageGPT/master/Screenshots/Videos/bilibili.mp4) 39 | 40 | ### 🤬Gripes 41 | *** 42 | **Gripes About `TikTok` Live Streaming Platform:** 43 | 44 | 1. In order to go live from a computer, you are required to have a minimum of `1,000 followers`. If you choose not to go live from a computer, you won't be able to utilize OBS for streaming, and the method of capturing TikTok's streaming code through packet capture is no longer effective. 45 | 2. Unfortunately, you cannot mute the microphone while streaming from a mobile device. 46 | 3. I have made several attempts at live-streaming. My initial plan was to showcase my personal PC's screen (which was running the project) by using a remote tool. However, TikTok unjustly and inexplicably banned my live stream on two separate occasions. 47 | 48 | So, that's why there are no `TikTok(Douyin)` screenshots in this project. 49 | **Fuck the TikTok(Douyin)!** 50 | 51 | ### 📋TODO 52 | *** 53 | - [ ] Gift-triggered AI Q&A (Only users who have sent gifts are allowed to AI Q&A by sending barrage, such as starting an AI Q&A session with a rose gift.) 54 | - [ ] Virtual AI Characters (A virtual character displayed alongside and capable of providing voice-read responses.) 55 | - [x] Third-party intermediary API for OpenAI -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.8.3 2 | Brotli==1.0.9 3 | loguru==0.7.0 4 | pure_protobuf==3.0.0 5 | PyQt5==5.15.10 6 | PyQt5_sip==12.12.2 7 | Requests==2.31.0 8 | typing_extensions==4.9.0 9 | websockets==11.0.3 10 | -------------------------------------------------------------------------------- /tiktok.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | import threading 4 | 5 | import requests 6 | from PyQt5.QtGui import QPixmap 7 | 8 | from Barrage import Tiktok 9 | from Model.TikTok import BarrageMessage 10 | 11 | import sys 12 | from PyQt5 import QtWidgets 13 | from Windows import MainWindowUI 14 | from GPT import Chat 15 | from Utils import Config, Common 16 | 17 | MAIN_WINDOW = MainWindowUI() 18 | LIVE_MSG_CONTENT = "" 19 | PENDING_LIST = [] 20 | GIFT_LIST = [] 21 | 22 | 23 | def update_answer_msg(msg: str): 24 | MAIN_WINDOW.textEdit.setMarkdown(msg) 25 | # scroll to bottom 26 | MAIN_WINDOW.textEdit.moveCursor(MAIN_WINDOW.textEdit.textCursor().End) 27 | 28 | 29 | def update_ask_msg(msg: BarrageMessage): 30 | MAIN_WINDOW.textEdit_2.setText(msg.msg_content) 31 | MAIN_WINDOW.labelUsername.setText(f"@{msg.user_nickname}") 32 | try: 33 | rep = requests.get(msg.user_avatar) 34 | if rep.status_code == 200: 35 | pixmap = QPixmap() 36 | pixmap.loadFromData(rep.content) 37 | MAIN_WINDOW.labelUserLogo.setPixmap(Common.round_avatar(pixmap)) 38 | except: 39 | MAIN_WINDOW.labelUserLogo.setPixmap(Common.round_avatar(QPixmap("Resources/tiktok_default_avatar.jfif"))) 40 | MAIN_WINDOW.labelUserLogo.setScaledContents(True) 41 | 42 | 43 | def process_pending_list(): 44 | global PENDING_LIST 45 | while True: 46 | if len(PENDING_LIST) > 0: 47 | message = PENDING_LIST.pop(0) 48 | MAIN_WINDOW.update_ask_signal.emit(message) 49 | chat = Chat(Config().get_api_key()) 50 | answer = chat.chat(message.msg_content, stream=True) 51 | answer_content = '' 52 | for char in answer: 53 | answer_content += char 54 | MAIN_WINDOW.update_answer_signal.emit(answer_content) 55 | MAIN_WINDOW.update_wait_list_signal.emit(PENDING_LIST) 56 | 57 | 58 | def process_wait_list(wait_list: list): 59 | # convert list to string and update 60 | wait_list_str = '' 61 | for i in wait_list: 62 | wait_list_str += f"{i.user_nickname}: {i.msg_content}\n\n" 63 | MAIN_WINDOW.labelWaitList.setText(wait_list_str) 64 | 65 | 66 | def tiktok_process_message(message: BarrageMessage): 67 | global PENDING_LIST, GIFT_LIST 68 | # TODO: code gift logic here 69 | PENDING_LIST.append(message) 70 | MAIN_WINDOW.update_wait_list_signal.emit(PENDING_LIST) 71 | 72 | 73 | def run_main_window(): 74 | app = QtWidgets.QApplication(sys.argv) 75 | MainWindow = QtWidgets.QMainWindow() 76 | MAIN_WINDOW.setupUi(MainWindow) 77 | MAIN_WINDOW.update_ask_signal.connect(update_ask_msg) 78 | MAIN_WINDOW.update_answer_signal.connect(update_answer_msg) 79 | MAIN_WINDOW.update_wait_list_signal.connect(process_wait_list) 80 | MainWindow.show() 81 | sys.exit(app.exec_()) 82 | 83 | 84 | if __name__ == "__main__": 85 | # set proxy for openai API 86 | if Config().is_proxy_on(): 87 | os.environ["HTTP_PROXY"] = Config().get_proxy_url() 88 | os.environ["HTTPS_PROXY"] = Config().get_proxy_url() 89 | 90 | 91 | # thread run main window 92 | thread_main_window = threading.Thread(target=run_main_window) 93 | thread_main_window.start() 94 | 95 | # thread process pending list 96 | thread_process_pending_list = threading.Thread(target=process_pending_list) 97 | thread_process_pending_list.start() 98 | 99 | # tiktok barrage 100 | client = Tiktok() 101 | asyncio.get_event_loop().run_until_complete(client.connect()) 102 | asyncio.ensure_future(client.receive_messages()) 103 | asyncio.ensure_future(client.process_messages(tiktok_process_message)) 104 | asyncio.get_event_loop().run_forever() 105 | --------------------------------------------------------------------------------