├── .gitignore ├── LICENSE ├── README.md ├── app.ts ├── bilive ├── @types │ ├── bilive.d.ts │ └── danmaku.d.ts ├── danmuLog.ts ├── db.ts ├── dm_client_re.ts ├── index.ts ├── lib │ ├── app_client.ts │ ├── dm_client.ts │ └── tools.ts ├── listener.ts ├── options.default.json ├── options.ts ├── roomlistener.ts └── wsserver.ts ├── ecosystem.config.js ├── nedb └── roomList.db ├── package.json └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # Build files 61 | build 62 | .vscode 63 | package-lock.json 64 | 65 | # User generated options 66 | options -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Vector000 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bilive_server 2 | 3 | [![Node.js](https://img.shields.io/badge/Node.js-v10.0%2B-green.svg)](https://nodejs.org) 4 | [![Commitizen friendly](https://img.shields.io/badge/Commitizen-Friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) 5 | ![GitHub repo size](https://img.shields.io/github/repo-size/Vector000/bilive_server.svg) 6 | [![MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/Vector000/bilive_server/blob/2.1.0-beta/LICENSE) 7 | 8 | * 这是一个次分支,感谢所有对[主分支](https://github.com/lzghzr/bilive_client)做出贡献的人及其他同类开源软件的开发者 9 | * 有兴趣支持原作者的,请朝这里打钱=>[给lzghzr打钱](https://github.com/lzghzr/bilive_client/wiki) 10 | * 有兴趣向我投喂的,请朝这里打钱=>[请给我钱](https://github.com/Vector000/Something_Serious/blob/master/pics/mm_reward_qrcode.png) 11 | 12 | ## 自行编译 13 | 14 | * 第一次使用 15 | 1. 安装[Git](https://git-scm.com/downloads) 16 | 2. 安装[Node.js](https://nodejs.org/) 17 | 3. 命令行 `git clone https://github.com/Vector000/bilive_server.git` 18 | 4. 命令行 `cd bilive_server` 19 | 5. 命令行 `mkdir options` 20 | 6. 命令行 `cp nedb/roomList.db options/roomList.db` 21 | 7. 命令行 `npm install` 22 | 8. 命令行 `npm run build` 23 | 9. 命令行 `npm start` 24 | 25 | * 版本更新 26 | 1. 定位到目录 27 | 2. 命令行 `git pull` 28 | 3. 命令行 `npm install` 29 | 4. 命令行 `npm run build` 30 | 5. 命令行 `npm start` 31 | 32 | [点此进行设置](http://vector000.coding.me/bilive_setting/#path=ws://localhost:20080&protocol=admin) 33 | 34 | 此为服务端, 仅用来监听房间弹幕, 更多功能请使用[客户端](https://github.com/Vector000/bilive_client) 35 | -------------------------------------------------------------------------------- /app.ts: -------------------------------------------------------------------------------- 1 | const BiLive = require(__dirname + '/bilive/index').default 2 | const bilive = new BiLive() 3 | bilive.Start() -------------------------------------------------------------------------------- /bilive/@types/bilive.d.ts: -------------------------------------------------------------------------------- 1 | /******************* 2 | ****** index ****** 3 | *******************/ 4 | /** 5 | * 应用设置 6 | * 7 | * @interface options 8 | */ 9 | interface options { 10 | server: server 11 | config: config 12 | user: userCollection 13 | newUserData: userData 14 | info: optionsInfo 15 | } 16 | interface server { 17 | path: string 18 | hostname: string 19 | port: number 20 | protocol: string 21 | netkey: string 22 | } 23 | interface config { 24 | [index: string]: number | boolean | string | string[] 25 | dbTime: number 26 | globalListener: boolean 27 | globalListenNum: number 28 | adminServerChan: string 29 | liveOrigin: string 30 | apiVCOrigin: string 31 | apiLiveOrigin: string 32 | excludeCMD: string[] 33 | sysmsg: string 34 | } 35 | interface userCollection { 36 | [index: string]: userData 37 | } 38 | interface userData { 39 | [index: string]: string | boolean 40 | status: boolean 41 | userHash: string 42 | welcome: string 43 | usermsg: string 44 | smallTV: boolean 45 | raffle: boolean 46 | lottery: boolean 47 | pklottery: boolean 48 | beatStorm: boolean 49 | } 50 | interface optionsInfo { 51 | [index: string]: configInfoData 52 | liveOrigin: configInfoData 53 | apiVCOrigin: configInfoData 54 | apiLiveOrigin: configInfoData 55 | excludeCMD: configInfoData 56 | sysmsg: configInfoData 57 | status: configInfoData 58 | userHash: configInfoData 59 | welcome: configInfoData 60 | usermsg: configInfoData 61 | smallTV: configInfoData 62 | raffle: configInfoData 63 | lottery: configInfoData 64 | pklottery: configInfoData 65 | beatStorm: configInfoData 66 | } 67 | interface configInfoData { 68 | description: string 69 | tip: string 70 | type: string 71 | cognate?: string 72 | } 73 | /******************* 74 | ****** User ****** 75 | *******************/ 76 | interface AppClient { 77 | readonly actionKey: string 78 | readonly platform: string 79 | readonly appKey: string 80 | readonly build: string 81 | readonly device: string 82 | readonly mobiApp: string 83 | readonly TS: number 84 | readonly RND: number 85 | readonly DeviceID: string 86 | readonly baseQuery: string 87 | signQuery(params: string, ts?: boolean): string 88 | signQueryBase(params?: string): string 89 | readonly status: typeof status 90 | captcha: string 91 | userName: string 92 | passWord: string 93 | biliUID: number 94 | accessToken: string 95 | refreshToken: string 96 | cookieString: string 97 | headers: Headers 98 | init(): Promise 99 | getCaptcha(): Promise 100 | login(): Promise 101 | logout(): Promise 102 | refresh(): Promise 103 | } 104 | /******************* 105 | **** dm_client **** 106 | *******************/ 107 | declare enum dmErrorStatus { 108 | 'client' = 0, 109 | 'danmaku' = 1, 110 | 'timeout' = 2 111 | } 112 | interface DMclientOptions { 113 | roomID?: number 114 | userID?: number 115 | protocol?: DMclientProtocol 116 | } 117 | type DMclientProtocol = 'socket' | 'flash' | 'ws' | 'wss' 118 | type DMerror = DMclientError | DMdanmakuError 119 | interface DMclientError { 120 | status: dmErrorStatus.client | dmErrorStatus.timeout 121 | error: Error 122 | } 123 | interface DMdanmakuError { 124 | status: dmErrorStatus.danmaku 125 | error: TypeError 126 | data: Buffer 127 | } 128 | // 弹幕服务器 129 | interface danmuInfo { 130 | code: number 131 | message: string 132 | ttl: number 133 | data: danmuInfoData 134 | } 135 | interface danmuInfoData { 136 | refresh_row_factor: number 137 | refresh_rate: number 138 | max_delay: number 139 | token: string 140 | host_list: danmuInfoDataHostList[] 141 | ip_list: danmuInfoDataIPList[] 142 | } 143 | interface danmuInfoDataHostList { 144 | host: string 145 | port: number 146 | wss_port: number 147 | ws_port: number 148 | } 149 | interface danmuInfoDataIPList { 150 | host: string 151 | port: number 152 | } 153 | /******************* 154 | *** app_client **** 155 | *******************/ 156 | declare enum appStatus { 157 | 'success' = 0, 158 | 'captcha' = 1, 159 | 'error' = 2, 160 | 'httpError' = 3 161 | } 162 | /** 163 | * 公钥返回 164 | * 165 | * @interface getKeyResponse 166 | */ 167 | interface getKeyResponse { 168 | ts: number 169 | code: number 170 | data: getKeyResponseData 171 | } 172 | interface getKeyResponseData { 173 | hash: string 174 | key: string 175 | } 176 | /** 177 | * 验证返回 178 | * 179 | * @interface authResponse 180 | */ 181 | interface authResponse { 182 | ts: number 183 | code: number 184 | data: authResponseData 185 | } 186 | interface authResponseData { 187 | status: number 188 | token_info: authResponseTokeninfo 189 | cookie_info: authResponseCookieinfo 190 | sso: string[] 191 | } 192 | interface authResponseCookieinfo { 193 | cookies: authResponseCookieinfoCooky[] 194 | domains: string[] 195 | } 196 | interface authResponseCookieinfoCooky { 197 | name: string 198 | value: string 199 | http_only: number 200 | expires: number 201 | } 202 | interface authResponseTokeninfo { 203 | mid: number 204 | access_token: string 205 | refresh_token: string 206 | expires_in: number 207 | } 208 | /** 209 | * 注销返回 210 | * 211 | * @interface revokeResponse 212 | */ 213 | interface revokeResponse { 214 | message: string 215 | ts: number 216 | code: number 217 | } 218 | /** 219 | * 登录返回信息 220 | */ 221 | type loginResponse = loginResponseSuccess | loginResponseCaptcha | loginResponseError | loginResponseHttp 222 | interface loginResponseSuccess { 223 | status: appStatus.success 224 | data: authResponse 225 | } 226 | interface loginResponseCaptcha { 227 | status: appStatus.captcha 228 | data: authResponse 229 | } 230 | interface loginResponseError { 231 | status: appStatus.error 232 | data: authResponse 233 | } 234 | interface loginResponseHttp { 235 | status: appStatus.httpError 236 | data: XHRresponse | XHRresponse | undefined 237 | } 238 | /** 239 | * 登出返回信息 240 | */ 241 | type logoutResponse = revokeResponseSuccess | revokeResponseError | revokeResponseHttp 242 | interface revokeResponseSuccess { 243 | status: appStatus.success 244 | data: revokeResponse 245 | } 246 | interface revokeResponseError { 247 | status: appStatus.error 248 | data: revokeResponse 249 | } 250 | interface revokeResponseHttp { 251 | status: appStatus.httpError 252 | data: XHRresponse | undefined 253 | } 254 | /** 255 | * 验证码返回信息 256 | */ 257 | type captchaResponse = captchaResponseSuccess | captchaResponseError 258 | interface captchaResponseSuccess { 259 | status: appStatus.success 260 | data: Buffer 261 | } 262 | interface captchaResponseError { 263 | status: appStatus.error 264 | data: XHRresponse | undefined 265 | } 266 | /******************* 267 | ****** tools ****** 268 | *******************/ 269 | /** 270 | * XHR返回 271 | * 272 | * @interface response 273 | * @template T 274 | */ 275 | interface XHRresponse { 276 | response: { 277 | statusCode: number 278 | } 279 | body: T 280 | } 281 | /** 282 | * 客户端消息 283 | * 284 | * @interface systemMSG 285 | */ 286 | interface systemMSG { 287 | message: string 288 | options: options 289 | } 290 | /** 291 | * Server酱 292 | * 293 | * @interface serverChan 294 | */ 295 | interface serverChan { 296 | errno: number 297 | errmsg: string 298 | dataset: string 299 | } 300 | /******************* 301 | ******* db ******** 302 | *******************/ 303 | /** 304 | * db.roomList 305 | * 306 | * @interface roomList 307 | */ 308 | interface roomList { 309 | roomID: number 310 | masterID: number 311 | beatStorm: number 312 | smallTV: number 313 | raffle: number 314 | updateTime: number 315 | } 316 | /******************* 317 | ** bilive_client ** 318 | *******************/ 319 | /** 320 | * 消息格式 321 | * 322 | * @interface raffleMessage 323 | */ 324 | interface raffleMessage { 325 | cmd: 'raffle' 326 | roomID: number 327 | id: number 328 | type: string 329 | title: string 330 | time: number 331 | max_time: number 332 | time_wait: number 333 | } 334 | /** 335 | * 消息格式 336 | * 337 | * @interface lotteryMessage 338 | */ 339 | interface lotteryMessage { 340 | cmd: 'lottery' | 'pklottery' 341 | roomID: number 342 | id: number 343 | type: string 344 | title: string 345 | time: number 346 | } 347 | /** 348 | * 消息格式 349 | * 350 | * @interface beatStormMessage 351 | */ 352 | interface beatStormMessage { 353 | cmd: 'beatStorm' 354 | roomID: number 355 | id: number 356 | num: number 357 | type: string 358 | title: string 359 | time: number 360 | } 361 | /** 362 | * 消息格式 363 | * 364 | * @interface systemMessage 365 | */ 366 | interface systemMessage { 367 | cmd: 'sysmsg' 368 | msg: string 369 | ts?: string 370 | } 371 | type message = systemMessage | raffleMessage | lotteryMessage | beatStormMessage 372 | /******************* 373 | **** listener ***** 374 | *******************/ 375 | /** 376 | * 统一抽奖信息 377 | * 378 | * @interface lotteryInfo 379 | */ 380 | interface lotteryInfo { 381 | code: number 382 | message: string 383 | ttl: number 384 | data: lotteryInfoData 385 | } 386 | interface lotteryInfoData { 387 | activity_box: null 388 | bls_box: null 389 | gift_list: lotteryInfoDataGiftList[] 390 | guard: lotteryInfoDataGuard[] 391 | pk: lotteryInfoDataPk[] 392 | slive_box: lotteryInfoDataSilverBox 393 | storm: lotteryInfoDataStorm 394 | } 395 | interface lotteryInfoDataGiftList { 396 | raffleId: number 397 | title: string 398 | type: string 399 | payflow_id: number 400 | from_user: lotteryInfoDataGiftListFromUser 401 | time_wait: number 402 | time: number 403 | max_time: number 404 | status: number 405 | asset_animation_pic: string 406 | asset_tips_pic: string 407 | sender_type: number 408 | } 409 | interface lotteryInfoDataGiftListFromUser { 410 | uname: string 411 | face: string 412 | } 413 | interface lotteryInfoDataGuard { 414 | id: number 415 | sender: lotteryInfoDataGuardSender 416 | keyword: string 417 | privilege_type: number 418 | time: number 419 | status: number 420 | payflow_id: string 421 | } 422 | interface lotteryInfoDataGuardSender { 423 | uid: number 424 | uname: string 425 | face: string 426 | } 427 | interface lotteryInfoDataPk { 428 | id: number 429 | pk_id: number 430 | room_id: number 431 | time: number 432 | status: number 433 | asset_icon: string 434 | asset_animation_pic: string 435 | title: string 436 | max_time: number 437 | } 438 | interface lotteryInfoDataSilverBox { 439 | minute: number 440 | silver: number 441 | time_end: number 442 | time_start: number 443 | times: number 444 | max_times: number 445 | status: number 446 | } 447 | interface lotteryInfoDataStorm { 448 | id: number 449 | num: number 450 | time: number 451 | content: string 452 | hadJoin: number 453 | storm_gif: string 454 | } 455 | /** 456 | * 获取直播列表 457 | * 458 | * @interface getAllList 459 | */ 460 | interface getAllList { 461 | code: number 462 | msg: string 463 | message: string 464 | data: getAllListData 465 | } 466 | interface getAllListData { 467 | interval: number 468 | module_list: getAllListDataList[] 469 | } 470 | type getAllListDataList = getAllListDataModules | getAllListDataRooms 471 | interface getAllListDataModules { 472 | module_info: getAllListDataModuleInfo 473 | list: getAllListDataModuleList[] 474 | } 475 | interface getAllListDataRooms { 476 | module_info: getAllListDataRoomInfo 477 | list: getAllListDataRoomList[] 478 | } 479 | interface getAllListDataBaseInfo { 480 | id: number 481 | type: number 482 | pic: string 483 | title: string 484 | link: string 485 | } 486 | interface getAllListDataModuleInfo extends getAllListDataBaseInfo { 487 | count?: number 488 | } 489 | interface getAllListDataRoomInfo extends getAllListDataBaseInfo { 490 | type: 6 | 9 491 | } 492 | interface getAllListDataModuleList { 493 | id: number 494 | pic: string 495 | link: string 496 | title: string 497 | } 498 | interface getAllListDataRoomList { 499 | roomid: number 500 | title: string 501 | uname: string 502 | online: number 503 | cover: string 504 | link: string 505 | face: string 506 | area_v2_parent_id: number 507 | area_v2_parent_name: string 508 | area_v2_id: number 509 | area_v2_name: string 510 | play_url: string 511 | current_quality: number 512 | accept_quality: number[] 513 | broadcast_type: number 514 | pendent_ld: string 515 | pendent_ru: string 516 | rec_type: number 517 | pk_id: number 518 | } 519 | /******************* 520 | ** roomlistener *** 521 | *******************/ 522 | /** 523 | * 房间信息 524 | * 525 | * @interface roomInit 526 | */ 527 | interface roomInit { 528 | code: number 529 | msg: string 530 | message: string 531 | data: roomInitDataData 532 | } 533 | interface roomInitDataData { 534 | room_id: number 535 | short_id: number 536 | uid: number 537 | need_p2p: number 538 | is_hidden: boolean 539 | is_locked: boolean 540 | is_portrait: boolean 541 | live_status: number 542 | hidden_till: number 543 | lock_till: number 544 | encrypted: boolean 545 | pwd_verified: boolean 546 | } 547 | /******************* 548 | ***** wsserver **** 549 | *******************/ 550 | /** 551 | * WebSocket消息 552 | * 553 | * @interface clientMessage 554 | */ 555 | interface adminMessage { 556 | cmd: string 557 | ts: string 558 | msg?: string 559 | uid?: string 560 | data?: config | optionsInfo | string[] | userData | connectedUser[] 561 | } 562 | /** 563 | * 已连接用户 564 | * 565 | * @interface connectedUser 566 | */ 567 | interface connectedUser { 568 | protocol: string 569 | ip: string 570 | ua: string 571 | } -------------------------------------------------------------------------------- /bilive/@types/danmaku.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 弹幕基本格式 3 | * 4 | * @interface danmuJson 5 | */ 6 | interface danmuJson { 7 | /** 关键字 */ 8 | cmd: string 9 | roomid?: number 10 | /** dm_client自动添加 */ 11 | _roomid: number 12 | } 13 | /** 14 | * 弹幕消息 15 | * {"info":[[0,5,25,16738408,1517306023,1405289835,0,"c23b254e",0],"好想抱回家",[37089851,"Dark2笑",0,1,1,10000,1,"#7c1482"],[17,"言叶","枫言w",367,16752445,"union"],[35,0,10512625,">50000"],["title-140-2","title-140-2"],0,1,{"uname_color":"#7c1482"}],"cmd":"DANMU_MSG","_roomid":1175880} 16 | * 17 | * @interface DANMU_MSG 18 | * @extends {danmuJson} 19 | */ 20 | interface DANMU_MSG extends danmuJson { 21 | info: DANMU_MSG_info 22 | } 23 | interface DANMU_MSG_info extends Array { 24 | /** 弹幕信息 */ 25 | 0: DANMU_MSG_info_danmu 26 | /** 弹幕内容 */ 27 | 1: string 28 | /** 用户信息 */ 29 | 2: DANMU_MSG_info_user 30 | /** 用户徽章 */ 31 | 3: DANMU_MSG_info_medal 32 | /** 用户排行 */ 33 | 4: DANMU_MSG_info_rank 34 | /** teamid */ 35 | 5: number 36 | /** 舰队等级 */ 37 | 6: number 38 | 7: DANMU_MSG_info_other 39 | } 40 | interface DANMU_MSG_info_danmu extends Array { 41 | 0: number 42 | /** 模式 */ 43 | 1: number 44 | /** 字号 */ 45 | 2: number 46 | /** 颜色 */ 47 | 3: number 48 | /** 发送时间 */ 49 | 4: number 50 | /** rnd */ 51 | 5: number | string 52 | 6: number 53 | /** uid crc32 */ 54 | 7: string 55 | 8: number 56 | } 57 | interface DANMU_MSG_info_user extends Array { 58 | /** 用户uid */ 59 | 0: number 60 | /** 用户名 */ 61 | 1: string 62 | /** 是否为管理员 */ 63 | 2: 0 | 1 64 | /** 是否为月费老爷 */ 65 | 3: 0 | 1 66 | /** 是否为年费老爷 */ 67 | 4: 0 | 1 68 | /** 直播间排行 */ 69 | 5: number 70 | 6: number 71 | /** 用户名颜色, #32进制颜色代码 */ 72 | 7: string 73 | } 74 | interface DANMU_MSG_info_medal extends Array { 75 | /** 徽章等级 */ 76 | 0: number 77 | /** 勋章名 */ 78 | 1: string 79 | /** 主播名 */ 80 | 2: string 81 | /** 直播间, 字符串的貌似是原始房间号 */ 82 | 3: number | string 83 | 4: number, 84 | /** 特殊样式 */ 85 | 5: 'union' | string 86 | } 87 | interface DANMU_MSG_info_rank extends Array { 88 | /** 用户等级 */ 89 | 0: number 90 | 1: number 91 | 2: number 92 | /** 等级排名, 具体值为number */ 93 | 3: number | string 94 | } 95 | interface DANMU_MSG_info_title extends Array { 96 | /** 头衔标识 */ 97 | 0: string 98 | /** 头衔图片 */ 99 | 1: string 100 | } 101 | interface DANMU_MSG_info_other { 102 | /** #32进制颜色代码 */ 103 | uname_color: string 104 | } 105 | /** 106 | * 礼物消息, 用户包裹和瓜子的数据直接在里面, 真是窒息 107 | * {"cmd":"SEND_GIFT","data":{"giftName":"B坷垃","num":1,"uname":"Vilitarain","rcost":28963232,"uid":2081485,"top_list":[{"uid":3091444,"uname":"丶你真难听","face":"http://i1.hdslb.com/bfs/face/b1e39bae99efc6277b95993cd2a0d7c176b52ce2.jpg","rank":1,"score":1657600,"guard_level":3,"isSelf":0},{"uid":135813741,"uname":"EricOuO","face":"http://i2.hdslb.com/bfs/face/db8cf9a9506d2e3fe6dcb3d8f2eee4da6c0e3e2d.jpg","rank":2,"score":1606200,"guard_level":2,"isSelf":0},{"uid":10084110,"uname":"平凡无奇迷某人","face":"http://i2.hdslb.com/bfs/face/df316f596d7dcd8625de7028172027aa399323af.jpg","rank":3,"score":1333100,"guard_level":3,"isSelf":0}],"timestamp":1517306026,"giftId":3,"giftType":0,"action":"赠送","super":1,"price":9900,"rnd":"1517301823","newMedal":1,"newTitle":0,"medal":{"medalId":"397","medalName":"七某人","level":1},"title":"","beatId":"0","biz_source":"live","metadata":"","remain":0,"gold":100,"silver":77904,"eventScore":0,"eventNum":0,"smalltv_msg":[],"specialGift":null,"notice_msg":[],"capsule":{"normal":{"coin":68,"change":1,"progress":{"now":1100,"max":10000}},"colorful":{"coin":0,"change":0,"progress":{"now":0,"max":5000}}},"addFollow":0,"effect_block":0},"_roomid":50583} 108 | * 109 | * @interface SEND_GIFT 110 | * @extends {danmuJson} 111 | */ 112 | interface SEND_GIFT extends danmuJson { 113 | data: SEND_GIFT_data 114 | } 115 | interface SEND_GIFT_data { 116 | /** 道具文案 */ 117 | giftName: string 118 | /** 数量 */ 119 | num: number 120 | /** 用户名 */ 121 | uname: string 122 | /** 主播积分 */ 123 | rcost: number 124 | /** 用户uid */ 125 | uid: number 126 | /** 更新排行 */ 127 | top_list: SEND_GIFT_data_top_list[] 128 | /** 用户提供的rnd, 正常为10位 */ 129 | timestamp: number 130 | /** 礼物id */ 131 | giftId: number 132 | /** 礼物类型(普通, 弹幕, 活动) */ 133 | giftType: number 134 | action: '喂食' | '赠送' 135 | /** 高能 */ 136 | super: 0 | 1 137 | /** 价值 */ 138 | price: number 139 | rnd: string 140 | /** 是否获取到新徽章 */ 141 | newMedal: 0 | 1 142 | /** 是否获取到新头衔 */ 143 | newTitle: 0 | 1 144 | /** 新徽章 */ 145 | medal: SEND_GIFT_data_medal | any[] 146 | /** 新头衔 */ 147 | title: string 148 | /** 节奏风暴内容id \d | u\d+ */ 149 | beatId: 0 | '' | string 150 | biz_source: 'live' 151 | metadata: string 152 | /** 道具包裹剩余数量 */ 153 | remain: number 154 | /** 剩余金瓜子 */ 155 | gold: number 156 | /** 剩余银瓜子 */ 157 | silver: number 158 | /** 主播活动积分, 普通道具为0 */ 159 | eventScore: number 160 | eventNum: number 161 | /** 小电视 */ 162 | smalltv_msg: SYS_MSG[] | any[] 163 | /** 特殊礼物 */ 164 | specialGift: SPECIAL_GIFT_data | null 165 | /** SYS_GIFT */ 166 | notice_msg: string[] | any[] 167 | /** 扭蛋 */ 168 | capsule: SEND_GIFT_data_capsule 169 | /** 是否新关注 */ 170 | addFollow: 0 | 1 171 | /** 估计只有辣条才能是1 */ 172 | effect_block: 0 | 1 173 | } 174 | interface SEND_GIFT_data_top_list { 175 | /** 用户uid */ 176 | uid: number 177 | /** 用户名 */ 178 | uname: string 179 | /** 头像地址 */ 180 | face: string 181 | /** 排行 */ 182 | rank: number 183 | /** 投喂总数 */ 184 | score: number 185 | /** 舰队等级 */ 186 | guard_level: number 187 | /** 是否本人 */ 188 | isSelf: 0 | 1 189 | } 190 | interface SEND_GIFT_data_medal { 191 | /** 徽章id */ 192 | medalId: string 193 | /** 徽章名 */ 194 | medalName: string 195 | /** 徽章等级 */ 196 | level: 1 197 | } 198 | interface SEND_GIFT_data_capsule { 199 | /** 普通扭蛋 */ 200 | normal: SEND_GIFT_data_capsule_data 201 | /** 梦幻扭蛋 */ 202 | colorful: SEND_GIFT_data_capsule_data 203 | } 204 | interface SEND_GIFT_data_capsule_data { 205 | /** 数量 */ 206 | coin: number 207 | /** 数量发生变化 */ 208 | change: number 209 | progress: SEND_GIFT_data_capsule_data_progress 210 | } 211 | interface SEND_GIFT_data_capsule_data_progress { 212 | /** 当前送出道具价值 */ 213 | now: number 214 | /** 需要的道具价值 */ 215 | max: number 216 | } 217 | /** 218 | * 礼物连击, 我十分怀疑之前的COMBO_END是打错了 219 | * {"cmd":"COMBO_SEND","data":{"uid":12767334,"uname":"Edoのsomo愛豆子","combo_num":2,"gift_name":"???","gift_id":20007,"action":"赠送"},"_roomid":1274658} 220 | * 221 | * @interface COMBO_SEND 222 | * @extends {danmuJson} 223 | */ 224 | interface COMBO_SEND extends danmuJson { 225 | /** 礼物连击 */ 226 | data: COMBO_SEND_data 227 | } 228 | interface COMBO_SEND_data { 229 | /** 送礼人UID */ 230 | uid: number 231 | /** 送礼人 */ 232 | uname: string 233 | /** 连击次数 */ 234 | combo_num: number 235 | /** 礼物数量 */ 236 | gift_name: string 237 | /** 礼物名 */ 238 | gift_id: number 239 | /** 赠送, 投喂 */ 240 | action: string 241 | } 242 | /** 243 | * 礼物连击结束 244 | * {"cmd":"COMBO_END","data":{"uname":"虫章虫良阝恶霸","r_uname":"坂本叔","combo_num":99,"price":100,"gift_name":"凉了","gift_id":20010,"start_time":1527510537,"end_time":1527510610},"_roomid":5067} 245 | * 246 | * @interface COMBO_END 247 | * @extends {danmuJson} 248 | */ 249 | interface COMBO_END extends danmuJson { 250 | /** 礼物连击结束 */ 251 | data: COMBO_END_data 252 | } 253 | interface COMBO_END_data { 254 | /** 送礼人 */ 255 | uname: string 256 | /** 主播 */ 257 | r_uname: string 258 | /** 连击次数 */ 259 | combo_num: number 260 | /** 礼物价值 */ 261 | price: number 262 | /** 礼物名 */ 263 | gift_name: string 264 | /** 礼物ID */ 265 | gift_id: number 266 | /** 开始时间 */ 267 | start_time: number 268 | /** 结束时间 */ 269 | end_time: number 270 | } 271 | /** 272 | * 系统消息, 广播 273 | * {"cmd":"SYS_MSG","msg":"亚军主播【赤瞳不是翅桶是赤瞳】开播啦,一起去围观!","msg_text":"亚军主播【赤瞳不是翅桶是赤瞳】开播啦,一起去围观!","url":"http://live.bilibili.com/5198","_roomid":23058} 274 | * {"cmd":"SYS_MSG","msg":"【国民六妹】:?在直播间:?【896056】:?赠送 小电视一个,请前往抽奖","msg_text":"【国民六妹】:?在直播间:?【896056】:?赠送 小电视一个,请前往抽奖","rep":1,"styleType":2,"url":"http://live.bilibili.com/896056","roomid":896056,"real_roomid":896056,"rnd":1517304134,"tv_id":"36676","_roomid":1199214} 275 | * {"cmd":"SYS_MSG","msg":"忧伤小草:?送给:?龙崎77-:?一个摩天大楼,点击前往TA的房间去抽奖吧","msg_text":"忧伤小草:?送给:?龙崎77-:?一个摩天大楼,点击前往TA的房间去抽奖吧","rep":1,"styleType":2,"url":"http://live.bilibili.com/307","roomid":307,"real_roomid":371020,"rnd":1382374449,"tv_id":0,"_roomid":23058} 276 | * {"cmd":"SYS_MSG","msg":"丨奕玉丨:?送给:?大吉叽叽叽:?一个小电视飞船,点击前往TA的房间去抽奖吧","msg_text":"丨奕玉丨:?送给:?大吉叽叽叽:?一个小电视飞船,点击前往TA的房间去抽奖吧","msg_common":"全区广播:<%丨奕玉丨%>送给<%大吉叽叽叽%>一个小电视飞船,点击前往TA的房间去抽奖吧","msg_self":"全区广播:<%丨奕玉丨%>送给<%大吉叽叽叽%>一个小电视飞船,快来抽奖吧","rep":1,"styleType":2,"url":"http://live.bilibili.com/286","roomid":286,"real_roomid":170908,"rnd":2113258721,"broadcast_type":1,"_roomid":23058} 277 | * 278 | * @interface SYS_MSG 279 | * @extends {danmuJson} 280 | */ 281 | interface SYS_MSG extends danmuJson { 282 | /** 消息内容 */ 283 | msg: string 284 | /** 同msg */ 285 | msg_text: string 286 | /** 点击跳转的地址 */ 287 | url: string 288 | } 289 | interface SYS_MSG extends danmuJson { 290 | /** 消息内容 */ 291 | msg: string 292 | /** 同msg */ 293 | msg_text: string 294 | /** 广播: 消息内容 */ 295 | msg_common: string 296 | /** 广播: 消息内容 */ 297 | msg_self: string 298 | rep: 1 299 | /** 2为小电视通知 */ 300 | styleType: 2 301 | /** 点击跳转的地址 */ 302 | url: string 303 | /** 原始房间号 */ 304 | real_roomid: number 305 | rnd: number 306 | /** 广播类型 */ 307 | broadcast_type: number 308 | } 309 | /** 310 | * 系统礼物消息, 广播 311 | * {"cmd":"SYS_GIFT","msg":"叫我大兵就对了:? 在贪玩游戏的:?直播间5254205:?内赠送:?109:?共225个","rnd":"930578893","uid":30623524,"msg_text":"叫我大兵就对了在贪玩游戏的直播间5254205内赠送红灯笼共225个","_roomid":23058} 312 | * {"cmd":"SYS_GIFT","msg":"亚瑟不懂我心在直播间26057开启了新春抽奖,红包大派送啦!一起来沾沾喜气吧!","msg_text":"亚瑟不懂我心在直播间26057开启了新春抽奖,红包大派送啦!一起来沾沾喜气吧!","tips":"亚瑟不懂我心在直播间26057开启了新春抽奖,红包大派送啦!一起来沾沾喜气吧!","url":"http://live.bilibili.com/26057","roomid":26057,"real_roomid":26057,"giftId":110,"msgTips":0,"_roomid":23058} 313 | * 314 | * @interface SYS_GIFT 315 | * @extends {danmuJson} 316 | */ 317 | interface SYS_GIFT extends danmuJson { 318 | /** 消息内容 */ 319 | msg: string 320 | rnd: number 321 | /** 赠送人uid */ 322 | uid: number 323 | /** 同msg, 无标记符号 */ 324 | msg_text: string 325 | } 326 | interface SYS_GIFT extends danmuJson { 327 | /** 消息内容 */ 328 | msg: string 329 | /** 同msg */ 330 | msg_text: string 331 | /** 同msg */ 332 | tips: string 333 | /** 点击跳转的地址 */ 334 | url: string 335 | /** 原始房间号 */ 336 | real_roomid: number 337 | /** 礼物id */ 338 | giftId: number 339 | msgTips: number 340 | } 341 | /** 342 | * 欢迎消息 343 | * {"cmd":"WELCOME","data":{"uid":42469177,"uname":"还是森然","isadmin":0,"vip":1},"roomid":10248,"_roomid":10248} 344 | * {"cmd":"WELCOME","data":{"uid":36157605,"uname":"北熠丶","is_admin":false,"vip":1},"_roomid":5096} 345 | * 346 | * @interface WELCOME 347 | * @extends {danmuJson} 348 | */ 349 | interface WELCOME extends danmuJson { 350 | data: WELCOME_data 351 | } 352 | interface WELCOME_data_base { 353 | /** 用户uid */ 354 | uid: number 355 | /** 用户名 */ 356 | uname: string 357 | } 358 | interface WELCOME_data_base_admin extends WELCOME_data_base { 359 | /** 是否为管理员 */ 360 | isadmin: 0 | 1 361 | } 362 | interface WELCOME_data_base_admin extends WELCOME_data_base { 363 | /** 是否为管理员 */ 364 | is_admin: false 365 | } 366 | interface WELCOME_data extends WELCOME_data_base_admin { 367 | /** 是否为月费老爷 */ 368 | vip: 0 | 1 369 | } 370 | interface WELCOME_data extends WELCOME_data_base_admin { 371 | /** 是否为年费老爷 */ 372 | svip: 0 | 1 373 | } 374 | /** 375 | * 欢迎消息-舰队 376 | * {"cmd":"WELCOME_GUARD","data":{"uid":33401915,"username":"按时咬希尔","guard_level":3,"water_god":0},"roomid":1374115,"_roomid":1374115} 377 | * 378 | * @interface WELCOME_GUARD 379 | * @extends {danmuJson} 380 | */ 381 | interface WELCOME_GUARD extends danmuJson { 382 | data: WELCOME_GUARD_data 383 | } 384 | interface WELCOME_GUARD_data { 385 | /** 用户uid */ 386 | uid: number 387 | /** 用户名 */ 388 | username: string 389 | /** 舰队等级 */ 390 | guard_level: number 391 | water_god: number 392 | } 393 | /** 394 | * 欢迎消息-活动 395 | * {"cmd":"WELCOME_ACTIVITY","data":{"uid":38728279,"uname":"胖橘喵_只听歌不聊骚","type":"goodluck"},"_roomid":12722} 396 | * 397 | * @interface WELCOME_ACTIVITY 398 | * @extends {danmuJson} 399 | */ 400 | interface WELCOME_ACTIVITY extends danmuJson { 401 | data: WELCOME_ACTIVITY_data 402 | } 403 | interface WELCOME_ACTIVITY_data { 404 | /** 用户uid */ 405 | uid: number 406 | /** 用户名 */ 407 | uname: string 408 | /** 文案 */ 409 | type: string 410 | } 411 | /** 412 | * 活动入场特效 413 | * {"cmd":"ENTRY_EFFECT","data":{"id":3,"uid":210983254,"target_id":20848957,"show_avatar":1,"copy_writing":"欢迎 <%失去_理智%> 进入房间","highlight_color":"#FFF100","basemap_url":"http://i0.hdslb.com/bfs/live/d208b9654b93a70b4177e1aa7e2f0343f8a5ff1a.png","effective_time":1,"priority":50,"privilege_type":2,"face":"http://i0.hdslb.com/bfs/face/3d47da79e92d9b7c676abca94730f744d296e8cd.jpg"},"_roomid":66688} 414 | * 415 | * @interface ENTRY_EFFECT 416 | * @extends {danmuJson} 417 | */ 418 | interface ENTRY_EFFECT extends danmuJson { 419 | data: ENTRY_EFFECT_data 420 | } 421 | interface ENTRY_EFFECT_data { 422 | id: number 423 | uid: number 424 | target_id: number 425 | show_avatar: number 426 | copy_writing: string 427 | highlight_color: string 428 | basemap_url: string 429 | effective_time: number 430 | priority: number 431 | privilege_type: number 432 | face: string 433 | } 434 | /** 435 | * 舰队购买 436 | * {"cmd":"GUARD_BUY","data":{"uid":43510479,"username":"416の老木鱼","guard_level":3,"num":1},"roomid":"24308","_roomid":24308} 437 | * 438 | * @interface GUARD_BUY 439 | * @extends {danmuJson} 440 | */ 441 | interface GUARD_BUY extends danmuJson { 442 | data: GUARD_BUY_data 443 | } 444 | interface GUARD_BUY_data { 445 | /** 用户uid */ 446 | uid: number 447 | /** 用户名 */ 448 | username: string 449 | /** 舰队等级 */ 450 | guard_level: number 451 | /** 购买数量 */ 452 | num: number 453 | } 454 | /** 455 | * 舰队消息 456 | * {"cmd":"GUARD_MSG","msg":"欢迎 :?总督 Tikiあいしてる:? 登船","roomid":237328,"_roomid":237328} 457 | * {"cmd":"GUARD_MSG","msg":"用户 :?EricOuO:? 在主播 七七见奈波丶 的直播间开通了总督","buy_type":1,"_roomid":23058} 458 | * 459 | * @interface GUARD_MSG 460 | * @extends {danmuJson} 461 | */ 462 | interface GUARD_MSG extends danmuJson { 463 | /** 消息内容 */ 464 | msg: string 465 | buy_type?: number 466 | } 467 | /** 468 | * 抽奖开始 469 | * {"cmd":"RAFFLE_START","roomid":11365,"data":{"raffleId":5082,"type":"newspring","from":"LexBurner","time":60},"_roomid":11365} 470 | * {"cmd":"RAFFLE_START","data":{"id":"54588","dtime":180,"msg":{"cmd":"SYS_MSG","msg":"一圆滚滚:?送给:?-牛奶喵:?一个摩天大楼,点击前往TA的房间去抽奖吧","msg_text":"一圆滚滚:?送给:?-牛奶喵:?一个摩天大楼,点击前往TA的房间去抽奖吧","rep":1,"styleType":2,"url":"http://live.bilibili.com/344839","roomid":344839,"real_roomid":344839,"rnd":1003073948,"tv_id":0},"raffleId":54588,"title":"摩天大楼抽奖","type":"GIFT_20003","from":"一圆滚滚","from_user":{"uname":"一圆滚滚","face":"http://static.hdslb.com/images/member/noface.gif"},"time":180,"max_time":180,"time_wait":120,"asset_animation_pic":"http://i0.hdslb.com/bfs/live/7e47e9cfb744acd0319a4480e681258ce3a611fe.gif","asset_tips_pic":"http://s1.hdslb.com/bfs/live/380bcd708da496d75737c68930965dd67b82879d.png"},"_roomid":344839} 471 | * 472 | * @interface RAFFLE_START 473 | * @extends {danmuJson} 474 | */ 475 | interface RAFFLE_START extends danmuJson { 476 | data: RAFFLE_START_data 477 | } 478 | interface RAFFLE_START_data { 479 | /** 抽奖编号 */ 480 | id: string 481 | /** 持续时间 */ 482 | dtime: number 483 | /** 系统广播 */ 484 | msg: SYS_MSG 485 | /** 抽奖编号 */ 486 | raffleId: number 487 | /** 文案 */ 488 | title: string 489 | /** 文案 */ 490 | type: string 491 | /** 赠送人 */ 492 | from: string 493 | /** 持续时间 */ 494 | time: number 495 | /** 持续时间 */ 496 | max_time: number 497 | /** 等待时间 */ 498 | time_wait: number 499 | /** 动画图片 */ 500 | asset_animation_pic: string 501 | /** 静态图片 */ 502 | asset_tips_pic: string 503 | } 504 | /** 505 | * 抽奖结束 506 | * {"cmd":"RAFFLE_END","data":{"id":"56496","uname":"等着豆子发芽","sname":"外星人","giftName":"2.3333w银瓜子","mobileTips":"恭喜 等着豆子发芽 获得2.3333w银瓜子","raffleId":"56496","type":"GIFT_20003","from":"外星人","fromFace":"http://i2.hdslb.com/bfs/face/60c1d92c378f3ec9769ee8d46300d6829d14869d.jpg","fromGiftId":20003,"win":{"uname":"等着豆子发芽","face":"http://i1.hdslb.com/bfs/face/11e57d535980dfab77682427433efee9bca0bc3e.jpg","giftName":"银瓜子","giftId":"silver","giftNum":23333,"msg":"恭喜<%等着豆子发芽%>获得大奖<%2.3333w银瓜子%>, 感谢<%外星人%>的赠送"}},"_roomid":5619438} 507 | * 508 | * @interface RAFFLE_END 509 | * @extends {danmuJson} 510 | */ 511 | interface RAFFLE_END extends danmuJson { 512 | data: RAFFLE_END_data 513 | } 514 | interface RAFFLE_END_data { 515 | /** 编号 */ 516 | raffleId: number 517 | /** 文案 */ 518 | type: string 519 | /** 赠送人 */ 520 | from: string 521 | /** 赠送人头像地址 */ 522 | fromFace: string 523 | win: RAFFLE_END_data_win 524 | } 525 | interface RAFFLE_END_data_win { 526 | /** 获赠人 */ 527 | uname: string 528 | /** 获赠人头像地址 */ 529 | face: string 530 | /** 礼物名 '银瓜子' | '经验原石' */ 531 | giftName: string 532 | /** 礼物类型 'silver' | 'stuff-1' */ 533 | giftId: string 534 | /** 礼物数量 100000 | 10*/ 535 | giftNum: number 536 | /** 中奖消息 */ 537 | msg: string 538 | } 539 | /** 540 | * 中奖通知 541 | * {"cmd":"NOTICE_MSG","full":{"head_icon":"","is_anim":1,"tail_icon":"","background":"#33ffffff","color":"#33ffffff","highlight":"#33ffffff","border":"#33ffffff","time":10},"half":{"head_icon":"","is_anim":0,"tail_icon":"","background":"#33ffffff","color":"#33ffffff","highlight":"#33ffffff","border":"#33ffffff","time":8},"roomid":"360972","real_roomid":"493","msg_common":"恭喜<%千里一醉醉醉醉醉醉%>获得大奖<%100x普通扭蛋币%>, 感谢<%丨四四丨%>的赠送","msg_self":"恭喜<%千里一醉醉醉醉醉醉%>获得大奖<%100x普通扭蛋币%>, 感谢<%丨四四丨%>的赠送","link_url":"http://live.bilibili.com/493","msg_type":4,"_roomid":360972} 542 | * 543 | * @interface NOTICE_MSG 544 | * @extends {danmuJson} 545 | */ 546 | interface NOTICE_MSG extends danmuJson { 547 | full: NOTICE_MSG_style 548 | half: NOTICE_MSG_style 549 | real_roomid: string 550 | msg_common: string 551 | msg_self: string 552 | link_url: string 553 | msg_type: number 554 | } 555 | interface NOTICE_MSG_style { 556 | head_icon: string 557 | is_anim: number 558 | tail_icon: string 559 | background: string 560 | color: string 561 | highlight: string 562 | border: string 563 | time: number 564 | } 565 | /** 566 | * 小电视抽奖开始 567 | * {"cmd":"TV_START","data":{"id":"56473","dtime":180,"msg":{"cmd":"SYS_MSG","msg":"GDinBoston:?送给:?宝贤酱:?一个小电视飞船,点击前往TA的房间去抽奖吧","msg_text":"GDinBoston:?送给:?宝贤酱:?一个小电视飞船,点击前往TA的房间去抽奖吧","rep":1,"styleType":2,"url":"http://live.bilibili.com/5520","roomid":5520,"real_roomid":4069122,"rnd":1527998406,"tv_id":0},"raffleId":56473,"title":"小电视飞船抽奖","type":"small_tv","from":"GDinBoston","from_user":{"uname":"GDinBoston","face":"http://i2.hdslb.com/bfs/face/6f42b610b2b3846bf054f78c348051c21ff223f1.jpg"},"time":180,"max_time":180,"time_wait":120,"asset_animation_pic":"http://i0.hdslb.com/bfs/live/746a8db0702740ec63106581825667ae525bb11a.gif","asset_tips_pic":"http://s1.hdslb.com/bfs/live/1a3acb48c59eb10010ad53b59623e14dc1339968.png"},"_roomid":4069122} 568 | * 569 | * @interface TV_START 570 | * @extends {danmuJson} 571 | */ 572 | interface TV_START extends danmuJson { 573 | data: TV_START_data 574 | } 575 | interface TV_START_data extends RAFFLE_START_data { 576 | type: 'small_tv' 577 | /** 赠送人信息 */ 578 | from_user: TV_START_data_from 579 | time_wait: number 580 | } 581 | interface TV_START_data_from { 582 | /** 赠送人 */ 583 | uname: string 584 | /** 赠送人头像地址 */ 585 | face: string 586 | } 587 | /** 588 | * 小电视抽奖结束 589 | * {"cmd":"TV_END","data":{"id":"56503","uname":"-清柠_","sname":"君子应如兰","giftName":"小电视抱枕","mobileTips":"恭喜 -清柠_ 获得小电视抱枕","raffleId":"56503","type":"small_tv","from":"君子应如兰","fromFace":"http://i1.hdslb.com/bfs/face/dfde2619c96280fa5f3f309d20207c8426a3722b.jpg","fromGiftId":25,"win":{"uname":"-清柠_","face":"http://i2.hdslb.com/bfs/face/e37a453b392be0342de2bae3caa18533273ad043.jpg","giftName":"小电视抱枕","giftId":"small_tv","giftNum":1,"msg":"恭喜<%-清柠_%>获得大奖<%小电视抱枕%>, 感谢<%君子应如兰%>的赠送"}},"_roomid":40270} 590 | * 591 | * @interface TV_END 592 | * @extends {danmuJson} 593 | */ 594 | interface TV_END extends danmuJson { 595 | data: TV_END_data 596 | } 597 | interface TV_END_data extends RAFFLE_END_data { 598 | /** 小电视编号 */ 599 | id: string 600 | /** 获赠人 */ 601 | uname: string 602 | /** 赠送人 */ 603 | sname: string 604 | /** '10W银瓜子' | '抱枕' */ 605 | giftName: string 606 | /** 中奖消息 */ 607 | mobileTips: string 608 | } 609 | /** 610 | * 活动相关 611 | * {"roomid":11365,"cmd":"EVENT_CMD","data":{"event_type":"newspring-5082","event_img":"http://s1.hdslb.com/bfs/static/blive/live-assets/mobile/activity/newspring_2018/raffle.png"},"_roomid":11365} 612 | * 613 | * @interface EVENT_CMD 614 | * @extends {danmuJson} 615 | */ 616 | interface EVENT_CMD extends danmuJson { 617 | data: EVENT_CMD_data 618 | } 619 | interface EVENT_CMD_data { 620 | /** 文案-编号 */ 621 | event_type: string 622 | /** 图片地址 */ 623 | event_img: string 624 | } 625 | /** 626 | * 抽奖LOTTERY 627 | * {"cmd":"LOTTERY_START","data":{"id":216101,"roomid":5712065,"message":"290974992 在【5712065】购买了总督,请前往抽奖","type":"guard","link":"https://live.bilibili.com/5712065","lottery":{"id":216101,"sender":{"uid":290974992,"uname":"","face":""},"keyword":"guard","time":86400,"status":1,"mobile_display_mode":2,"mobile_static_asset":"","mobile_animation_asset":""}},"_roomid":5712065} 628 | * 629 | * @interface LOTTERY_START 630 | * @extends {danmuJson} 631 | */ 632 | interface LOTTERY_START extends danmuJson { 633 | data: LOTTERY_START_data 634 | } 635 | interface LOTTERY_START_data { 636 | /* 编号 */ 637 | id: number 638 | /* 房间号 */ 639 | roomid: number 640 | /* 消息 */ 641 | message: string 642 | /* 抽奖类型 */ 643 | type: string 644 | /* 房间链接 */ 645 | link: string 646 | /* 抽奖信息 */ 647 | lottery: LOTTERY_START_data_lottery 648 | } 649 | interface LOTTERY_START_data_lottery { 650 | /* 编号 */ 651 | id: number 652 | /* 抽奖发起人信息 */ 653 | sender: LOTTERY_START_data_lottery_sender 654 | /* 关键字, 目前和type一致 */ 655 | keyword: string 656 | time: number 657 | status: number 658 | mobile_display_mode: number 659 | mobile_static_asset: string 660 | mobile_animation_asset: string 661 | } 662 | interface LOTTERY_START_data_lottery_sender { 663 | /* 发起人uid */ 664 | uid: number 665 | /* 发起人昵称 */ 666 | uname: string 667 | /* 头像地址 */ 668 | face: string 669 | } 670 | /** 671 | * 舰队抽奖 672 | * {"cmd":"GUARD_LOTTERY_START","data":{"id":382934,"roomid":5096,"message":"千尘の可爱笋 在【5096】购买了舰长,请前往抽奖","type":"guard","privilege_type":3,"link":"https://live.bilibili.com/5096","lottery":{"id":382934,"sender":{"uid":281218616,"uname":"千尘の可爱笋","face":"http://i0.hdslb.com/bfs/face/939ea830a4ea3f3db6daba8fa900818e213ccc00.jpg"},"keyword":"guard","time":1200,"status":1,"mobile_display_mode":2,"mobile_static_asset":"","mobile_animation_asset":""}},"_roomid":5096} 673 | * 674 | * @interface GUARD_LOTTERY_START 675 | */ 676 | interface GUARD_LOTTERY_START extends LOTTERY_START { } 677 | /** 678 | * 快速抽奖 679 | * 680 | * @interface LIGHTEN_START 681 | * @extends {danmuJson} 682 | */ 683 | interface LIGHTEN_START extends danmuJson { 684 | data: LIGHTEN_START_Data 685 | } 686 | interface LIGHTEN_START_Data { 687 | type: string // 活动标识 688 | lightenId: number // 参与id 689 | time: number // 持续时间 690 | } 691 | /** 692 | * 快速抽奖结束 693 | * 694 | * @interface LIGHTEN_END 695 | * @extends {danmuJson} 696 | */ 697 | interface LIGHTEN_END extends danmuJson { 698 | data: LIGHTEN_END_Data 699 | } 700 | interface LIGHTEN_END_Data { 701 | type: string // 活动标识 702 | lightenId: number // 参与id 703 | } 704 | /** 705 | * 特殊礼物消息 706 | * {"cmd":"SPECIAL_GIFT","data":{"39":{"id":169666,"time":90,"hadJoin":0,"num":1,"content":"啦噜啦噜","action":"start","storm_gif":"http://static.hdslb.com/live-static/live-room/images/gift-section/mobilegift/2/jiezou.gif?2017011901"}},"_roomid":5096} 707 | * {"cmd":"SPECIAL_GIFT","data":{"39":{"id":169666,"action":"end"}},"_roomid":5096} 708 | * 709 | * @interface SPECIAL_GIFT 710 | * @extends {danmuJson} 711 | */ 712 | interface SPECIAL_GIFT extends danmuJson { 713 | data: SPECIAL_GIFT_data 714 | } 715 | interface SPECIAL_GIFT_data { 716 | /** 节奏风暴 */ 717 | '39': SPECIAL_GIFT_data_beatStorm 718 | } 719 | type SPECIAL_GIFT_data_beatStorm = SPECIAL_GIFT_data_beatStorm_start | SPECIAL_GIFT_data_beatStorm_end 720 | interface SPECIAL_GIFT_data_beatStorm_start { 721 | /** 节奏风暴id */ 722 | id: number 723 | /** 节奏持续时间 */ 724 | time: number 725 | /** 是否已经参与 */ 726 | hadJoin: 0 | 1 727 | /** 节奏数量 */ 728 | num: number 729 | /** 节奏内容 */ 730 | content: string 731 | /** 节奏开始 */ 732 | action: 'start' 733 | /** 节奏风暴图标地址 */ 734 | storm_gif: string 735 | } 736 | interface SPECIAL_GIFT_data_beatStorm_end { 737 | /** 节奏风暴id */ 738 | id: number 739 | /** 结束 */ 740 | action: 'end' 741 | } 742 | /** 743 | * 准备直播, 下播 744 | * {"cmd":"PREPARING","round":1,"roomid":"66287","_roomid":66287} 745 | * 746 | * @interface PREPARING 747 | * @extends {danmuJson} 748 | */ 749 | interface PREPARING extends danmuJson { 750 | round?: 1 751 | } 752 | /** 753 | * 开始直播 754 | * {"cmd":"LIVE","roomid":66688,"_roomid":66688} 755 | * 756 | * @interface LIVE 757 | * @extends {danmuJson} 758 | */ 759 | interface LIVE extends danmuJson { } 760 | /** 761 | * 开始手机直播 762 | * 763 | * @interface MOBILE_LIVE 764 | * @extends {danmuJson} 765 | */ 766 | interface MOBILE_LIVE extends danmuJson { 767 | type: 1 768 | } 769 | /** 770 | * 房间开启禁言 771 | * {"cmd":"ROOM_SILENT_ON","data":{"type":"level","level":1,"second":1517318804},"roomid":544893,"_roomid":544893} 772 | * 773 | * @interface ROOM_SILENT_ON 774 | * @extends {danmuJson} 775 | */ 776 | interface ROOM_SILENT_ON extends danmuJson { 777 | data: ROOM_SILENT_ON_data 778 | } 779 | interface ROOM_SILENT_ON_data { 780 | /** 等级 | 勋章 | 全员 */ 781 | type: 'level' | 'medal' | 'member' 782 | /** 禁言等级 */ 783 | level: number 784 | /** 禁言时间, -1为本次 */ 785 | second: number 786 | } 787 | /** 788 | * 房间禁言结束 789 | * {"cmd":"ROOM_SILENT_OFF","data":[],"roomid":"101526","_roomid":101526} 790 | * 791 | * @interface ROOM_SILENT_OFF 792 | * @extends {danmuJson} 793 | */ 794 | interface ROOM_SILENT_OFF extends danmuJson { 795 | data: any[] 796 | } 797 | /** 798 | * 房间屏蔽 799 | * {"cmd":"ROOM_SHIELD","type":0,"user":"","keyword":"","roomid":939654,"_roomid":939654} 800 | * 801 | * @interface ROOM_SHIELD 802 | * @extends {danmuJson} 803 | */ 804 | interface ROOM_SHIELD extends danmuJson { 805 | type: number 806 | user: string 807 | keyword: string 808 | } 809 | /** 810 | * 房间封禁消息 811 | * {"cmd":"ROOM_BLOCK_MSG","uid":"12482716","uname":"筱小公主","roomid":5501645,"_roomid":5501645} 812 | * 813 | * @interface ROOM_BLOCK_MSG 814 | * @extends {danmuJson} 815 | */ 816 | interface ROOM_BLOCK_MSG extends danmuJson { 817 | /** 用户uid */ 818 | uid: number 819 | /** 用户名 */ 820 | uname: string 821 | } 822 | /** 823 | * 管理员变更 824 | * {"cmd":"ROOM_ADMINS","uids":[37690892,22741742,21861760,35306422,40186466,27138800],"roomid":5667325,"_roomid":5667325} 825 | * 826 | * @interface ROOM_ADMINS 827 | * @extends {danmuJson} 828 | */ 829 | interface ROOM_ADMINS extends danmuJson { 830 | /** 管理员列表 */ 831 | uids: number[] 832 | } 833 | /** 834 | * 房间设置变更 835 | * {"cmd":"CHANGE_ROOM_INFO","background":"http://i0.hdslb.com/bfs/live/6411059a373a594e648b26d9714d7eab4ee556ed.jpg","_roomid":24308} 836 | * 837 | * @interface CHANGE_ROOM_INFO 838 | * @extends {danmuJson} 839 | */ 840 | interface CHANGE_ROOM_INFO extends danmuJson { 841 | /** 背景图片地址 */ 842 | background: string 843 | } 844 | /** 845 | * 许愿瓶 846 | * {"cmd":"WISH_BOTTLE","data":{"action":"update","id":6301,"wish":{"id":6301,"uid":610390,"type":1,"type_id":109,"wish_limit":99999,"wish_progress":39370,"status":1,"content":"灯笼挂着好看","ctime":"2018-01-21 13:20:12","count_map":[1,20,225]}},"_roomid":14893} 847 | * 848 | * @interface WISH_BOTTLE 849 | * @extends {danmuJson} 850 | */ 851 | interface WISH_BOTTLE extends danmuJson { 852 | data: WISH_BOTTLE_data 853 | } 854 | interface WISH_BOTTLE_data { 855 | action: 'update' | 'delete' | 'full' | 'create' | 'finish' 856 | /** 许愿瓶id */ 857 | id: number 858 | wish: WISH_BOTTLE_data_wish 859 | } 860 | interface WISH_BOTTLE_data_wish { 861 | /** 许愿瓶id */ 862 | id: number 863 | /** 主播uid */ 864 | uid: number 865 | type: number 866 | /** 礼物id */ 867 | type_id: number 868 | /** 礼物上限 */ 869 | wish_limit: number 870 | /** 当前礼物数量 */ 871 | wish_progress: number 872 | /** 873 | * 'delete': -1 874 | * 'update' | 'create': 1 875 | * 'full': 2 876 | * 'finish': 3 877 | */ 878 | status: number 879 | /** 礼物说明 */ 880 | content: string 881 | /** 开始时间 yyyy-MM-dd HH:mm:ss 格式 */ 882 | ctime: string 883 | /** 礼物选择数量 */ 884 | count_map: number[] 885 | } 886 | /** 887 | * 活动 888 | * {"cmd":"ACTIVITY_EVENT","data":{"keyword":"newspring_2018","type":"cracker","limit":300000,"progress":41818},"_roomid":14893} 889 | * 890 | * @interface ACTIVITY_EVENT 891 | * @extends {danmuJson} 892 | */ 893 | interface ACTIVITY_EVENT extends danmuJson { 894 | data: ACTIVITY_EVENT_data 895 | } 896 | interface ACTIVITY_EVENT_data { 897 | /** 活动标识 */ 898 | keyword: string 899 | /** 文案 */ 900 | type: string 901 | /** 积分上限 */ 902 | limit: number 903 | /** 当前积分 */ 904 | progress: number 905 | } 906 | /** 907 | * 实物抽奖结束 908 | * 909 | * @interface WIN_ACTIVITY 910 | * @extends {danmuJson} 911 | */ 912 | interface WIN_ACTIVITY extends danmuJson { 913 | /** 第n轮抽奖 */ 914 | number: number 915 | } 916 | /** 917 | * 直播警告 918 | * {"cmd":"WARNING","msg":"违反直播着装规范,请立即调整","roomid":883802,"_roomid":883802} 919 | * 920 | * @interface WARNING 921 | */ 922 | interface WARNING { 923 | msg: string 924 | } 925 | /** 926 | * 直播强制切断 927 | * {"cmd":"CUT_OFF","msg":"违反直播规范","roomid":945626,"_roomid":945626} 928 | * 929 | * @interface CUT_OFF 930 | * @extends {danmuJson} 931 | */ 932 | interface CUT_OFF extends danmuJson { 933 | /** 切断原因 */ 934 | msg: string 935 | } 936 | /** 937 | * 直播封禁 938 | * 939 | * @interface ROOM_LOCK 940 | * @extends {danmuJson} 941 | */ 942 | interface ROOM_LOCK extends danmuJson { 943 | expire: string // 封禁时间 yyyy-MM-dd HH:mm:ss 944 | } 945 | /** 946 | * 房间排行榜 947 | * {"cmd":"ROOM_RANK","data":{"roomid":1327236,"rank_desc":"元气榜 4","color":"#B15BFF","h5_url":"https://live.bilibili.com/p/eden/rank-h5?nav=hour&uid=33594828","timestamp":1525871406},"_roomid":1327236} 948 | * {"cmd":"ROOM_RANK","data":{"roomid":6154037,"rank_desc":"今日榜 49","color":"#00BB00","h5_url":"https://live.bilibili.com/pages/lpl2018/lol2018msi.html&uid=194484313","timestamp":1525871406},"_roomid":6154037} 949 | * 950 | * @interface ROOM_RANK 951 | * @extends {danmuJson} 952 | */ 953 | interface ROOM_RANK extends danmuJson { 954 | /** 房间排行榜 */ 955 | data: ROOM_RANK_Data 956 | } 957 | interface ROOM_RANK_Data { 958 | /** 房间号 */ 959 | roomid: number 960 | /** 排行榜文案 */ 961 | rank_desc: string 962 | /** 排行榜颜色 */ 963 | color: string 964 | /** 排行榜页面 */ 965 | h5_url: string 966 | timestamp: number 967 | } 968 | /** 969 | * 连麦PK 970 | * 971 | * @interface PK_MIC_Base 972 | * @extends {danmuJson} 973 | */ 974 | interface PK_MIC_Base extends danmuJson { 975 | /** PK编号 */ 976 | pk_id: number 977 | /** PK状态 */ 978 | pk_status: number 979 | } 980 | /** 981 | * PK邀请 982 | * {"cmd":"PK_INVITE_INIT","pk_invite_status":200,"invite_id":514,"face":"http://i2.hdslb.com/bfs/face/b50c99b0c989eb303e308b0574d509acab7b8012.jpg","uname":"不会编程的飞飞","area_name":"视频聊天","user_level":22,"master_level":16,"roomid":11420618,"_roomid":11420618} 983 | * 984 | * @interface PK_INVITE_INIT 985 | * @extends {danmuJson} 986 | */ 987 | interface PK_INVITE_INIT extends danmuJson { 988 | pk_invite_status: number 989 | invite_id: number 990 | face: string 991 | uname: string 992 | area_name: string 993 | user_level: number 994 | master_level: number 995 | } 996 | /** 997 | * 拒绝PK邀请 998 | * {"cmd":"PK_INVITE_REFUSE","pk_invite_status":1100,"invite_id":698,"roomid":"11741803","_roomid":11741803} 999 | * 1000 | * @interface PK_INVITE_REFUSE 1001 | * @extends {danmuJson} 1002 | */ 1003 | interface PK_INVITE_REFUSE extends danmuJson { 1004 | pk_invite_status: number 1005 | invite_id: number 1006 | } 1007 | /** 1008 | * 取消PK邀请 1009 | * {"cmd":"PK_INVITE_CANCEL","pk_invite_status":1200,"invite_id":1023,"face":"http://i2.hdslb.com/bfs/face/e68d36b37038428fd1e32f894f8c2eee6388412d.jpg","uname":"纯情的黄老师","area_name":"视频聊天","user_level":10,"master_level":19,"roomid":"5375","_roomid":5375} 1010 | * 1011 | * @interface PK_INVITE_CANCEL 1012 | * @extends {danmuJson} 1013 | */ 1014 | interface PK_INVITE_CANCEL extends danmuJson { 1015 | pk_invite_status: number 1016 | invite_id: number 1017 | face: string 1018 | uname: string 1019 | area_name: string 1020 | user_level: number 1021 | master_level: number 1022 | } 1023 | /** 1024 | * 启用PK邀请 1025 | * {"cmd":"PK_INVITE_SWITCH_OPEN","roomid":5923408,"_roomid":5923408} 1026 | * 1027 | * @interface PK_INVITE_SWITCH_OPEN 1028 | * @extends {danmuJson} 1029 | */ 1030 | interface PK_INVITE_SWITCH_OPEN extends danmuJson { } 1031 | /** 1032 | * 禁用PK邀请 1033 | * {"cmd":"PK_INVITE_SWITCH_CLOSE","roomid":5923408,"_roomid":5923408} 1034 | * 1035 | * @interface PK_INVITE_SWITCH_CLOSE 1036 | * @extends {danmuJson} 1037 | */ 1038 | interface PK_INVITE_SWITCH_CLOSE extends danmuJson { } 1039 | /** 1040 | * PK邀请失败 1041 | * {"cmd":"PK_INVITE_FAIL","pk_invite_status":1100,"invite_id":7529,"roomid":"10817769","_roomid":10817769} 1042 | * 1043 | * @interface PK_INVITE_FAIL 1044 | * @extends {danmuJson} 1045 | */ 1046 | interface PK_INVITE_FAIL extends danmuJson { 1047 | pk_invite_status: number 1048 | invite_id: number 1049 | } 1050 | /** 1051 | * PK匹配 1052 | * {"cmd":"PK_MATCH","pk_status":100,"pk_id":3291,"data":{"init_id":273022,"match_id":52320,"escape_time":5,"is_portrait":false,"uname":"栗子蛋糕酱","face":"http://i0.hdslb.com/bfs/face/6fb781f75b9c30d2d8b384793fcd02ad3238b1bd.jpg","uid":922127},"roomid":273022,"_roomid":273022} 1053 | * 1054 | * @interface PK_MATCH 1055 | * @extends {PK_MIC_Base} 1056 | */ 1057 | interface PK_MATCH extends PK_MIC_Base { 1058 | /** PK匹配 */ 1059 | data: PK_MATCH_Data 1060 | } 1061 | interface PK_MATCH_Data { 1062 | /** 发起人房间号 */ 1063 | init_id: number 1064 | /** 匹配人房间号 */ 1065 | match_id: number 1066 | /** 逃跑时间 */ 1067 | escape_time: number 1068 | is_portrait: boolean 1069 | /** 匹配人昵称 */ 1070 | uname: string 1071 | /** 匹配人头像 */ 1072 | face: string 1073 | /** 匹配人UID */ 1074 | uid: number 1075 | } 1076 | /** 1077 | * PK准备 1078 | * {"cmd":"PK_PRE","pk_id":3291,"pk_status":200,"data":{"init_id":273022,"match_id":52320,"count_down":5,"pk_topic":"跳一支舞 ","pk_pre_time":1528442545,"pk_start_time":1528442550,"pk_end_time":1528442610,"end_time":1528442670},"_roomid":273022} 1079 | * 1080 | * @interface PK_PRE 1081 | * @extends {PK_MIC_Base} 1082 | */ 1083 | interface PK_PRE extends PK_MIC_Base { 1084 | /** PK准备 */ 1085 | data: PK_PRE_Data 1086 | } 1087 | interface PK_PRE_Data { 1088 | /** 发起人房间号 */ 1089 | init_id: number 1090 | /** 匹配人房间号 */ 1091 | match_id: number 1092 | /** 倒计时 */ 1093 | count_down: number 1094 | /** PK项目 */ 1095 | pk_topic: string 1096 | /** PK匹配时间 */ 1097 | pk_pre_time: number 1098 | /** PK开始时间 */ 1099 | pk_start_time: number 1100 | /** PK结束时间 */ 1101 | pk_end_time: number 1102 | /** 结束时间 */ 1103 | end_time: number 1104 | } 1105 | /** 1106 | * PK开始 1107 | * {"cmd":"PK_START","pk_id":3291,"pk_status":300,"data":{"init_id":273022,"match_id":52320,"pk_topic":"跳一支舞 "},"_roomid":273022} 1108 | * 1109 | * @interface PK_START 1110 | * @extends {PK_MIC_Base} 1111 | */ 1112 | interface PK_START extends PK_MIC_Base { 1113 | /** PK开始 */ 1114 | data: PK_START_Data 1115 | } 1116 | interface PK_START_Data { 1117 | /** 发起人房间号 */ 1118 | init_id: number 1119 | /** 匹配人房间号 */ 1120 | match_id: number 1121 | /** PK项目 */ 1122 | pk_topic: string 1123 | } 1124 | /** 1125 | * PK进行 1126 | * {"cmd":"PK_PROCESS","pk_id":3291,"pk_status":300,"data":{"uid":220870717,"init_votes":0,"match_votes":1,"user_votes":1},"_roomid":273022} 1127 | * 1128 | * @interface PK_PROCESS 1129 | * @extends {PK_MIC_Base} 1130 | */ 1131 | interface PK_PROCESS extends PK_MIC_Base { 1132 | /** PK进行 */ 1133 | data: PK_PROCESS_Data 1134 | } 1135 | interface PK_PROCESS_Data { 1136 | /** 投票人UID */ 1137 | uid: number 1138 | /** 发起人票数 */ 1139 | init_votes: number 1140 | /** 匹配人票数 */ 1141 | match_votes: number 1142 | /** 投票人投票数 */ 1143 | user_votes: number 1144 | } 1145 | /** 1146 | * PK结束 1147 | * {"cmd":"PK_END","pk_id":3291,"pk_status":400,"data":{"init_id":273022,"match_id":52320,"punish_topic":"惩罚:唱《九妹》"},"_roomid":273022} 1148 | * 1149 | * @interface PK_END 1150 | * @extends {PK_MIC_Base} 1151 | */ 1152 | interface PK_END extends PK_MIC_Base { 1153 | /** PK结束 */ 1154 | data: PK_END_Data 1155 | } 1156 | interface PK_END_Data { 1157 | /** 发起人房间号 */ 1158 | init_id: number 1159 | /** 匹配人房间号 */ 1160 | match_id: number 1161 | /** 惩罚 */ 1162 | punish_topic: string 1163 | } 1164 | /** 1165 | * PK结束数据 1166 | * {"cmd":"PK_SETTLE","pk_id":3291,"pk_status":400,"data":{"pk_id":3291,"init_info":{"uid":28008980,"init_id":273022,"uname":"崛起而零距离的目标和梦想和理想之","face":"http://i0.hdslb.com/bfs/face/2c3a364cf409a85b4c651a6afbf6ffe22208c654.jpg","votes":0,"is_winner":false},"match_info":{"uid":922127,"match_id":52320,"uname":"栗子蛋糕酱","face":"http://i0.hdslb.com/bfs/face/6fb781f75b9c30d2d8b384793fcd02ad3238b1bd.jpg","votes":1,"is_winner":true,"vip_type":2,"exp":{"color":5805790,"user_level":35,"master_level":{"level":7,"color":6406234}},"vip":{"vip":0,"svip":0},"face_frame":"","badge":{"url":"http://i0.hdslb.com/bfs/live/b5e9ebd5ddb979a482421ca4ea2f8c1cc593370b.png","desc":"","position":3}},"best_user":{"uid":220870717,"uname":"陶渊明呼呼","face":"http://i1.hdslb.com/bfs/face/dfa72087e929665d3542778144bad0b7f0406998.jpg","vip_type":2,"exp":{"color":6406234,"user_level":14,"master_level":{"level":1,"color":6406234}},"vip":{"vip":1,"svip":0},"privilege_type":0,"face_frame":"","badge":{"url":"http://i0.hdslb.com/bfs/live/b5e9ebd5ddb979a482421ca4ea2f8c1cc593370b.png","desc":"","position":3}},"punish_topic":"惩罚:唱《九妹》"},"_roomid":273022} 1167 | * 1168 | * @interface PK_SETTLE 1169 | * @extends {PK_MIC_Base} 1170 | */ 1171 | interface PK_SETTLE extends PK_MIC_Base { 1172 | /** PK结束数据 */ 1173 | data: PK_SETTLE_Data 1174 | } 1175 | interface PK_SETTLE_Data { 1176 | /** PK编号 */ 1177 | pk_id: number 1178 | /** 发起人信息 */ 1179 | init_info: PK_SETTLE_Data_InitInfo 1180 | /** 匹配人信息 */ 1181 | match_info: PK_SETTLE_Data_MatchInfo 1182 | /** 最佳助攻 */ 1183 | best_user: PK_SETTLE_Data_BestUser 1184 | /** 惩罚 */ 1185 | punish_topic: string 1186 | } 1187 | interface PK_SETTLE_Data_UserInfoBase { 1188 | /** 用户UID */ 1189 | uid: number 1190 | /** 用户昵称 */ 1191 | uname: string 1192 | /** 用户头像 */ 1193 | face: string 1194 | } 1195 | interface PK_SETTLE_Data_UserInfo extends PK_SETTLE_Data_UserInfoBase { 1196 | /** 得票数 */ 1197 | votes: number 1198 | /** 是否胜利 */ 1199 | is_winner: boolean 1200 | } 1201 | interface PK_SETTLE_Data_UserInfoEx { 1202 | /** VIP类型 */ 1203 | vip_type: number 1204 | /** 用户经验 */ 1205 | exp: PK_SETTLE_Data_UserInfoEx_Exp 1206 | /** 用户VIP */ 1207 | vip: PK_SETTLE_Data_UserInfoEx_Vip 1208 | /** 头像边框地址 */ 1209 | face_frame: string 1210 | /** 徽章 */ 1211 | badge: PK_SETTLE_Data_UserInfoEx_Badge 1212 | } 1213 | interface PK_SETTLE_Data_UserInfoEx_Badge { 1214 | /** 徽章图片地址 */ 1215 | url: string 1216 | /** 描述 */ 1217 | desc: string 1218 | /** 位置 */ 1219 | position: number 1220 | } 1221 | interface PK_SETTLE_Data_UserInfoEx_Vip { 1222 | /** 普通VIP */ 1223 | vip: number 1224 | /** 超级VIP */ 1225 | svip: number 1226 | } 1227 | interface PK_SETTLE_Data_UserInfoEx_Exp { 1228 | /** 等级颜色 */ 1229 | color: number 1230 | /** 用户等级 */ 1231 | user_level: number 1232 | /** 直播等级 */ 1233 | master_level: PK_SETTLE_Data_UserInfoEx_Exp_Master 1234 | } 1235 | interface PK_SETTLE_Data_UserInfoEx_Exp_Master { 1236 | /** 直播等级 */ 1237 | level: number 1238 | /** 直播等级颜色 */ 1239 | color: number 1240 | } 1241 | interface PK_SETTLE_Data_InitInfo extends PK_SETTLE_Data_UserInfo { 1242 | /** 发起人房间号 */ 1243 | init_id: number 1244 | } 1245 | interface PK_SETTLE_Data_MatchInfo extends PK_SETTLE_Data_UserInfo, PK_SETTLE_Data_UserInfoEx { 1246 | /** 匹配人房间号 */ 1247 | match_id: number 1248 | } 1249 | interface PK_SETTLE_Data_BestUser extends PK_SETTLE_Data_UserInfoBase, PK_SETTLE_Data_UserInfoEx { } 1250 | /** 1251 | * 再次PK 1252 | * {"pk_status":400,"pk_id":8110,"cmd":"PK_CLICK_AGAIN","roomid":882855,"_roomid":882855} 1253 | * 1254 | * @interface PK_CLICK_AGAIN 1255 | * @extends {PK_MIC_Base} 1256 | */ 1257 | interface PK_CLICK_AGAIN extends PK_MIC_Base { } 1258 | /** 1259 | * 再次PK匹配 1260 | * {"cmd":"PK_AGAIN","pk_id":8159,"pk_status":400,"data":{"new_pk_id":8179,"init_id":13566,"match_id":7326390,"escape_time":5,"is_portrait":true,"uname":"宇天学长","face":"http://i2.hdslb.com/bfs/face/488dda4a85251f9d0fd9ad82a733f874b5cec585.jpg","uid":261738266},"roomid":13566,"_roomid":13566} 1261 | * 1262 | * @interface PK_AGAIN 1263 | * @extends {PK_MIC_Base} 1264 | */ 1265 | interface PK_AGAIN extends PK_MIC_Base { 1266 | data: PK_AGAIN_Data; 1267 | } 1268 | interface PK_AGAIN_Data extends PK_MATCH_Data { 1269 | /** 新PK编号 */ 1270 | new_pk_id: number 1271 | } 1272 | /** 1273 | * 连麦PK结束 1274 | * {"cmd":"PK_MIC_END","pk_id":3291,"pk_status":1000,"data":{"type":0},"_roomid":273022} 1275 | * {"cmd":"PK_MIC_END","pk_id":7803,"pk_status":1100,"data":{"type":0,"exception_id":1585470},"_roomid":11303074} 1276 | * {"cmd":"PK_MIC_END","pk_id":7810,"pk_status":1200,"data":{"type":0,"exception_id":353549},"_roomid":353549} 1277 | * 1278 | * @interface PK_MIC_END 1279 | * @extends {PK_MIC_Base} 1280 | */ 1281 | interface PK_MIC_END extends PK_MIC_Base { 1282 | /** 连麦PK结束 */ 1283 | data: PK_MIC_END_Data 1284 | } 1285 | interface PK_MIC_END_Data { 1286 | /** 结束类型 */ 1287 | type: number 1288 | /** 异常?编号 */ 1289 | exception_id?: number 1290 | } 1291 | /** 1292 | * 大乱斗抽奖 1293 | * {"cmd":"PK_LOTTERY_START","data":{"asset_animation_pic":"https://i0.hdslb.com/bfs/live/e1ab9f88b4af63fbf15197acea2dbb60bfc4434b.gif","asset_icon":"https://i0.hdslb.com/bfs/vc/44c367b09a8271afa22853785849e65797e085a1.png","id":332787,"max_time":120,"pk_id":332787,"room_id":4870575,"time":120,"title":"恭喜主播大乱斗胜利"},"_roomid":4870575} 1294 | * 1295 | * @interface PK_LOTTERY_START 1296 | * @extends {danmuJson} 1297 | */ 1298 | interface PK_LOTTERY_START extends danmuJson { 1299 | data: PK_LOTTERY_START_Data 1300 | } 1301 | interface PK_LOTTERY_START_Data { 1302 | asset_animation_pic: string 1303 | asset_icon: string 1304 | id: number 1305 | max_time: number 1306 | pk_id: number 1307 | room_id: number 1308 | time: number 1309 | title: string 1310 | } 1311 | /** 1312 | * 用户头衔(存疑) 1313 | * {"cmd":"USER_TITLE_GET","data":{"title_id":"may-pillow","source":"2016 五月病","name":"被窝","description":"赠送 25 个被窝","colorful":0,"create_time":"2018-10-31 20:17:15","expire_time":"永久","url":"/may","mobile_pic_url":"http://s1.hdslb.com/bfs/static/blive/live-assets/mobile/titles/title/3/may-pillow.png?20180726173300","web_pic_url":"http://s1.hdslb.com/bfs/static/blive/live-assets/mobile/titles/title/3/may-pillow.png?20180726173300","num":1,"score":0,"level":1},"uid":301606770,"_roomid":9950825} 1314 | * {"cmd":"USER_TITLE_GET","data":{"title_id":"title-174-1","source":"2018 BLS年终盛典 ","name":"幻影","description":"通过普通扭蛋机有几率获得 ","colorful":0,"create_time":"2018-10-31 20:19:26","expire_time":"永久","url":"http://live.bilibili.com/blackboard/bls-2018-web.html","mobile_pic_url":"http://s1.hdslb.com/bfs/vc/a61f2913f8a86b03ef432a286fd5e9e3e22e17bd.png?20180726173300","web_pic_url":"http://s1.hdslb.com/bfs/vc/a61f2913f8a86b03ef432a286fd5e9e3e22e17bd.png?20180726173300","num":1,"score":0,"level":1},"uid":66822870,"_roomid":2776645} 1315 | * 1316 | * @interface USER_TITLE_GET 1317 | * @extends {danmuJson} 1318 | */ 1319 | interface USER_TITLE_GET extends danmuJson { 1320 | data: USER_TITLE_GET_Data 1321 | uid: number 1322 | } 1323 | interface USER_TITLE_GET_Data { 1324 | title_id: string 1325 | source: string 1326 | name: string 1327 | description: string 1328 | colorful: number 1329 | create_time: string 1330 | expire_time: string 1331 | url: string 1332 | mobile_pic_url: string 1333 | web_pic_url: string 1334 | num: number 1335 | score: number 1336 | level: number 1337 | } 1338 | /** 1339 | * 实物宝箱 1340 | * {"cmd":"BOX_ACTIVITY_START", "aid": 323,"_roomid":9950825} 1341 | * 1342 | * @interface BOX_ACTIVITY_START 1343 | * @extends {danmuJson} 1344 | */ 1345 | interface BOX_ACTIVITY_START extends danmuJson { 1346 | aid: number 1347 | } 1348 | /** 1349 | * 画板活动 1350 | * 1351 | * @interface DRAW_UPDATE 1352 | * @extends {danmuJson} 1353 | */ 1354 | interface DRAW_UPDATE extends danmuJson { 1355 | /** 1356 | * 个人用户一像素 x_min === x_max y_min === y_max 1357 | * 管理员可用笔刷 1358 | */ 1359 | data: DRAW_UPDATE_data 1360 | } 1361 | interface DRAW_UPDATE_data { 1362 | /** x起点坐标 */ 1363 | x_min: number 1364 | /** x终点坐标 */ 1365 | x_max: number 1366 | /** y起点坐标 */ 1367 | y_min: number 1368 | /** y终点坐标 */ 1369 | y_max: number 1370 | /** 颜色代码[0-9A-V] */ 1371 | color: string 1372 | } 1373 | -------------------------------------------------------------------------------- /bilive/danmuLog.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import util from 'util' 3 | import { EventEmitter } from 'events' 4 | const FSwriteFile = util.promisify(fs.writeFile) 5 | 6 | /** 7 | * 8 | * @class DanmuLib 9 | * @extends EventEmitter 10 | */ 11 | class DanmuLib extends EventEmitter { 12 | constructor() { 13 | super() 14 | this._danmuLibPath = __dirname + '/../../options/danmuLib.json' 15 | const hasFile = fs.existsSync(this._danmuLibPath) 16 | if (hasFile) { 17 | const danmuLibBuffer = fs.readFileSync(this._danmuLibPath) 18 | const danmuLib = JSON.parse(danmuLibBuffer.toString()) 19 | if (danmuLib === undefined) throw new TypeError('文件格式化失败') 20 | this._ = danmuLib 21 | } 22 | } 23 | private _danmuLibPath: string 24 | public _: danmuLib = {} 25 | public async add(item: any) { 26 | const cmd = item['cmd'] 27 | if (this._[cmd] === undefined) { 28 | this._[cmd] = item 29 | await this.save() 30 | } 31 | } 32 | public async save() { 33 | const error = await FSwriteFile(this._danmuLibPath, JSON.stringify(this._, null, 2)) 34 | if (error !== undefined) console.error(`${new Date().toString().slice(4, 24)} :`, error) 35 | return this._ 36 | } 37 | } 38 | 39 | interface danmuLib { 40 | [index: string]: any 41 | } 42 | 43 | export default new DanmuLib() 44 | -------------------------------------------------------------------------------- /bilive/db.ts: -------------------------------------------------------------------------------- 1 | import nedb from 'nedb' 2 | 3 | class myDB { 4 | constructor(path: string) { 5 | this._path = path 6 | } 7 | private __db!: nedb 8 | protected _path: string 9 | /** 10 | * 加载 11 | * 12 | * @returns {(Promise)} 13 | * @memberof newdb 14 | */ 15 | public load(): Promise { 16 | return new Promise(resolve => { 17 | this.__db = new nedb({ 18 | filename: this._path, autoload: true, onload: err => { 19 | if (err === null) this.__db.persistence.setAutocompactionInterval(60 * 60 * 1000) 20 | resolve(err) 21 | } 22 | }) 23 | }) 24 | } 25 | /** 26 | * 查找 27 | * 28 | * @template T 29 | * @param {*} query 30 | * @returns {(Promise)} 31 | * @memberof newdb 32 | */ 33 | public find(query: any): Promise { 34 | return new Promise(resolve => { 35 | this.__db.find(query, (err: Error | null, documents: T[]) => err === null ? resolve(documents) : resolve(err)) 36 | }) 37 | } 38 | /** 39 | * 查找一个 40 | * 41 | * @template T 42 | * @param {*} query 43 | * @returns {(Promise)} 44 | * @memberof newdb 45 | */ 46 | public findOne(query: any): Promise { 47 | return new Promise(resolve => { 48 | this.__db.findOne(query, (err: Error | null, document: T) => err === null ? resolve(document) : resolve(err)) 49 | }) 50 | } 51 | /** 52 | * 更新 53 | * 54 | * @param {*} query 55 | * @param {*} updateQuery 56 | * @param {Nedb.UpdateOptions} [options] 57 | * @returns {(Promise)} 58 | * @memberof newdb 59 | */ 60 | public update(query: any, updateQuery: any, options?: Nedb.UpdateOptions): Promise { 61 | return new Promise(resolve => { 62 | this.__db.update(query, updateQuery, options, (err: Error | null) => resolve(err)) 63 | }) 64 | } 65 | } 66 | 67 | const dbPath = __dirname + '/../../options' 68 | const db = { roomList: new myDB(dbPath + '/roomList.db') } 69 | 70 | export default db 71 | -------------------------------------------------------------------------------- /bilive/dm_client_re.ts: -------------------------------------------------------------------------------- 1 | import tools from './lib/tools' 2 | import DMclient from './lib/dm_client' 3 | /** 4 | * 弹幕客户端, 可自动重连 5 | * 因为之前重连逻辑写在一起实在太乱了, 所以独立出来 6 | * 7 | * @class DMclientRE 8 | * @extends {DMclient} 9 | */ 10 | class DMclientRE extends DMclient { 11 | /** 12 | * Creates an instance of DMclientRE. 13 | * @param {DMclientOptions} [{ roomID = 23058, userID = 0, protocol = 'flash' }={}] 14 | * @memberof DMclientRE 15 | */ 16 | constructor({ roomID = 23058, userID = 0, protocol = 'flash' }: DMclientOptions = {}) { 17 | super({ roomID, userID, protocol }) 18 | this.on('DMerror', error => tools.ErrorLog(error)) 19 | this.on('close', () => this._ClientReConnect()) 20 | } 21 | /** 22 | * 重连次数, 以五次为阈值 23 | * 24 | * @type {number} 25 | * @memberof DMclientRE 26 | */ 27 | public reConnectTime: number = 0 28 | /** 29 | * 重新连接 30 | * 31 | * @private 32 | * @memberof DMclientRE 33 | */ 34 | private _ClientReConnect() { 35 | this._Timer = setTimeout(() => { 36 | if (this.reConnectTime >= 5) { 37 | this.reConnectTime = 0 38 | this._DelayReConnect() 39 | } 40 | else { 41 | this.reConnectTime++ 42 | this.Connect({ server: this._server, port: this.port }) 43 | } 44 | }, 3 * 1000) 45 | } 46 | /** 47 | * 5分钟后重新连接 48 | * 49 | * @private 50 | * @memberof DMclientRE 51 | */ 52 | private _DelayReConnect() { 53 | this._Timer = setTimeout(() => 54 | this.Connect({ server: this._server, port: this.port }), 55 | 2 * 60 * 1000) 56 | tools.ErrorLog('重连弹幕服务器失败,两分钟后继续尝试') 57 | } 58 | } 59 | export default DMclientRE -------------------------------------------------------------------------------- /bilive/index.ts: -------------------------------------------------------------------------------- 1 | import WSServer from './wsserver' 2 | import Listener from './listener' 3 | import Options from './options' 4 | 5 | /** 6 | * 主程序 7 | * 8 | * @export 9 | * @class BiLive 10 | */ 11 | class BiLive { 12 | constructor() {} 13 | private _Listener!: Listener 14 | private _WSServer!: WSServer 15 | // 全局计时器 16 | private _lastTime = '' 17 | public loop!: NodeJS.Timer 18 | /** 19 | * 开始主程序 20 | * 21 | * @memberof BiLive 22 | */ 23 | public async Start() { 24 | // 开启监听 25 | this._WSServer = new WSServer() 26 | this._WSServer.Start() 27 | this.Listener() 28 | this.loop = setInterval(() => this._loop(), 55 * 1000) 29 | } 30 | /** 31 | * 计时器 32 | * 33 | * @private 34 | * @memberof BiLive 35 | */ 36 | private _loop() { 37 | const csttime = Date.now() + 8 * 60 * 60 * 1000 38 | const cst = new Date(csttime) 39 | const cstString = cst.toUTCString().substr(17, 5) // 'HH:mm' 40 | if (cstString === this._lastTime) return 41 | this._lastTime = cstString 42 | const cstHour = cst.getUTCHours() 43 | const cstMin = cst.getUTCMinutes() 44 | if (cstMin === 0) Options.save() 45 | if (cstMin === 59) this._Listener.logAllID(cstHour + 1) 46 | if (cstString === '00:00') { 47 | this._Listener.clearAllID() 48 | this._Listener._MSGCache.clear() 49 | } 50 | } 51 | /** 52 | * 监听系统消息 53 | * 54 | * @memberof BiLive 55 | */ 56 | public Listener() { 57 | this._Listener = new Listener() 58 | this._Listener 59 | .on('raffle', raffleMessage => this._WSServer.Raffle(raffleMessage)) 60 | .on('lottery', lotteryMessage => this._WSServer.Lottery(lotteryMessage)) 61 | .on('pklottery', lotteryMessage => this._WSServer.PKLottery(lotteryMessage)) 62 | .on('beatStorm', beatStormMessage => this._WSServer.BeatStorm(beatStormMessage)) 63 | .Start() 64 | } 65 | } 66 | 67 | export default BiLive -------------------------------------------------------------------------------- /bilive/lib/app_client.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto' 2 | import request from 'request' 3 | import tools from './tools' 4 | /** 5 | * 登录状态 6 | * 7 | * @enum {number} 8 | */ 9 | enum appStatus { 10 | 'success', 11 | 'captcha', 12 | 'error', 13 | 'httpError', 14 | } 15 | /** 16 | * Creates an instance of AppClient. 17 | * 18 | * @class AppClient 19 | */ 20 | class AppClient { 21 | /** 22 | * Creates an instance of AppClient. 23 | * @memberof AppClient 24 | */ 25 | constructor() { 26 | // 设置 Buvid 27 | this.headers['Buvid'] = AppClient.RandomID(37).toUpperCase() 28 | // 设置 Display-ID 29 | this.headers['Display-ID'] = `${this.headers['Buvid']}-${AppClient.TS}` 30 | // 设置 Device-ID 31 | this.headers['Device-ID'] = AppClient.RandomID(54) 32 | } 33 | public static readonly actionKey: string = 'appkey' 34 | public static readonly device: string = 'android' 35 | // bilibili 客户端 36 | private static readonly __secretKey: string = '560c52ccd288fed045859ed18bffd973' 37 | public static readonly appKey: string = '1d8b6e7d45233436' 38 | public static readonly build: string = '5431000' 39 | public static readonly mobiApp: string = 'android' 40 | public static readonly platform: string = 'android' 41 | // bilibili 国际版 42 | // private static readonly __secretKey: string = '36efcfed79309338ced0380abd824ac1' 43 | // public static readonly appKey: string = 'bb3101000e232e27' 44 | // public static readonly build: string = '112000' 45 | // public static readonly mobiApp: string = 'android_i' 46 | // bilibili 概念版 47 | // private static readonly __secretKey: string = '25bdede4e1581c836cab73a48790ca6e' 48 | // public static readonly appKey: string = '07da50c9a0bf829f' 49 | // public static readonly build: string = '591204' 50 | // public static readonly mobiApp: string = 'android_b' 51 | // bilibili TV 52 | // private static readonly __secretKey: string = '59b43e04ad6965f34319062b478f83dd' 53 | // public static readonly appKey: string = '4409e2ce8ffd12b8' 54 | // public static readonly build: string = '140600' 55 | // public static readonly mobiApp: string = 'android_tv' 56 | // bilibili link 57 | // private static readonly __secretKey: string = 'e988e794d4d4b6dd43bc0e89d6e90c43' 58 | // public static readonly appKey: string = '37207f2beaebf8d7' 59 | // public static readonly build: string = '3900007' 60 | // public static readonly mobiApp: string = 'biliLink' 61 | // public static readonly platform: string = 'android_link' 62 | /** 63 | * 谜一样的TS 64 | * 65 | * @readonly 66 | * @static 67 | * @type {number} 68 | * @memberof AppClient 69 | */ 70 | public static get TS(): number { 71 | return Math.floor(Date.now() / 1000) 72 | } 73 | /** 74 | * 谜一样的RND 75 | * 76 | * @readonly 77 | * @static 78 | * @type {number} 79 | * @memberof AppClient 80 | */ 81 | public static get RND(): number { 82 | return Math.floor(Math.random() * 1e+8) + 1e+7 83 | } 84 | /** 85 | * 谜一样的RandomID 86 | * 87 | * @static 88 | * @param {number} length 89 | * @returns {string} 90 | * @memberof AppClient 91 | */ 92 | public static RandomID(length: number): string { 93 | const words = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' 94 | let randomID = '' 95 | for (let i = 0; i < length; i++) randomID += words[Math.floor(Math.random() * 62)] 96 | return randomID 97 | } 98 | /** 99 | * 基本请求参数 100 | * 101 | * @readonly 102 | * @static 103 | * @type {string} 104 | * @memberof AppClient 105 | */ 106 | public static get baseQuery(): string { 107 | return `actionKey=${this.actionKey}&appkey=${this.appKey}&build=${this.build}\ 108 | &device=${this.device}&mobi_app=${this.mobiApp}&platform=${this.platform}` 109 | } 110 | /** 111 | * 对参数签名 112 | * 113 | * @static 114 | * @param {string} params 115 | * @param {boolean} [ts=true] 116 | * @returns {string} 117 | * @memberof AppClient 118 | */ 119 | public static signQuery(params: string, ts = true): string { 120 | let paramsSort = params 121 | if (ts) paramsSort = `${params}&ts=${this.TS}` 122 | paramsSort = paramsSort.split('&').sort().join('&') 123 | const paramsSecret = paramsSort + this.__secretKey 124 | const paramsHash = tools.Hash('md5', paramsSecret) 125 | return `${paramsSort}&sign=${paramsHash}` 126 | } 127 | /** 128 | * 对参数加参后签名 129 | * 130 | * @static 131 | * @param {string} [params] 132 | * @returns {string} 133 | * @memberof AppClient 134 | */ 135 | public static signQueryBase(params?: string): string { 136 | const paramsBase = params === undefined ? this.baseQuery : `${params}&${this.baseQuery}` 137 | return this.signQuery(paramsBase) 138 | } 139 | /** 140 | * 登录状态 141 | * 142 | * @static 143 | * @type {typeof appStatus} 144 | * @memberof AppClient 145 | */ 146 | public static readonly status: typeof appStatus = appStatus 147 | /** 148 | * 验证码, 登录时会自动清空 149 | * 150 | * @type {string} 151 | * @memberof AppClient 152 | */ 153 | public captcha: string = '' 154 | /** 155 | * 用户名, 推荐邮箱或电话号 156 | * 157 | * @abstract 158 | * @type {string} 159 | * @memberof AppClient 160 | */ 161 | public userName!: string 162 | /** 163 | * 密码 164 | * 165 | * @abstract 166 | * @type {string} 167 | * @memberof AppClient 168 | */ 169 | public passWord!: string 170 | /** 171 | * 登录后获取的B站UID 172 | * 173 | * @abstract 174 | * @type {number} 175 | * @memberof AppClient 176 | */ 177 | public biliUID!: number 178 | /** 179 | * 登录后获取的access_token 180 | * 181 | * @abstract 182 | * @type {string} 183 | * @memberof AppClient 184 | */ 185 | public accessToken!: string 186 | /** 187 | * 登录后获取的refresh_token 188 | * 189 | * @abstract 190 | * @type {string} 191 | * @memberof AppClient 192 | */ 193 | public refreshToken!: string 194 | /** 195 | * 登录后获取的cookieString 196 | * 197 | * @abstract 198 | * @type {string} 199 | * @memberof AppClient 200 | */ 201 | public cookieString!: string 202 | /** 203 | * 请求头 204 | * 205 | * @type {request.Headers} 206 | * @memberof AppClient 207 | */ 208 | public headers: request.Headers = { 209 | 'User-Agent': 'Mozilla/5.0 BiliDroid/5.43.1 (bbcallen@gmail.com)', 210 | 'Connection': 'Keep-Alive', 211 | } 212 | /** 213 | * cookieJar 214 | * 215 | * @private 216 | * @type {request.CookieJar} 217 | * @memberof AppClient 218 | */ 219 | private __jar: request.CookieJar = request.jar() 220 | /** 221 | * 对密码进行加密 222 | * 223 | * @protected 224 | * @param {getKeyResponseData} publicKey 225 | * @returns {string} 226 | * @memberof AppClient 227 | */ 228 | protected _RSAPassWord(publicKey: getKeyResponseData): string { 229 | const padding = { 230 | key: publicKey.key, 231 | // @ts-ignore d.ts错误 232 | padding: crypto.constants.RSA_PKCS1_PADDING 233 | } 234 | const hashPassWord = publicKey.hash + this.passWord 235 | const encryptPassWord = crypto.publicEncrypt(padding, Buffer.from(hashPassWord)).toString('base64') 236 | return encodeURIComponent(encryptPassWord) 237 | } 238 | /** 239 | * 获取公钥 240 | * 241 | * @protected 242 | * @returns {(Promise | undefined>)} 243 | * @memberof AppClient 244 | */ 245 | protected _getKey(): Promise | undefined> { 246 | const getKey: request.Options = { 247 | method: 'POST', 248 | uri: 'https://passport.bilibili.com/api/oauth2/getKey', 249 | body: AppClient.signQueryBase(), 250 | jar: this.__jar, 251 | json: true, 252 | headers: this.headers 253 | } 254 | return tools.XHR(getKey, 'Android') 255 | } 256 | /** 257 | * 验证登录信息 258 | * 259 | * @protected 260 | * @param {getKeyResponseData} publicKey 261 | * @returns {Promise | undefined>)} 262 | * @memberof AppClient 263 | */ 264 | protected _auth(publicKey: getKeyResponseData): Promise | undefined> { 265 | const passWord = this._RSAPassWord(publicKey) 266 | const captcha = this.captcha === '' ? '' : `&captcha=${this.captcha}` 267 | const authQuery = `username=${encodeURIComponent(this.userName)}&password=${passWord}${captcha}` 268 | const auth: request.Options = { 269 | method: 'POST', 270 | uri: 'https://passport.bilibili.com/api/v2/oauth2/login', 271 | body: AppClient.signQueryBase(authQuery), 272 | jar: this.__jar, 273 | json: true, 274 | headers: this.headers 275 | } 276 | this.captcha = '' 277 | return tools.XHR(auth, 'Android') 278 | } 279 | /** 280 | * 更新用户凭证 281 | * 282 | * @protected 283 | * @param {authResponseData} authResponseData 284 | * @memberof AppClient 285 | */ 286 | protected _update(authResponseData: authResponseData) { 287 | const tokenInfo = authResponseData.token_info 288 | const cookies = authResponseData.cookie_info.cookies 289 | this.biliUID = +tokenInfo.mid 290 | this.accessToken = tokenInfo.access_token 291 | this.refreshToken = tokenInfo.refresh_token 292 | this.cookieString = cookies.reduce((cookieString, cookie) => cookieString === '' 293 | ? `${cookie.name}=${cookie.value}` 294 | : `${cookieString}; ${cookie.name}=${cookie.value}` 295 | , '') 296 | } 297 | /** 298 | * 获取验证码 299 | * 300 | * @returns {Promise} 301 | * @memberof AppClient 302 | */ 303 | public async getCaptcha(): Promise { 304 | const captcha: request.Options = { 305 | uri: 'https://passport.bilibili.com/captcha', 306 | encoding: null, 307 | jar: this.__jar, 308 | headers: this.headers 309 | } 310 | const captchaResponse = await tools.XHR(captcha, 'Android') 311 | if (captchaResponse !== undefined && captchaResponse.response.statusCode === 200) 312 | return { status: appStatus.success, data: captchaResponse.body, } 313 | return { status: appStatus.error, data: captchaResponse } 314 | } 315 | /** 316 | * 客户端登录 317 | * 318 | * @returns {Promise} 319 | * @memberof AppClient 320 | */ 321 | public async login(): Promise { 322 | const getKeyResponse = await this._getKey() 323 | if (getKeyResponse !== undefined && getKeyResponse.response.statusCode === 200 && getKeyResponse.body.code === 0) { 324 | const authResponse = await this._auth(getKeyResponse.body.data) 325 | if (authResponse !== undefined && authResponse.response.statusCode === 200) { 326 | if (authResponse.body.code === 0) { 327 | this._update(authResponse.body.data) 328 | return { status: appStatus.success, data: authResponse.body } 329 | } 330 | if (authResponse.body.code === -105) return { status: appStatus.captcha, data: authResponse.body } 331 | return { status: appStatus.error, data: authResponse.body } 332 | } 333 | return { status: appStatus.httpError, data: authResponse } 334 | } 335 | return { status: appStatus.httpError, data: getKeyResponse } 336 | } 337 | /** 338 | * 客户端登出 339 | * 340 | * @returns {Promise} 341 | * @memberof AppClient 342 | */ 343 | public async logout(): Promise { 344 | const revokeQuery = `${this.cookieString.replace(/; */g, '&')}&access_token=${this.accessToken}` 345 | const revoke: request.Options = { 346 | method: 'POST', 347 | uri: 'https://passport.bilibili.com/api/v2/oauth2/revoke', 348 | body: AppClient.signQueryBase(revokeQuery), 349 | json: true, 350 | headers: this.headers 351 | } 352 | const revokeResponse = await tools.XHR(revoke, 'Android') 353 | if (revokeResponse !== undefined && revokeResponse.response.statusCode === 200) { 354 | if (revokeResponse.body.code === 0) return { status: appStatus.success, data: revokeResponse.body } 355 | return { status: appStatus.error, data: revokeResponse.body } 356 | } 357 | return { status: appStatus.httpError, data: revokeResponse } 358 | } 359 | /** 360 | * 更新access_token 361 | * 362 | * @returns {Promise} 363 | * @memberof AppClient 364 | */ 365 | public async refresh(): Promise { 366 | const refreshQuery = `access_token=${this.accessToken}&refresh_token=${this.refreshToken}` 367 | const refresh: request.Options = { 368 | method: 'POST', 369 | uri: 'https://passport.bilibili.com/api/v2/oauth2/refresh_token', 370 | body: AppClient.signQueryBase(refreshQuery), 371 | json: true, 372 | headers: this.headers 373 | } 374 | const refreshResponse = await tools.XHR(refresh, 'Android') 375 | if (refreshResponse !== undefined && refreshResponse.response.statusCode === 200) { 376 | if (refreshResponse.body !== undefined && refreshResponse.body.code === 0) { 377 | this._update(refreshResponse.body.data) 378 | return { status: appStatus.success, data: refreshResponse.body } 379 | } 380 | return { status: appStatus.error, data: refreshResponse.body } 381 | } 382 | return { status: appStatus.httpError, data: refreshResponse } 383 | } 384 | } 385 | export default AppClient -------------------------------------------------------------------------------- /bilive/lib/dm_client.ts: -------------------------------------------------------------------------------- 1 | import { Socket } from 'net' 2 | import { inflate } from 'zlib' 3 | import { EventEmitter } from 'events' 4 | import ws from 'ws' 5 | import tools from './tools' 6 | import AppClient from './app_client' 7 | /** 8 | * 错误类型 9 | * 10 | * @enum {number} 11 | */ 12 | enum dmErrorStatus { 13 | 'client', 14 | 'danmaku', 15 | 'timeout', 16 | } 17 | /** 18 | * 弹幕客户端, 用于连接弹幕服务器和发送弹幕事件 19 | * 20 | * @class DMclient 21 | * @extends {EventEmitter} 22 | */ 23 | class DMclient extends EventEmitter { 24 | /** 25 | * Creates an instance of DMclient. 26 | * @param {Options} [{ roomID = 23058, userID = 0, protocol = 'socket' }={}] 27 | * @memberof DMclient 28 | */ 29 | constructor({ roomID = 23058, userID = 0, protocol = 'socket' }: DMclientOptions = {}) { 30 | super() 31 | this.roomID = roomID 32 | this.userID = userID 33 | this._protocol = protocol 34 | } 35 | /** 36 | * 用户UID 37 | * 38 | * @type {number} 39 | * @memberof DMclient 40 | */ 41 | public userID: number 42 | /** 43 | * 房间号, 注意不要短号 44 | * 45 | * @type {number} 46 | * @memberof DMclient 47 | */ 48 | public roomID: number 49 | /** 50 | * 连接使用的token, 暂时不知道功能 51 | * 52 | * @type {string} 53 | * @memberof DMclient 54 | */ 55 | public key = '' 56 | /** 57 | * 连接弹幕服务器使用的协议 58 | * 为了避免不必要的麻烦, 禁止外部修改 59 | * 60 | * @protected 61 | * @type {DMclientProtocol} 62 | * @memberof DMclient 63 | */ 64 | protected _protocol: DMclientProtocol 65 | /** 66 | * 连接弹幕服务器使用的协议 67 | * 68 | * @readonly 69 | * @type {DMclientProtocol} 70 | * @memberof DMclient 71 | */ 72 | public get protocol(): DMclientProtocol { 73 | return this._protocol 74 | } 75 | /** 76 | * 当前连接的弹幕服务器 77 | * 为了避免不必要的麻烦, 禁止外部修改 78 | * 79 | * @protected 80 | * @type {string} 81 | * @memberof DMclient 82 | */ 83 | protected _server!: string 84 | /** 85 | * 当前连接的弹幕服务器 86 | * 87 | * @readonly 88 | * @type {string} 89 | * @memberof DMclient 90 | */ 91 | public get server(): string { 92 | return this._server 93 | } 94 | /** 95 | * 当前连接的弹幕服务器端口 96 | * 为了避免不必要的麻烦, 禁止外部修改 97 | * 98 | * @protected 99 | * @type {number} 100 | * @memberof DMclient 101 | */ 102 | protected _port!: number 103 | /** 104 | * 当前连接的弹幕服务器端口 105 | * 106 | * @readonly 107 | * @type {number} 108 | * @memberof DMclient 109 | */ 110 | public get port(): number { 111 | return this._port 112 | } 113 | /** 114 | * 是否已经连接到服务器 115 | * 为了避免不必要的麻烦, 禁止外部修改 116 | * 117 | * @protected 118 | * @type {boolean} 119 | * @memberof DMclient 120 | */ 121 | protected _connected: boolean = false 122 | /** 123 | * 是否已经连接到服务器 124 | * 125 | * @readonly 126 | * @type {boolean} 127 | * @memberof DMclient 128 | */ 129 | public get connected(): boolean { 130 | return this._connected 131 | } 132 | /** 133 | * 版本 134 | * 135 | * @type {number} 136 | * @memberof DMclient 137 | */ 138 | public version: number = this.driver 139 | /** 140 | * 猜测为客户端设备 141 | * 142 | * @readonly 143 | * @type {(0 | 1)} 144 | * @memberof DMclient 145 | */ 146 | public get driver(): 0 | 1 { 147 | return this._protocol === 'socket' ? 0 : 1 148 | } 149 | /** 150 | * 全局计时器, 负责除心跳超时的其他任务, 便于停止 151 | * 152 | * @protected 153 | * @type {NodeJS.Timer} 154 | * @memberof DMclient 155 | */ 156 | protected _Timer!: NodeJS.Timer 157 | /** 158 | * 心跳超时 159 | * 160 | * @protected 161 | * @type {NodeJS.Timer} 162 | * @memberof DMclient 163 | */ 164 | protected _timeout!: NodeJS.Timer 165 | /** 166 | * 模仿客户端与服务器进行通讯 167 | * 168 | * @protected 169 | * @type {(Socket | ws)} 170 | * @memberof DMclient 171 | */ 172 | protected _client!: Socket | ws 173 | /** 174 | * 缓存数据 175 | * 176 | * @private 177 | * @type {Buffer} 178 | * @memberof DMclient 179 | */ 180 | private __data!: Buffer 181 | /** 182 | * 错误类型 183 | * 184 | * @static 185 | * @type {typeof dmErrorStatus} 186 | * @memberof DMclient 187 | */ 188 | public static readonly errorStatus: typeof dmErrorStatus = dmErrorStatus 189 | /** 190 | * 连接到指定服务器 191 | * 192 | * @param {{ server: string, port: number }} [options] 193 | * @memberof DMclient 194 | */ 195 | public async Connect(options?: { server: string, port: number }) { 196 | if (this._connected) return 197 | this._connected = true 198 | if (options === undefined) { 199 | // 动态获取服务器地址, 防止B站临时更换 200 | const getDanmuInfo = { uri: `https://api.live.bilibili.com/xlive/app-room/v1/index/getDanmuInfo?${AppClient.signQueryBase(`room_id=${this.roomID}`)}` } 201 | const danmuInfo = await tools.XHR(getDanmuInfo) 202 | let socketServer = 'broadcastlv.chat.bilibili.com' 203 | let socketPort = 2243 204 | let wsServer = 'broadcastlv.chat.bilibili.com' 205 | let wsPort = 2244 206 | let wssPort = 443 207 | if (danmuInfo !== undefined && danmuInfo.response.statusCode === 200 && danmuInfo.body.code === 0) { 208 | socketServer = danmuInfo.body.data.ip_list[0].host 209 | socketPort = danmuInfo.body.data.ip_list[0].port 210 | wsServer = danmuInfo.body.data.host_list[0].host 211 | wsPort = danmuInfo.body.data.host_list[0].ws_port 212 | wssPort = danmuInfo.body.data.host_list[0].wss_port 213 | this.key = danmuInfo.body.data.token 214 | } 215 | if (this._protocol === 'socket' || this._protocol === 'flash') { 216 | this._server = socketServer 217 | this._port = socketPort 218 | } 219 | else { 220 | this._server = wsServer 221 | if (this._protocol === 'ws') this._port = wsPort 222 | if (this._protocol === 'wss') this._port = wssPort 223 | } 224 | } 225 | else { 226 | this._server = options.server 227 | this._port = options.port 228 | } 229 | this._ClientConnect() 230 | } 231 | /** 232 | * 断开与服务器的连接 233 | * 234 | * @memberof DMclient 235 | */ 236 | public Close() { 237 | if (!this._connected) return 238 | this._connected = false 239 | clearTimeout(this._Timer) 240 | clearTimeout(this._timeout) 241 | if (this._protocol === 'socket' || this._protocol === 'flash') { 242 | (this._client).end(); 243 | (this._client).destroy() 244 | } 245 | else { 246 | (this._client).close(); 247 | (this._client).terminate() 248 | } 249 | this._client.removeAllListeners() 250 | // 发送关闭消息 251 | this.emit('close') 252 | } 253 | /** 254 | * 客户端连接 255 | * 256 | * @protected 257 | * @memberof DMclient 258 | */ 259 | protected _ClientConnect() { 260 | if (this._protocol === 'socket' || this._protocol === 'flash') { 261 | this._client = new Socket().connect(this._port, this._server) 262 | .on('connect', () => this._ClientConnectHandler()) 263 | .on('data', data => this._ClientDataHandler(data)) 264 | .on('end', () => this.Close()) 265 | } 266 | else { 267 | this._client = new ws(`${this._protocol}://${this._server}:${this._port}/sub`) 268 | .on('open', () => this._ClientConnectHandler()) 269 | .on('message', (data: Buffer) => this._ClientDataHandler(data)) 270 | .on('close', () => this.Close()) 271 | } 272 | this._client.on('error', error => { 273 | const errorInfo: DMclientError = { status: dmErrorStatus.client, error: error } 274 | this._ClientErrorHandler(errorInfo) 275 | }) 276 | } 277 | /** 278 | * 客户端错误 279 | * 280 | * @protected 281 | * @param {DMerror} errorInfo 282 | * @memberof DMclient 283 | */ 284 | protected _ClientErrorHandler(errorInfo: DMerror) { 285 | // 'error' 为关键词, 为了避免麻烦不使用 286 | this.emit('DMerror', errorInfo) 287 | if (errorInfo.status !== DMclient.errorStatus.danmaku) this.Close() 288 | } 289 | /** 290 | * 向服务器发送自定义握手数据 291 | * 292 | * @protected 293 | * @memberof DMclient 294 | */ 295 | protected _ClientConnectHandler() { 296 | let data: string 297 | if (this._protocol === 'socket') 298 | data = JSON.stringify({ uid: this.userID, roomid: this.roomID, key: this.key, platform: 'android', clientver: '5.43.0.5430400', hwid: AppClient.RandomID(20), protover: 2 }) 299 | else if (this._protocol === 'flash') 300 | data = JSON.stringify({ key: this.key, clientver: '2.4.6-9e02b4f1', roomid: this.roomID, uid: this.userID, protover: 2, platform: 'flash' }) 301 | else data = JSON.stringify({ uid: this.userID, roomid: this.roomID, protover: 2, platform: 'web', clientver: '1.7.4', type: 2, key: this.key }) 302 | this._Timer = setTimeout(() => this._ClientHeart(), 30 * 1000) 303 | this._ClientSendData(16 + data.length, 16, this.version, 7, this.driver, data) 304 | } 305 | /** 306 | * 心跳包 307 | * 308 | * @protected 309 | * @memberof DMclient 310 | */ 311 | protected _ClientHeart() { 312 | if (!this._connected) return 313 | let data: string 314 | if (this._protocol === 'socket') data = '{}' 315 | else if (this._protocol === 'flash') data = '' 316 | else data = '[object Object]' 317 | this._timeout = setTimeout(() => { 318 | const errorInfo: DMclientError = { status: dmErrorStatus.timeout, error: new Error('心跳超时') } 319 | this._ClientErrorHandler(errorInfo) 320 | }, 10 * 1000) 321 | this._Timer = setTimeout(() => this._ClientHeart(), 30 * 1000) 322 | this._ClientSendData(16 + data.length, 16, this.version, 2, this.driver, data) 323 | } 324 | /** 325 | * 向服务器发送数据 326 | * 327 | * @protected 328 | * @param {number} totalLen 总长度 329 | * @param {number} [headLen=16] 头部长度 330 | * @param {number} [version=this.version] 版本 331 | * @param {number} [type=2] 类型 332 | * @param {number} [driver=this.driver] 设备 333 | * @param {string} [data] 数据 334 | * @memberof DMclient 335 | */ 336 | protected _ClientSendData(totalLen: number, headLen = 16 337 | , version = this.version, type = 2, driver = this.driver, data?: string) { 338 | const bufferData = Buffer.allocUnsafe(totalLen) 339 | bufferData.writeInt32BE(totalLen, 0) 340 | bufferData.writeInt16BE(headLen, 4) 341 | bufferData.writeInt16BE(version, 6) 342 | bufferData.writeInt32BE(type, 8) 343 | bufferData.writeInt32BE(driver, 12) 344 | if (data) bufferData.write(data, headLen) 345 | if (this._protocol === 'socket' || this._protocol === 'flash') (this._client).write(bufferData) 346 | else (this._client).send(bufferData) 347 | } 348 | /** 349 | * 解析从服务器接收的数据 350 | * 抛弃循环, 使用递归 351 | * 352 | * @protected 353 | * @param {Buffer} data 354 | * @memberof DMclient 355 | */ 356 | protected async _ClientDataHandler(data: Buffer) { 357 | // 拼接数据 358 | if (this.__data !== undefined) { 359 | // 把数据合并到缓存 360 | this.__data = Buffer.concat([this.__data, data]) 361 | const dataLen = this.__data.length 362 | const packageLen = this.__data.readInt32BE(0) 363 | if (dataLen >= packageLen) { 364 | data = this.__data 365 | delete this.__data 366 | } 367 | else return 368 | } 369 | // 读取数据 370 | const dataLen = data.length 371 | if (dataLen < 16 || dataLen > 0x100000) { 372 | // 抛弃长度过短和过长的数据 373 | const errorInfo: DMdanmakuError = { status: dmErrorStatus.danmaku, error: new TypeError('数据长度异常'), data } 374 | return this._ClientErrorHandler(errorInfo) 375 | } 376 | const packageLen = data.readInt32BE(0) 377 | if (packageLen < 16 || packageLen > 0x100000) { 378 | // 抛弃包长度异常的数据 379 | const errorInfo: DMdanmakuError = { status: dmErrorStatus.danmaku, error: new TypeError('包长度异常'), data } 380 | return this._ClientErrorHandler(errorInfo) 381 | } 382 | // 等待拼接数据 383 | if (dataLen < packageLen) return this.__data = data 384 | // 数据长度20时为在线人数 385 | if (dataLen > 20) { 386 | // const version = data.readInt16BE(6) 387 | // if (version === 2) { 388 | const compress = data.readInt16BE(16) 389 | if (compress === 0x78DA) { 390 | // 检查是否压缩, 目前来说压缩格式固定 391 | const uncompressData = await this._Uncompress(data.slice(16, packageLen)) 392 | if (uncompressData !== undefined) { 393 | this._ClientDataHandler(uncompressData) 394 | if (dataLen > packageLen) this._ClientDataHandler(data.slice(packageLen)) 395 | return 396 | } 397 | else { 398 | // 直接抛弃解压失败的数据 399 | const errorInfo: DMdanmakuError = { status: dmErrorStatus.danmaku, error: new TypeError('解压数据失败'), data } 400 | return this._ClientErrorHandler(errorInfo) 401 | } 402 | } 403 | } 404 | this._ParseClientData(data.slice(0, packageLen)) 405 | if (dataLen > packageLen) this._ClientDataHandler(data.slice(packageLen)) 406 | } 407 | /** 408 | * 解析消息 409 | * 410 | * @protected 411 | * @param {Buffer} data 412 | * @memberof DMclient 413 | */ 414 | protected async _ParseClientData(data: Buffer) { 415 | switch (data.readInt32BE(8)) { 416 | case 3: 417 | // 每次发送心跳包都会接收到此类消息, 所以用来判断是否超时 418 | clearTimeout(this._timeout) 419 | this.emit('online', data.readInt32BE(16)) 420 | break 421 | case 5: { 422 | const dataJson = await tools.JSONparse(data.toString('UTF-8', 16)) 423 | if (dataJson !== undefined) this._ClientData(dataJson) 424 | else { 425 | // 格式化消息失败则跳过 426 | const errorInfo: DMdanmakuError = { status: dmErrorStatus.danmaku, error: new TypeError('意外的弹幕信息'), data } 427 | this._ClientErrorHandler(errorInfo) 428 | } 429 | } 430 | break 431 | case 8: 432 | this.emit('connect') 433 | break 434 | default: { 435 | const errorInfo: DMdanmakuError = { status: dmErrorStatus.danmaku, error: new TypeError('未知的弹幕内容'), data } 436 | this._ClientErrorHandler(errorInfo) 437 | } 438 | break 439 | } 440 | } 441 | /** 442 | * 发送消息事件 443 | * 444 | * @protected 445 | * @param {danmuJson} dataJson 446 | * @memberof DMclient 447 | */ 448 | protected _ClientData(dataJson: danmuJson) { 449 | dataJson._roomid = this.roomID 450 | this.emit('ALL_MSG', dataJson) 451 | this.emit(dataJson.cmd, dataJson) 452 | } 453 | /** 454 | * 解压数据 455 | * 456 | * @protected 457 | * @param {Buffer} data 458 | * @returns {Promise} 459 | * @memberof DMclient 460 | */ 461 | protected _Uncompress(data: Buffer): Promise { 462 | return new Promise(resolve => { 463 | inflate(data, (error, result) => { 464 | if (error === null) return resolve(result) 465 | else { 466 | tools.ErrorLog(data, error) 467 | return resolve() 468 | } 469 | }) 470 | }) 471 | } 472 | } 473 | export default DMclient -------------------------------------------------------------------------------- /bilive/lib/tools.ts: -------------------------------------------------------------------------------- 1 | import util from 'util' 2 | import crypto from 'crypto' 3 | import request from 'request' 4 | import { EventEmitter } from 'events' 5 | import Options from '../options' 6 | /** 7 | * 一些工具, 供全局调用 8 | * 9 | * @class Tools 10 | * @extends EventEmitter 11 | */ 12 | class Tools extends EventEmitter { 13 | constructor() { 14 | super() 15 | this.on('systemMSG', (data: systemMSG) => this.Log(data.message)) 16 | } 17 | /** 18 | * 请求头 19 | * 20 | * @param {string} platform 21 | * @returns {request.Headers} 22 | * @memberof tools 23 | */ 24 | public getHeaders(platform: string): request.Headers { 25 | switch (platform) { 26 | case 'Android': 27 | return { 28 | 'Connection': 'Keep-Alive', 29 | 'User-Agent': 'Mozilla/5.0 BiliDroid/5.43.1 (bbcallen@gmail.com)' 30 | } 31 | case 'WebView': 32 | return { 33 | 'Accept': 'application/json, text/javascript, */*', 34 | 'Accept-Language': 'zh-CN', 35 | 'Connection': 'keep-alive', 36 | 'Cookie': 'l=v', 37 | 'Origin': 'https://live.bilibili.com', 38 | 'User-Agent': 'Mozilla/5.0 (Linux; Android 8.0.0; G8142 Build/47.1.A.12.270; wv) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.91 Mobile Safari/537.36 BiliApp/5300000', 39 | 'X-Requested-With': 'tv.danmaku.bili' 40 | } 41 | default: 42 | return { 43 | 'Accept': 'application/json, text/javascript, */*', 44 | 'Accept-Language': 'zh-CN', 45 | 'Connection': 'keep-alive', 46 | 'Cookie': 'l=v', 47 | 'DNT': '1', 48 | 'Origin': 'https://live.bilibili.com', 49 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36' 50 | } 51 | } 52 | } 53 | /** 54 | * 添加request头信息 55 | * 56 | * @template T 57 | * @param {request.OptionsWithUri} options 58 | * @param {('PC' | 'Android' | 'WebView')} [platform='PC'] 59 | * @returns {(Promise | undefined>)} 60 | * @memberof tools 61 | */ 62 | public XHR(options: request.OptionsWithUri, platform: 'PC' | 'Android' | 'WebView' = 'PC'): Promise | undefined> { 63 | return new Promise | undefined>(resolve => { 64 | options.gzip = true 65 | // 添加头信息 66 | const headers = this.getHeaders(platform) 67 | options.headers = options.headers === undefined ? headers : Object.assign(headers, options.headers) 68 | if (options.method === 'POST' && options.headers['Content-Type'] === undefined) 69 | options.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8' 70 | // 返回异步request 71 | request(options, (error, response, body) => { 72 | if (error === null) resolve({ response, body }) 73 | else { 74 | this.ErrorLog(options.uri, error) 75 | resolve() 76 | } 77 | }) 78 | }) 79 | } 80 | /** 81 | * 获取cookie值 82 | * 83 | * @param {request.CookieJar} jar 84 | * @param {string} key 85 | * @param {*} [url=apiLiveOrigin] 86 | * @returns {string} 87 | * @memberof tools 88 | */ 89 | public getCookie(jar: request.CookieJar, key: string, url = 'https://api.live.bilibili.com'): string { 90 | const cookies = jar.getCookies(url) 91 | const cookieFind = cookies.find(cookie => cookie.key === key) 92 | return cookieFind === undefined ? '' : cookieFind.value 93 | } 94 | /** 95 | * 设置cookie 96 | * 97 | * @param {string} cookieString 98 | * @returns {request.CookieJar} 99 | * @memberof tools 100 | */ 101 | public setCookie(cookieString: string): request.CookieJar { 102 | const jar = request.jar() 103 | cookieString.split(';').forEach(cookie => { 104 | jar.setCookie(`${cookie}; Domain=bilibili.com; Path=/`, 'https://bilibili.com') 105 | }) 106 | return jar 107 | } 108 | /** 109 | * 格式化JSON 110 | * 111 | * @template T 112 | * @param {string} text 113 | * @param {((key: any, value: any) => any)} [reviver] 114 | * @returns {(Promise)} 115 | * @memberof tools 116 | */ 117 | public JSONparse(text: string, reviver?: ((key: any, value: any) => any)): Promise { 118 | return new Promise(resolve => { 119 | try { 120 | const obj = JSON.parse(text, reviver) 121 | return resolve(obj) 122 | } 123 | catch (error) { 124 | this.ErrorLog('JSONparse', error) 125 | return resolve() 126 | } 127 | }) 128 | } 129 | /** 130 | * Hash 131 | * 132 | * @param {string} algorithm 133 | * @param {(string | Buffer)} data 134 | * @returns {string} 135 | * @memberof tools 136 | */ 137 | public Hash(algorithm: string, data: string | Buffer): string { 138 | return crypto.createHash(algorithm).update(data).digest('hex') 139 | } 140 | /** 141 | * 当前系统时间 142 | * 143 | * @returns {string} 144 | * @memberof Tools 145 | */ 146 | public Date(): string { 147 | return new Date().toString().slice(4, 24) 148 | } 149 | /** 150 | * 格式化输出, 配合PM2凑合用 151 | * 152 | * @param {...any[]} message 153 | * @memberof tools 154 | */ 155 | public Log(...message: any[]) { 156 | const log = util.format(`${this.Date()} :`, ...message) 157 | if (this.logs.length > 500) this.logs.shift() 158 | this.emit('log', log) 159 | this.logs.push(log) 160 | console.log(log) 161 | } 162 | public logs: string[] = [] 163 | /** 164 | * 格式化输出, 配合PM2凑合用 165 | * 166 | * @param {...any[]} message 167 | * @memberof tools 168 | */ 169 | public ErrorLog(...message: any[]) { 170 | console.error(`${this.Date()} :`, ...message) 171 | } 172 | /** 173 | * sleep 174 | * 175 | * @param {number} ms 176 | * @returns {Promise<'sleep'>} 177 | * @memberof tools 178 | */ 179 | public Sleep(ms: number): Promise<'sleep'> { 180 | return new Promise<'sleep'>(resolve => setTimeout(() => resolve('sleep'), ms)) 181 | } 182 | /** 183 | * 为了兼容旧版 184 | * 185 | * @param {string} message 186 | * @memberof Tools 187 | */ 188 | public sendSCMSG(message: string) { 189 | const adminServerChan = Options._.config.adminServerChan 190 | if (adminServerChan !== '') { 191 | const sendtoadmin: request.Options = { 192 | method: 'POST', 193 | uri: `https://sc.ftqq.com/${adminServerChan}.send`, 194 | body: `text=bilive_server&desp=${message}` 195 | } 196 | this.XHR(sendtoadmin) 197 | } 198 | } 199 | /** 200 | * 异或加密 201 | * 202 | * @param {string} key 203 | * @param {string} input 204 | * @returns {string} 205 | */ 206 | public static xorStrings(key: string, input: string): string { 207 | let output: string = '' 208 | for (let i = 0, len = input.length; i < len; i++) { 209 | output += String.fromCharCode( 210 | input.charCodeAt(i) ^ key.charCodeAt(i % key.length) 211 | ) 212 | } 213 | return output 214 | } 215 | public B64XorCipher = { 216 | encode(key: string, data: string): string { 217 | return (data && data !== '' && key !== '') ? new Buffer(Tools.xorStrings(key, data), 'utf8').toString('base64') : data 218 | }, 219 | decode(key: string, data: string): string { 220 | return (data && data !== '' && key !== '') ? Tools.xorStrings(key, new Buffer(data, 'base64').toString('utf8')) : data 221 | } 222 | } 223 | } 224 | 225 | export default new Tools() 226 | -------------------------------------------------------------------------------- /bilive/listener.ts: -------------------------------------------------------------------------------- 1 | import { Options as requestOptions } from 'request' 2 | import { EventEmitter } from 'events' 3 | import tools from './lib/tools' 4 | import AppClient from './lib/app_client' 5 | import RoomListener from './roomlistener' 6 | import Options from './options' 7 | /** 8 | * 监听服务器消息 9 | * 10 | * @class Listener 11 | * @extends {EventEmitter} 12 | */ 13 | class Listener extends EventEmitter { 14 | constructor() { 15 | super() 16 | } 17 | /** 18 | * 抽奖ID 19 | * 20 | * @private 21 | * @type {Set} 22 | * @memberof Listener 23 | */ 24 | private _raffleID: Set = new Set() 25 | private _dailyRaffleID: Set = new Set() 26 | /** 27 | * 快速抽奖ID 28 | * 29 | * @private 30 | * @type {Set} 31 | * @memberof Listener 32 | */ 33 | private _lotteryID: Set = new Set() 34 | private _dailyLotteryID: Set = new Set() 35 | /** 36 | * 节奏风暴ID 37 | * 38 | * @private 39 | * @type {Set} 40 | * @memberof Listener 41 | */ 42 | private _beatStormID: Set = new Set() 43 | private _dailyBeatStormID: Set = new Set() 44 | private _pklotteryID: Set = new Set() 45 | /** 46 | * 房间监听 47 | * 48 | * @private 49 | * @type {RoomListener} 50 | * @memberof Listener 51 | */ 52 | private _RoomListener!: RoomListener 53 | /** 54 | * 开始监听时间 55 | * 56 | * @private 57 | * @type {number} 58 | * @memberof Listener 59 | */ 60 | private _ListenStartTime: number = Date.now() 61 | /** 62 | * 消息缓存 63 | * 64 | * @private 65 | * @type {Set} 66 | * @memberof Listener 67 | */ 68 | public _MSGCache: Set = new Set() 69 | /** 70 | * 开始监听 71 | * 72 | * @memberof Listener 73 | */ 74 | public Start() { 75 | this._RoomListener = new RoomListener() 76 | this._RoomListener 77 | .on('SYS_MSG', dataJson => this._RaffleCheck(dataJson)) 78 | .on('SYS_GIFT', dataJson => this._RaffleCheck(dataJson)) 79 | .on('raffle', (raffleMessage: raffleMessage) => this._RaffleHandler(raffleMessage)) 80 | .on('lottery', (lotteryMessage: lotteryMessage) => this._RaffleHandler(lotteryMessage)) 81 | .on('pklottery', (lotteryMessage: lotteryMessage) => this._RaffleHandler(lotteryMessage)) 82 | .on('beatStorm', (beatStormMessage: beatStormMessage) => this._RaffleHandler(beatStormMessage)) 83 | .on('lottery2', (lotteryMessage: lotteryMessage) => this._RaffleHandler2(lotteryMessage)) 84 | .on('pklottery2', (lotteryMessage: lotteryMessage) => this._RaffleHandler(lotteryMessage)) 85 | .on('beatStorm2', (beatStormMessage: beatStormMessage) => this._RaffleHandler2(beatStormMessage)) 86 | .Start() 87 | Options.on('dbTimeUpdate', () => this._RoomListener._AddDBRoom()) 88 | Options.on('globalFlagUpdate', () => this._RoomListener._RefreshLiveRoomListener()) 89 | } 90 | /** 91 | * 清空每日ID缓存 92 | * 93 | * @memberof Listener 94 | */ 95 | public clearAllID() { 96 | this._dailyBeatStormID.clear() 97 | this._dailyRaffleID.clear() 98 | this._dailyLotteryID.clear() 99 | } 100 | /** 101 | * 计算遗漏数量 102 | * 103 | * @private 104 | * @param {Set} Set1 105 | * @param {Set} Set2 106 | * @memberof Listener 107 | */ 108 | private getMisses(Set1: Set, Set2?: Set) { 109 | let query1 = [...Set1] 110 | let query2 = Set2 === undefined ? [] : [...Set2] 111 | if (query2.length > 0 && query2[0].toString().length > 6) // For beatStorm IDs 112 | for (let i = 0; i < query2.length; i++) query2[i] = Number(query2[i].toString().slice(0, -6)) 113 | let query = query1.concat(query2).sort(function(a, b){return a - b}) 114 | let Start: number = 0 115 | let End: number = 0 116 | if (query.length > 0) { 117 | Start = query[0] 118 | End = query[query.length-1] 119 | } 120 | let Misses = End - Start + 1 - query.length 121 | if (query.length === 0) Misses -= 1 122 | return Misses 123 | } 124 | /** 125 | * 监听数据Log 126 | * 127 | * @param {number} int 128 | * @memberof Listener 129 | */ 130 | public logAllID(int: number) { 131 | const raffleMiss = this.getMisses(this._raffleID) 132 | const lotteryMiss = this.getMisses(this._lotteryID, this._beatStormID) 133 | const dailyRaffleMiss = this.getMisses(this._dailyRaffleID) 134 | const dailyLotteryMiss = this.getMisses(this._dailyLotteryID, this._dailyBeatStormID) 135 | const allRaffle = raffleMiss + this._raffleID.size 136 | const allLottery = lotteryMiss + this._lotteryID.size + this._beatStormID.size 137 | const dailyAllRaffle = dailyRaffleMiss + this._dailyRaffleID.size 138 | const dailyAllLottery = dailyLotteryMiss + this._dailyLotteryID.size + this._dailyBeatStormID.size 139 | const raffleMissRate = 100 * raffleMiss / (allRaffle === 0 ? 1 : allRaffle) 140 | const lotteryMissRate = 100 * lotteryMiss / (allLottery === 0 ? 1 : allLottery) 141 | const dailyRaffleMissRate = 100 * dailyRaffleMiss / (dailyAllRaffle === 0 ? 1 : dailyAllRaffle) 142 | const dailyLotteryMissRate = 100 * dailyLotteryMiss / (dailyAllLottery === 0 ? 1 : dailyAllLottery) 143 | let logMsg: string = '\n' 144 | logMsg += `/********************************* bilive_server 运行信息 *********************************/\n` 145 | logMsg += `本次监听开始于:${new Date(this._ListenStartTime).toString()}\n` 146 | logMsg += `已监听房间数:${this._RoomListener.roomListSize()}\n` 147 | logMsg += `共监听到raffle抽奖数:${this._raffleID.size}(${this._dailyRaffleID.size})\n` 148 | logMsg += `共监听到lottery抽奖数:${this._lotteryID.size}(${this._dailyLotteryID.size})\n` 149 | logMsg += `共监听到beatStorm抽奖数:${this._beatStormID.size}(${this._dailyBeatStormID.size})\n` 150 | logMsg += `raffle漏监听:${raffleMiss}(${raffleMissRate.toFixed(1)}%)\n` 151 | logMsg += `lottery漏监听:${lotteryMiss}(${lotteryMissRate.toFixed(1)}%)\n` 152 | logMsg += `今日raffle漏监听:${dailyRaffleMiss}(${dailyRaffleMissRate.toFixed(1)}%)\n` 153 | logMsg += `今日lottery漏监听:${dailyLotteryMiss}(${dailyLotteryMissRate.toFixed(1)}%)\n` 154 | tools.Log(logMsg) 155 | let pushMsg: string = '' 156 | pushMsg += `# bilive_server 监听情况报告\n` 157 | pushMsg += `- 本次监听开始于:${new Date(this._ListenStartTime).toString()}\n` 158 | pushMsg += `- 已监听房间数:${this._RoomListener.roomListSize()}\n` 159 | pushMsg += `- 共监听到raffle抽奖数:${this._raffleID.size}(${this._dailyRaffleID.size})\n` 160 | pushMsg += `- 共监听到lottery抽奖数:${this._lotteryID.size}(${this._dailyLotteryID.size})\n` 161 | pushMsg += `- 共监听到beatStorm抽奖数:${this._beatStormID.size}(${this._dailyBeatStormID.size})\n` 162 | pushMsg += `- raffle漏监听:${raffleMiss}(${raffleMissRate.toFixed(1)}%)\n` 163 | pushMsg += `- lottery漏监听:${lotteryMiss}(${lotteryMissRate.toFixed(1)}%)\n` 164 | pushMsg += `- 今日raffle漏监听:${dailyRaffleMiss}(${dailyRaffleMissRate.toFixed(1)}%)\n` 165 | pushMsg += `- 今日lottery漏监听:${dailyLotteryMiss}(${dailyLotteryMissRate.toFixed(1)}%)\n` 166 | if (int % 8 === 0) tools.sendSCMSG(pushMsg) 167 | } 168 | /** 169 | * 检查房间抽奖raffle信息 170 | * 171 | * @private 172 | * @param {(SYS_MSG | SYS_GIFT)} dataJson 173 | * @memberof Listener 174 | */ 175 | private async _RaffleCheck(dataJson: SYS_MSG | SYS_GIFT) { 176 | if (dataJson.real_roomid === undefined || this._MSGCache.has(dataJson.msg_text)) return 177 | this._MSGCache.add(dataJson.msg_text) 178 | const roomID = dataJson.real_roomid 179 | // 等待3s, 防止土豪刷屏 180 | await tools.Sleep(3000) 181 | const _lotteryInfo: requestOptions = { 182 | uri: `${Options._.config.apiLiveOrigin}/xlive/lottery-interface/v1/lottery/getLotteryInfo?${AppClient.signQueryBase(`roomid=${roomID}`)}`, 183 | json: true 184 | } 185 | const lotteryInfo = await tools.XHR(_lotteryInfo, 'Android') 186 | if (lotteryInfo !== undefined && lotteryInfo.response.statusCode === 200 187 | && lotteryInfo.body.code === 0 && lotteryInfo.body.data.gift_list.length > 0) { 188 | lotteryInfo.body.data.gift_list.forEach(data => { 189 | const message: message = { 190 | cmd: 'raffle', 191 | roomID, 192 | id: +data.raffleId, 193 | type: data.type, 194 | title: data.title, 195 | time: +data.time_wait, 196 | max_time: +data.max_time, 197 | time_wait: +data.time_wait 198 | } 199 | this._RaffleHandler(message) 200 | }) 201 | } 202 | } 203 | /** 204 | * 监听抽奖消息 205 | * 206 | * @private 207 | * @param {raffleMessage | lotteryMessage | beatStormMessage} raffleMessage 208 | * @memberof Listener 209 | */ 210 | private _RaffleHandler(raffleMessage: raffleMessage | lotteryMessage | beatStormMessage) { 211 | const { cmd, id, roomID } = raffleMessage 212 | switch (cmd) { 213 | case 'raffle': 214 | if (this._raffleID.has(id)) return 215 | this._raffleID.add(id) 216 | this._dailyRaffleID.add(id) 217 | break 218 | case 'lottery': 219 | if (this._lotteryID.has(id)) return 220 | this._lotteryID.add(id) 221 | this._dailyLotteryID.add(id) 222 | break 223 | case 'pklottery': 224 | if (this._pklotteryID.has(id)) return 225 | this._pklotteryID.add(id) 226 | break 227 | case 'beatStorm': 228 | if (this._beatStormID.has(id)) return 229 | this._beatStormID.add(id) 230 | this._dailyBeatStormID.add(id) 231 | break 232 | default: 233 | return 234 | } 235 | this.emit(cmd, raffleMessage) 236 | this._RoomListener.AddRoom(roomID) 237 | tools.Log(`房间 ${roomID} 开启了第 ${id} 轮${raffleMessage.title}`) 238 | this._RoomListener.UpdateDB(roomID, cmd) 239 | } 240 | /** 241 | * 监听抽奖消息2 242 | * 243 | * @private 244 | * @param {lotteryMessage | beatStormMessage} raffleMessage 245 | * @memberof Listener 246 | */ 247 | private _RaffleHandler2(lotteryMessage: lotteryMessage | beatStormMessage) { 248 | const { cmd, id, roomID } = lotteryMessage 249 | switch (cmd) { 250 | case 'lottery': 251 | if (this._lotteryID.has(id)) return 252 | this._lotteryID.add(id) 253 | this._dailyLotteryID.add(id) 254 | break 255 | case 'pklottery': 256 | if (this._pklotteryID.has(id)) return 257 | this._pklotteryID.add(id) 258 | break 259 | case 'beatStorm': 260 | if (this._beatStormID.has(id)) return 261 | this._beatStormID.add(id) 262 | this._dailyBeatStormID.add(id) 263 | break 264 | default: return 265 | } 266 | this.emit(cmd, lotteryMessage) 267 | tools.Log(`房间 ${roomID} 开启了第 ${id} 轮${lotteryMessage.title}`) 268 | } 269 | } 270 | export default Listener 271 | -------------------------------------------------------------------------------- /bilive/options.default.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": { 3 | "path": "", 4 | "hostname": "", 5 | "port": 20080, 6 | "protocol": "admin", 7 | "netkey": "" 8 | }, 9 | "config": { 10 | "dbTime": 30, 11 | "adminServerChan": "", 12 | "globalListener": false, 13 | "globalListenNum": -1, 14 | "liveOrigin": "https://live.bilibili.com", 15 | "apiLiveOrigin": "https://api.live.bilibili.com", 16 | "apiVCOrigin": "https://api.vc.bilibili.com", 17 | "excludeCMD": [], 18 | "sysmsg": "" 19 | }, 20 | "user": {}, 21 | "newUserData": { 22 | "status": false, 23 | "userHash": "", 24 | "welcome": "已连接到服务器", 25 | "usermsg": "", 26 | "raffle": false, 27 | "lottery": false, 28 | "pklottery": false, 29 | "beatStorm": false 30 | }, 31 | "info": { 32 | "dbTime": { 33 | "description": "活跃房间时间", 34 | "tip": "监听过去N天内的活跃房间,适用于数据库内房间监听", 35 | "type": "number" 36 | }, 37 | "globalListener": { 38 | "description": "开播房间监听", 39 | "tip": "对所有开播房间进行监听,可能会导致网络问题,慎用", 40 | "type": "boolean" 41 | }, 42 | "globalListenNum": { 43 | "description": "监听开播房间数", 44 | "tip": "默认为-1,表示对所有开播房间进行监听,只在开启全局监听时有效", 45 | "type": "number" 46 | }, 47 | "adminServerChan": { 48 | "description": "SCKEY", 49 | "tip": "Server酱对应的KEY", 50 | "type": "string" 51 | }, 52 | "liveOrigin": { 53 | "description": "直播首页", 54 | "tip": "直播首页地址", 55 | "type": "string" 56 | }, 57 | "apiLiveOrigin": { 58 | "description": "直播API", 59 | "tip": "直播API地址", 60 | "type": "string" 61 | }, 62 | "apiVCOrigin": { 63 | "description": "直播消息API", 64 | "tip": "直播消息API地址", 65 | "type": "string" 66 | }, 67 | "excludeCMD": { 68 | "description": "排除消息", 69 | "tip": "日志上排除的房间消息", 70 | "type": "stringArray" 71 | }, 72 | "sysmsg": { 73 | "description": "系统广播", 74 | "tip": "向全体用户发送广播", 75 | "type": "string" 76 | }, 77 | "status": { 78 | "description": "开关", 79 | "tip": "用户组设置的总开关, 关闭时该用户组无法连接", 80 | "type": "boolean" 81 | }, 82 | "userHash": { 83 | "description": "用户子协议", 84 | "tip": "作为protocol与服务器连接", 85 | "type": "string" 86 | }, 87 | "welcome": { 88 | "description": "欢迎词", 89 | "tip": "用户组内连接到服务器所返回的消息", 90 | "type": "string" 91 | }, 92 | "usermsg": { 93 | "description": "用户广播", 94 | "tip": "向该用户组下的所有用户发送广播", 95 | "type": "string" 96 | }, 97 | "raffle": { 98 | "description": "raffle类抽奖", 99 | "tip": "是否向该用户组发送raffle类抽奖消息, 例如活动抽奖", 100 | "type": "boolean" 101 | }, 102 | "lottery": { 103 | "description": "lottery类抽奖", 104 | "tip": "是否向该用户组发送lottery类抽奖消息, 例如舰队抽奖", 105 | "type": "boolean" 106 | }, 107 | "pklottery": { 108 | "description": "pklottery类抽奖", 109 | "tip": "是否向该用户组发送pklottery类抽奖消息, 如大乱斗抽奖", 110 | "type": "boolean" 111 | }, 112 | "beatStorm": { 113 | "description": "节奏风暴抽奖", 114 | "tip": "是否向该用户组发送节奏风暴抽奖消息", 115 | "type": "boolean" 116 | }, 117 | "protocol": { 118 | "description": "协议", 119 | "tip": "该用户连接使用的协议", 120 | "type": "string" 121 | }, 122 | "ip": { 123 | "description": "IP", 124 | "tip": "该用户连接时的IP", 125 | "type": "string" 126 | }, 127 | "ua": { 128 | "description": "UA", 129 | "tip": "该用户连接所使用的UA", 130 | "type": "string" 131 | } 132 | } 133 | } -------------------------------------------------------------------------------- /bilive/options.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import util from 'util' 3 | import { EventEmitter } from 'events' 4 | const FSwriteFile = util.promisify(fs.writeFile) 5 | 6 | /** 7 | * 8 | * 9 | * @class Options 10 | * @extends {EventEmitter} 11 | */ 12 | class Options extends EventEmitter { 13 | constructor() { 14 | super() 15 | this._dirname = __dirname + '/../..' 16 | // 检查是否有options目录 17 | const hasDir = fs.existsSync(this._dirname + '/options/') 18 | if (!hasDir) fs.mkdirSync(this._dirname + '/options/') 19 | // 复制默认设置文件到用户设置文件 20 | const hasFile = fs.existsSync(this._dirname + '/options/options.json') 21 | if (!hasFile) fs.copyFileSync(this._dirname + '/bilive/options.default.json', this._dirname + '/options/options.json') 22 | // 读取默认设置文件 23 | const defaultOptionBuffer = fs.readFileSync(this._dirname + '/bilive/options.default.json') 24 | const defaultOption = JSON.parse(defaultOptionBuffer.toString()) 25 | // 读取用户设置文件 26 | const userOptionBuffer = fs.readFileSync(this._dirname + '/options/options.json') 27 | const userOption = JSON.parse(userOptionBuffer.toString()) 28 | if (defaultOption === undefined || userOption === undefined) throw new TypeError('文件格式化失败') 29 | defaultOption.server = Object.assign({}, defaultOption.server, userOption.server) 30 | defaultOption.config = Object.assign({}, defaultOption.config, userOption.config) 31 | for (const uid in userOption.user) 32 | defaultOption.user[uid] = Object.assign({}, defaultOption.newUserData, userOption.user[uid]) 33 | this._ = defaultOption 34 | } 35 | public _: options 36 | private _dirname: string 37 | public async save() { 38 | const blacklist = ['newUserData', 'info'] 39 | const error = await FSwriteFile(this._dirname + '/options/options.json' 40 | , JSON.stringify(this._, (key, value) => blacklist.includes(key) ? undefined : value, 2)) 41 | if (error !== undefined) console.error(`${new Date().toString().slice(4, 24)} :`, error) 42 | return this._ 43 | } 44 | } 45 | export default new Options() 46 | -------------------------------------------------------------------------------- /bilive/roomlistener.ts: -------------------------------------------------------------------------------- 1 | import db from './db' 2 | import tools from './lib/tools' 3 | import DMclient from './dm_client_re' 4 | import Options from './options' 5 | import DanmuLib from './danmuLog' 6 | import { EventEmitter } from 'events' 7 | import { Options as requestOptions } from 'request' 8 | 9 | /** 10 | * 监听房间消息 11 | * 12 | * @class RoomListener 13 | * @extends {EventEmitter} 14 | */ 15 | class RoomListener extends EventEmitter { 16 | constructor() { 17 | super() 18 | } 19 | /** 20 | * 监控房间 21 | * 22 | * @type {Map} 23 | * @memberof RoomListener 24 | */ 25 | private roomList: Map = new Map() 26 | private liveRoomList: Map = new Map() 27 | private _DBRoomRefreshTimer!: NodeJS.Timer 28 | private _LiveRoomRefreshTimer!: NodeJS.Timer 29 | // 弹幕error计数 30 | private _DMErrorCount: number = 0 31 | // 弹幕error刷新计时器 32 | private _DMErrorTimer!: NodeJS.Timer 33 | /** 34 | * 开始监听 35 | * 36 | * @memberof RoomListener 37 | */ 38 | public async Start() { 39 | const load = await db.roomList.load() 40 | if (load === null) { 41 | tools.Log('roomList was loaded') 42 | this._AddDBRoom() 43 | this._DBRoomRefreshTimer = setInterval(() => this._AddDBRoom(), 24 * 60 * 60 * 1000) 44 | if (Options._.config.globalListener) { 45 | this._AddLiveRoom() 46 | this._LiveRoomRefreshTimer = setInterval(() => this._AddLiveRoom(), 5 * 60 * 1000) 47 | } 48 | this._DMErrorTimer = setInterval(() => { 49 | if (this._DMErrorCount > 300) this._ResetRoom() 50 | }, 60 * 1000) 51 | } 52 | else tools.ErrorLog(load) 53 | } 54 | /** 55 | * 全站开播房间监听-刷新 56 | * 57 | * @public 58 | */ 59 | public async _RefreshLiveRoomListener() { 60 | if (Options._.config.globalListener) { 61 | this._AddLiveRoom() 62 | this._LiveRoomRefreshTimer = setInterval(() => this._AddLiveRoom(), 5 * 60 * 1000) 63 | } 64 | else { 65 | const len = this.liveRoomList.size 66 | this.liveRoomList.forEach(async (commentClient, roomID) => { 67 | commentClient 68 | .removeAllListeners() 69 | .Close() 70 | this.liveRoomList.delete(roomID) 71 | }) 72 | tools.Log(`已断开与 ${len} 个开播房间的连接`) 73 | } 74 | } 75 | /** 76 | * 添加数据库内房间 77 | * 78 | * @private 79 | * @param {number} [date=Options._.config.dbTime * 24 * 60 * 60 * 1000] 80 | * @memberof RoomListener 81 | */ 82 | public async _AddDBRoom(date = Options._.config.dbTime * 24 * 60 * 60 * 1000) { 83 | const roomList = await db.roomList.find({ updateTime: { $gt: Date.now() - date } }) 84 | if (roomList instanceof Error) tools.ErrorLog('读取数据库失败', roomList) 85 | else { 86 | const liveList: Set = new Set() 87 | roomList.forEach(room => { 88 | liveList.add(room.roomID) 89 | this.AddRoom(room.roomID, room.masterID) 90 | }) 91 | this.roomList.forEach(async (commentClient, roomID) => { 92 | if (liveList.has(roomID)) return 93 | commentClient 94 | .removeAllListeners() 95 | .Close() 96 | this.roomList.delete(roomID) 97 | }) 98 | tools.Log(`已连接到数据库中的 ${roomList.length} 个房间`) 99 | } 100 | } 101 | /** 102 | * 添加已开播房间 103 | * 104 | * @private 105 | * @memberof RoomListener 106 | */ 107 | private async _AddLiveRoom() { 108 | const liveRoomInfo: requestOptions = { 109 | uri: `https://api.live.bilibili.com/room/v1/Area/getLiveRoomCountByAreaID?areaId=0`, 110 | json: true 111 | } 112 | const liveRooms = await tools.XHR(liveRoomInfo) 113 | if (liveRooms === undefined || liveRooms.response.statusCode !== 200 || liveRooms.body.code !== 0) return 114 | const liveNumber = liveRooms.body.data.num 115 | let roomSet: Set = new Set() 116 | let connectNumber: number = liveNumber 117 | if (Options._.config.globalListenNum > -1 && connectNumber > Options._.config.globalListenNum) 118 | connectNumber = Options._.config.globalListenNum 119 | for (let i = 1; i <= Math.ceil(connectNumber / 500); i++) { 120 | let allRoom = await tools.XHR({ 121 | uri: `https://api.live.bilibili.com/room/v1/Area/getListByAreaID?page=${i}&pageSize=500`, 122 | json: true 123 | }) 124 | if (allRoom === undefined || allRoom.body.code !== 0) continue 125 | else if (allRoom.response.statusCode !== 200) return tools.Log(allRoom.response.statusCode) 126 | let allRoomData = allRoom.body.data 127 | allRoomData.forEach(room => { 128 | if (this.roomList.has(room.roomid)) return 129 | roomSet.add(room.roomid) 130 | this.AddLiveRoom(room.roomid, room.uid) 131 | }) 132 | await tools.Sleep(3 * 1000) 133 | } 134 | this.liveRoomList.forEach(async (commentClient, roomID) => { 135 | if (roomSet.has(roomID)) return 136 | commentClient 137 | .removeAllListeners() 138 | .Close() 139 | this.liveRoomList.delete(roomID) 140 | }) 141 | tools.Log(`已连接到 ${liveNumber} 个开播房间中的 ${connectNumber} 个`) 142 | } 143 | /** 144 | * 重设监听 145 | * 146 | * @memberof RoomListener 147 | */ 148 | private async _ResetRoom() { 149 | this._DMErrorCount = 0 150 | clearInterval(this._DMErrorTimer) 151 | clearInterval(this._DBRoomRefreshTimer) 152 | clearInterval(this._LiveRoomRefreshTimer) 153 | this.roomList.forEach(async (commentClient, roomID) => { 154 | commentClient 155 | .removeAllListeners() 156 | .Close() 157 | this.roomList.delete(roomID) 158 | }) 159 | this.liveRoomList.forEach(async (commentClient, roomID) => { 160 | commentClient 161 | .removeAllListeners() 162 | .Close() 163 | this.liveRoomList.delete(roomID) 164 | }) 165 | await this.Start() 166 | } 167 | /** 168 | * 添加直播房间 169 | * 170 | * @param {number} roomID 171 | * @param {number} [userID=0] 172 | * @memberof RoomListener 173 | */ 174 | public async AddRoom(roomID: number, userID: number = 0) { 175 | if (this.roomList.has(roomID)) return 176 | if (userID === 0) userID = await this._getMasterID(roomID) 177 | const commentClient = new DMclient({ roomID, userID, protocol: 'flash' }) 178 | commentClient 179 | .on('SYS_MSG', dataJson => this.emit('SYS_MSG', dataJson)) 180 | .on('SYS_GIFT', dataJson => this.emit('SYS_GIFT', dataJson)) 181 | .on('TV_START', dataJson => this._RaffleStartHandler(dataJson)) 182 | .on('RAFFLE_START', dataJson => this._RaffleStartHandler(dataJson)) 183 | .on('LOTTERY_START', dataJson => this._LotteryStartHandler(dataJson)) 184 | .on('PK_LOTTERY_START', dataJson => this._PKLotteryStartHandler(dataJson)) 185 | .on('GUARD_LOTTERY_START', dataJson => this._LotteryStartHandler(dataJson)) 186 | .on('SPECIAL_GIFT', dataJson => this._SpecialGiftHandler(dataJson)) 187 | .on('ALL_MSG', dataJson => { 188 | if (!Options._.config.excludeCMD.includes(dataJson.cmd)) { 189 | Options._.config.excludeCMD.push(dataJson.cmd) 190 | tools.Log(JSON.stringify(dataJson)) 191 | DanmuLib.add(dataJson) 192 | } 193 | }) 194 | .on('DMerror', () => this._DMErrorCount++) 195 | .Connect({ server: 'broadcastlv.chat.bilibili.com', port: 2243 }) 196 | this.roomList.set(roomID, commentClient) 197 | } 198 | /** 199 | * 添加直播房间2 200 | * 201 | * @param {number} roomID 202 | * @param {number} userID 203 | * @memberof RoomListener 204 | */ 205 | public async AddLiveRoom(roomID: number, userID: number = 0) { 206 | if (this.liveRoomList.has(roomID)) return 207 | if (userID === 0) userID = await this._getMasterID(roomID) 208 | const commentClient = new DMclient({ roomID, userID, protocol: 'flash' }) 209 | commentClient 210 | .on('SYS_MSG', dataJson => this.emit('SYS_MSG', dataJson)) 211 | .on('SYS_GIFT', dataJson => this.emit('SYS_GIFT', dataJson)) 212 | .on('TV_START', dataJson => this._RaffleStartHandler(dataJson)) 213 | .on('RAFFLE_START', dataJson => this._RaffleStartHandler(dataJson)) 214 | .on('LOTTERY_START', dataJson => this._LotteryStartHandler(dataJson, '2')) 215 | .on('PK_LOTTERY_START', dataJson => this._PKLotteryStartHandler(dataJson, '2')) 216 | .on('GUARD_LOTTERY_START', dataJson => this._LotteryStartHandler(dataJson, '2')) 217 | .on('SPECIAL_GIFT', dataJson => this._SpecialGiftHandler(dataJson, '2')) 218 | .on('ALL_MSG', dataJson => { 219 | if (!Options._.config.excludeCMD.includes(dataJson.cmd)) { 220 | Options._.config.excludeCMD.push(dataJson.cmd) 221 | tools.Log(JSON.stringify(dataJson)) 222 | DanmuLib.add(dataJson) 223 | } 224 | }) 225 | .on('DMerror', () => this._DMErrorCount++) 226 | .Connect({ server: 'broadcastlv.chat.bilibili.com', port: 2243 }) 227 | this.liveRoomList.set(roomID, commentClient) 228 | } 229 | /** 230 | * 返回房间数 231 | * 232 | * @memberof RoomListener 233 | */ 234 | public roomListSize() { 235 | return (this.roomList.size + this.liveRoomList.size) 236 | } 237 | /** 238 | * 监听抽奖 239 | * 240 | * @private 241 | * @param {RAFFLE_START} dataJson 242 | * @memberof RoomListener 243 | */ 244 | private _RaffleStartHandler(dataJson: RAFFLE_START) { 245 | if (dataJson.data === undefined || dataJson.data.raffleId === undefined) return 246 | const cmd = 'raffle' 247 | const raffleMessage: raffleMessage = { 248 | cmd, 249 | roomID: dataJson._roomid, 250 | id: +dataJson.data.raffleId, 251 | type: dataJson.data.type, 252 | title: dataJson.data.title, 253 | time: +dataJson.data.time, 254 | max_time: +dataJson.data.max_time, 255 | time_wait: +dataJson.data.time_wait 256 | } 257 | this.emit(cmd, raffleMessage) 258 | } 259 | /** 260 | * 监听快速抽奖 261 | * 262 | * @private 263 | * @param {LOTTERY_START} dataJson 264 | * @param {null | 2} source 265 | * @memberof RoomListener 266 | */ 267 | private _LotteryStartHandler(dataJson: LOTTERY_START, source: '' | '2' = '') { 268 | if (dataJson.data === undefined || dataJson.data.id === undefined) return 269 | const lotteryMessage: lotteryMessage = { 270 | cmd: 'lottery', 271 | roomID: dataJson._roomid, 272 | id: +dataJson.data.id, 273 | type: dataJson.data.type, 274 | title: '舰队抽奖', 275 | time: +dataJson.data.lottery.time 276 | } 277 | this.emit(`lottery${source}`, lotteryMessage) 278 | } 279 | /** 280 | * 监听大乱斗抽奖 281 | * 282 | * @private 283 | * @param {PK_LOTTERY_START} dataJson 284 | * @param {null | 2} source 285 | * @memberof RoomListener 286 | */ 287 | private _PKLotteryStartHandler(dataJson: PK_LOTTERY_START, source: '' | '2' = '') { 288 | if (dataJson.data === undefined || dataJson.data.id === undefined) return 289 | const raffleMessage: lotteryMessage = { 290 | cmd: 'pklottery', 291 | roomID: dataJson._roomid, 292 | id: +dataJson.data.id, 293 | type: 'pk', 294 | title: '大乱斗抽奖', 295 | time: +dataJson.data.time 296 | } 297 | //tools.sendSCMSG(JSON.stringify(raffleMessage)) 298 | this.emit(`pklottery${source}`, raffleMessage) 299 | } 300 | /** 301 | * 监听特殊礼物消息 302 | * 303 | * @private 304 | * @param {SPECIAL_GIFT} dataJson 305 | * @param {'' | '2'} source 306 | * @memberof RoomListener 307 | */ 308 | private _SpecialGiftHandler(dataJson: SPECIAL_GIFT, source: '' | '2' = '') { 309 | if (dataJson.data['39'] !== undefined && dataJson.data['39'].action === 'start') this._BeatStormHandler(dataJson, source) 310 | } 311 | /** 312 | * 监听节奏风暴消息 313 | * 314 | * @private 315 | * @param {SPECIAL_GIFT} dataJson 316 | * @param {'' | '2'} source 317 | * @memberof RoomListener 318 | */ 319 | private _BeatStormHandler(dataJson: SPECIAL_GIFT, source: '' | '2' = '') { 320 | const beatStormData = dataJson.data['39'] 321 | const beatStormMessage: beatStormMessage = { 322 | cmd: 'beatStorm', 323 | roomID: dataJson._roomid, 324 | num: +beatStormData.num, 325 | id: +beatStormData.id, 326 | type: 'beatStorm', 327 | title: '节奏风暴', 328 | time: Date.now() 329 | } 330 | this.emit(`beatStorm${source}`, beatStormMessage) 331 | } 332 | /** 333 | * 写入数据库 334 | * 335 | * @param {number} roomID 336 | * @param {string} cmd 337 | * @memberof RoomListener 338 | */ 339 | public async UpdateDB(roomID: number, cmd: string) { 340 | const $inc: { [index: string]: number } = {} 341 | $inc[cmd] = 1 342 | const roomInfo = await db.roomList.findOne({ roomID }) 343 | let $set 344 | if (!(roomInfo instanceof Error) && (roomInfo === null || roomInfo.masterID === 0)) { 345 | const masterID = await this._getMasterID(roomID) 346 | $set = { masterID, updateTime: Date.now() } 347 | } 348 | if ($set === undefined) $set = { updateTime: Date.now() } 349 | const update = await db.roomList.update({ roomID }, { $inc, $set }, { upsert: true }) 350 | if (update instanceof Error) tools.ErrorLog('更新数据库失败', update) 351 | } 352 | /** 353 | * 获取masterID 354 | * 355 | * @private 356 | * @param {number} roomID 357 | * @returns {Promise} 358 | * @memberof RoomListener 359 | */ 360 | private async _getMasterID(roomID: number): Promise { 361 | const getRoomInit: requestOptions = { 362 | uri: `${Options._.config.apiLiveOrigin}/room/v1/Room/mobileRoomInit?id=${roomID}}`, 363 | json: true 364 | } 365 | const roomInit = await tools.XHR(getRoomInit, 'Android') 366 | if (roomInit !== undefined && roomInit.response.statusCode === 200) 367 | return roomInit.body.data.uid 368 | return 0 369 | } 370 | } 371 | interface liveRooms { 372 | code: number 373 | data: liveRoomNum 374 | message: string 375 | msg: string 376 | } 377 | interface liveRoomNum { 378 | num: number 379 | } 380 | interface allRooms { 381 | code: number 382 | data: allRoomsData[] 383 | message: string 384 | msg: string 385 | } 386 | interface allRoomsData { 387 | uid: number 388 | roomid: number 389 | } 390 | 391 | export default RoomListener 392 | -------------------------------------------------------------------------------- /bilive/wsserver.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import ws from 'ws' 3 | import http from 'http' 4 | import { randomBytes } from 'crypto' 5 | import tools from './lib/tools' 6 | import Options from './options' 7 | const { B64XorCipher } = tools 8 | /** 9 | * WebSocket服务 10 | * 11 | * @class WSServer 12 | */ 13 | class WSServer { 14 | private _wsServer!: ws.Server 15 | private _clients: Map> = new Map() 16 | private _adminClient!: ws 17 | //@ts-ignore 18 | private _loop: NodeJS.Timer 19 | /** 20 | * 启动HTTP以及WebSocket服务 21 | * 22 | * @memberof WSServer 23 | */ 24 | public async Start() { 25 | this._HttpServer() 26 | } 27 | /** 28 | * HTTP服务 29 | * 30 | * @private 31 | * @memberof Options 32 | */ 33 | private _HttpServer() { 34 | // 跳转地址改成coding.me 35 | const server = http.createServer((req, res) => { 36 | req.on('error', error => tools.ErrorLog('req', error)) 37 | res.on('error', error => tools.ErrorLog('res', error)) 38 | res.writeHead(302, { 'Location': '//vector000.coding.me/bilive_setting/' }) 39 | res.end() 40 | }).on('error', error => tools.ErrorLog('http', error)) 41 | // 监听地址优先支持Unix Domain Socket 42 | const listen = Options._.server 43 | if (listen.path === '') { 44 | const host = process.env.HOST === undefined ? listen.hostname : process.env.HOST 45 | const port = process.env.PORT === undefined ? listen.port : Number.parseInt(process.env.PORT) 46 | server.listen(port, host, () => { 47 | this._WebSocketServer(server) 48 | tools.Log(`已监听 ${host}:${port}`) 49 | }) 50 | } 51 | else { 52 | if (fs.existsSync(listen.path)) fs.unlinkSync(listen.path) 53 | server.listen(listen.path, () => { 54 | fs.chmodSync(listen.path, '666') 55 | this._WebSocketServer(server) 56 | tools.Log(`已监听 ${listen.path}`) 57 | }) 58 | } 59 | } 60 | /** 61 | * WebSocket服务 62 | * 63 | * @private 64 | * @param {http.Server} server 65 | * @memberof WSServer 66 | */ 67 | private _WebSocketServer(server: http.Server) { 68 | // 不知道子协议的具体用法 69 | this._wsServer = new ws.Server({ 70 | server, 71 | verifyClient: (info: { origin: string, req: http.IncomingMessage, secure: boolean }) => { 72 | const protocol = info.req.headers['sec-websocket-protocol'] 73 | if (protocol === undefined) return false 74 | const adminProtocol = Options._.server.protocol 75 | const userData = Options._.user[protocol] 76 | if (protocol === adminProtocol || (userData !== undefined && userData.status)) return true 77 | else return false 78 | } 79 | }) 80 | this._wsServer 81 | .on('error', error => tools.ErrorLog('websocket', error)) 82 | .on('connection', (client: ws, req: http.IncomingMessage) => { 83 | // 使用Nginx可能需要 84 | const remoteAddress = req.headers['x-real-ip'] === undefined 85 | ? `${req.connection.remoteAddress}:${req.connection.remotePort}` 86 | : `${req.headers['x-real-ip']}:${req.headers['x-real-port']}` 87 | const useragent = req.headers['user-agent'] 88 | const protocol = client.protocol 89 | const adminProtocol = Options._.server.protocol 90 | let user: string 91 | if (protocol === adminProtocol) { 92 | user = '管理员' 93 | this._AdminConnectionHandler(client, remoteAddress) 94 | } 95 | else { 96 | user = `用户: ${client.protocol}` 97 | this._WsConnectionHandler(client, remoteAddress) 98 | } 99 | tools.Log(`${user} 地址: ${remoteAddress} 已连接. user-agent: ${useragent}`) 100 | }) 101 | this._loop = setInterval(() => this._WebSocketPing(), 60 * 1000) 102 | } 103 | /** 104 | * 管理员连接 105 | * 106 | * @private 107 | * @param {ws} client 108 | * @param {string} remoteAddress 109 | * @memberof WSServer 110 | */ 111 | private _AdminConnectionHandler(client: ws, remoteAddress: string) { 112 | // 限制同时只能连接一个客户端 113 | if (this._adminClient !== undefined) this._adminClient.close(1001, JSON.stringify({ cmd: 'close', msg: 'too many connections' })) 114 | // 使用function可能出现一些问题, 此处无妨 115 | const onLog = (data: string) => this._sendtoadmin({ cmd: 'log', ts: 'log', msg: data }) 116 | client 117 | .on('error', err => { 118 | tools.removeListener('log', onLog) 119 | this._destroyClient(client) 120 | tools.ErrorLog(client.protocol, remoteAddress, err) 121 | }) 122 | .on('close', (code, reason) => { 123 | tools.removeListener('log', onLog) 124 | this._destroyClient(client) 125 | tools.Log(`管理员 地址: ${remoteAddress} 已断开`, code, reason) 126 | }) 127 | .on('message', async (msg: string) => { 128 | const message = await tools.JSONparse(B64XorCipher.decode(Options._.server.netkey || '', msg)) 129 | if (message !== undefined && message.cmd !== undefined && (message).ts !== undefined) this._onCMD(message) 130 | else this._sendtoadmin({ cmd: 'error', ts: 'error', msg: '消息格式错误' }) 131 | }) 132 | this._adminClient = client 133 | // 日志 134 | tools.on('log', onLog) 135 | } 136 | /** 137 | * 处理连接事件 138 | * 139 | * @private 140 | * @param {ws} client 141 | * @param {string} remoteAddress 142 | * @memberof WSServer 143 | */ 144 | private _WsConnectionHandler(client: ws, remoteAddress: string) { 145 | const protocol = client.protocol 146 | const userData = Options._.user[protocol] 147 | // 分protocol存储 148 | if (this._clients.has(protocol)) { 149 | const clients = >this._clients.get(protocol) 150 | clients.add(client) 151 | } 152 | else { 153 | const clients = new Set([client]) 154 | this._clients.set(protocol, clients) 155 | } 156 | let timeout: NodeJS.Timer 157 | const setTimeoutError = () => { 158 | timeout = setTimeout(() => { 159 | client.emit('close', 4000, 'timeout') 160 | }, 2 * 60 * 1000) 161 | } 162 | client 163 | .on('error', err => { 164 | this._destroyClient(client) 165 | tools.ErrorLog(protocol, remoteAddress, err) 166 | }) 167 | .on('close', (code, reason) => { 168 | this._destroyClient(client) 169 | const clients = >this._clients.get(protocol) 170 | clients.delete(client) 171 | if (clients.size === 0) this._clients.delete(protocol) 172 | tools.Log(`用户: ${protocol} 地址: ${remoteAddress} 已断开`, code, reason) 173 | }) 174 | .on('pong', () => { 175 | clearTimeout(timeout) 176 | setTimeoutError() 177 | }) 178 | setTimeoutError() 179 | // 连接成功消息 180 | const welcome: message = { 181 | cmd: 'sysmsg', 182 | msg: userData.welcome 183 | } 184 | client.send(JSON.stringify(welcome), err => { if (err !== undefined) tools.ErrorLog(err) }) 185 | } 186 | /** 187 | * 销毁 188 | * 189 | * @private 190 | * @param {ws} client 191 | * @memberof WSServer 192 | */ 193 | private _destroyClient(client: ws) { 194 | client.close() 195 | client.terminate() 196 | client.removeAllListeners() 197 | } 198 | /** 199 | * Ping/Pong 200 | * 201 | * @private 202 | * @memberof WSServer 203 | */ 204 | private _WebSocketPing() { 205 | this._wsServer.clients.forEach(client => client.ping()) 206 | } 207 | /** 208 | * 消息广播 209 | * 210 | * @param {string} msg 211 | * @param {string} [protocol] 212 | * @memberof WSServer 213 | */ 214 | public SysMsg(msg: string, protocol?: string) { 215 | const systemMessage: systemMessage = { 216 | cmd: 'sysmsg', 217 | msg 218 | } 219 | this._Broadcast(systemMessage, 'sysmsg', protocol) 220 | } 221 | /** 222 | * 节奏风暴 223 | * 224 | * @param {beatStormInfo} beatStormInfo 225 | * @param {string} [protocol] 226 | * @memberof WSServer 227 | */ 228 | public BeatStorm(beatStormInfo: message, protocol?: string) { 229 | this._Broadcast(beatStormInfo, 'beatStorm', protocol) 230 | } 231 | /** 232 | * 抽奖raffle 233 | * 234 | * @param {raffleMessage} raffleMessage 235 | * @param {string} [protocol] 236 | * @memberof WSServer 237 | */ 238 | public Raffle(raffleMessage: raffleMessage, protocol?: string) { 239 | this._Broadcast(raffleMessage, 'raffle', protocol) 240 | } 241 | /** 242 | * 抽奖lottery 243 | * 244 | * @param {lotteryMessage} lotteryMessage 245 | * @param {string} [protocol] 246 | * @memberof WSServer 247 | */ 248 | public Lottery(lotteryMessage: message, protocol?: string) { 249 | this._Broadcast(lotteryMessage, 'lottery', protocol) 250 | } 251 | /** 252 | * 大乱斗抽奖 253 | * 254 | * @param {lotteryMessage} lotteryMessage 255 | * @param {string} [protocol] 256 | * @memberof WSServer 257 | */ 258 | public PKLottery(lotteryMessage: message, protocol?: string) { 259 | this._Broadcast(lotteryMessage, 'pklottery', protocol) 260 | } 261 | /** 262 | * 广播消息 263 | * 264 | * @private 265 | * @param {message} message 266 | * @param {string} key 267 | * @param {string} [protocol] 268 | * @memberof WSServer 269 | */ 270 | private _Broadcast(message: message, key: string, protocol?: string) { 271 | this._clients.forEach((clients, userprotocol) => { 272 | if (protocol !== undefined && protocol !== userprotocol) return 273 | const userData = Options._.user[userprotocol] 274 | if (userData !== undefined && (key === 'sysmsg' || userData[key])) { 275 | clients.forEach(client => { 276 | if (client.readyState === ws.OPEN) client.send(JSON.stringify(message), error => { if (error !== undefined) tools.Log(error) }) 277 | }) 278 | } 279 | }) 280 | } 281 | /** 282 | * 监听客户端发来的消息, CMD为关键字 283 | * 284 | * @private 285 | * @param {adminMessage} message 286 | * @memberof WSServer 287 | */ 288 | private async _onCMD(message: adminMessage) { 289 | const { cmd, ts } = message 290 | switch (cmd) { 291 | // 获取log 292 | case 'getLog': { 293 | const data = tools.logs 294 | this._sendtoadmin({ cmd, ts, data }) 295 | } 296 | break 297 | // 获取设置 298 | case 'getConfig': { 299 | const data = Options._.config 300 | this._sendtoadmin({ cmd, ts, data }) 301 | } 302 | break 303 | // 保存设置 304 | case 'setConfig': { 305 | const config = Options._.config 306 | const sysmsg = config.sysmsg 307 | const time = config.dbTime 308 | const globalFlag = config.globalListener 309 | const setConfig = message.data || {} 310 | let msg = '' 311 | for (const i in config) { 312 | if (typeof config[i] !== typeof setConfig[i]) { 313 | // 一般都是自用, 做一个简单的验证就够了 314 | msg = i + '参数错误' 315 | break 316 | } 317 | } 318 | if (msg === '') { 319 | // 防止setConfig里有未定义属性, 不使用Object.assign 320 | for (const i in config) config[i] = setConfig[i] 321 | Options.save() 322 | this._sendtoadmin({ cmd, ts, data: config }) 323 | if (sysmsg !== config.sysmsg) this.SysMsg(config.sysmsg) 324 | if (time !== config.dbTime) Options.emit('dbTimeUpdate') 325 | if (globalFlag !== config.globalListener) Options.emit('globalFlagUpdate') 326 | } 327 | else this._sendtoadmin({ cmd, ts, msg, data: config }) 328 | } 329 | break 330 | // 修改密钥 331 | case 'setNewNetkey': { 332 | const server = Options._.server 333 | const config = message.data || {} 334 | server.netkey = config.netkey || '' 335 | Options.save() 336 | this._sendtoadmin({ cmd, ts }) 337 | } 338 | break 339 | // 获取参数描述 340 | case 'getInfo': { 341 | const data = Options._.info 342 | this._sendtoadmin({ cmd, ts, data }) 343 | } 344 | break 345 | // 获取uid 346 | case 'getAllUID': { 347 | const data = Object.keys(Options._.user) 348 | this._sendtoadmin({ cmd, ts, data }) 349 | } 350 | break 351 | // 获取用户设置 352 | case 'getUserData': { 353 | const user = Options._.user 354 | const getUID = message.uid 355 | if (typeof getUID === 'string' && user[getUID] !== undefined) this._sendtoadmin({ cmd, ts, uid: getUID, data: user[getUID] }) 356 | else this._sendtoadmin({ cmd, ts, msg: '未知用户' }) 357 | } 358 | break 359 | // 保存用户设置 360 | case 'setUserData': { 361 | const user = Options._.user 362 | const setUID = message.uid 363 | if (setUID !== undefined && user[setUID] !== undefined) { 364 | const userData = user[setUID] 365 | const usermsg = userData.usermsg 366 | const setUserData = message.data || {} 367 | let msg = '' 368 | for (const i in userData) { 369 | if (typeof userData[i] !== typeof setUserData[i]) { 370 | msg = i + '参数错误' 371 | break 372 | } 373 | } 374 | if (msg === '') { 375 | for (const i in userData) { if (i !== 'userHash') userData[i] = setUserData[i] } 376 | Options.save() 377 | this._sendtoadmin({ cmd, ts, uid: setUID, data: userData }) 378 | // 删除已连接用户 379 | if (!userData.status) { 380 | if (this._clients.has(setUID)) { 381 | const clients = >this._clients.get(setUID) 382 | clients.forEach(client => this._destroyClient(client)) 383 | this._clients.delete(setUID) 384 | } 385 | } 386 | else if (usermsg !== userData.usermsg) this.SysMsg(userData.usermsg, setUID) 387 | } 388 | else this._sendtoadmin({ cmd, ts, uid: setUID, msg, data: userData }) 389 | } 390 | else this._sendtoadmin({ cmd, ts, uid: setUID, msg: '未知用户' }) 391 | } 392 | break 393 | // 删除用户设置 394 | case 'delUserData': { 395 | const user = Options._.user 396 | const delUID = message.uid 397 | if (delUID !== undefined && user[delUID] !== undefined) { 398 | const userData = user[delUID] 399 | delete Options._.user[delUID] 400 | Options.save() 401 | this._sendtoadmin({ cmd, ts, uid: delUID, data: userData }) 402 | // 删除已连接用户 403 | if (this._clients.has(delUID)) { 404 | const clients = >this._clients.get(delUID) 405 | clients.forEach(client => this._destroyClient(client)) 406 | this._clients.delete(delUID) 407 | } 408 | } 409 | else this._sendtoadmin({ cmd, ts, uid: delUID, msg: '未知用户' }) 410 | } 411 | break 412 | // 新建用户设置 413 | case 'newUserData': { 414 | // 虽然不能保证唯一性, 但是这都能重复的话可以去买彩票 415 | const uid = randomBytes(16).toString('hex') 416 | const data = Object.assign({}, Options._.newUserData) 417 | data.userHash = uid 418 | Options._.user[uid] = data 419 | Options.save() 420 | this._sendtoadmin({ cmd, ts, uid, data }) 421 | } 422 | break 423 | // 未知命令 424 | default: 425 | this._sendtoadmin({ cmd, ts, msg: '未知命令' }) 426 | break 427 | } 428 | } 429 | /** 430 | * 向客户端发送消息 431 | * 432 | * @private 433 | * @param {adminMessage} message 434 | * @memberof WebAPI 435 | */ 436 | private _sendtoadmin(message: adminMessage) { 437 | if (this._adminClient.readyState === ws.OPEN) this._adminClient.send(B64XorCipher.encode(Options._.server.netkey || '', JSON.stringify(message))) 438 | } 439 | } 440 | 441 | export default WSServer -------------------------------------------------------------------------------- /ecosystem.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps : [{ 3 | name: 'bilive_server', 4 | script: 'build/app.js', 5 | instances: 1, 6 | autorestart: true, 7 | watch: false, 8 | max_memory_restart: '1G', 9 | output: './logs/out.log', 10 | env: { 11 | NODE_ENV: 'development' 12 | }, 13 | env_production: { 14 | NODE_ENV: 'production' 15 | } 16 | }] 17 | }; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bilive_server", 3 | "version": "1.0.1", 4 | "description": "bilibili直播监控程序", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "npm run build:tsc && npm-run-posix-or-windows build:copy", 8 | "build:tsc": "tsc -p tsconfig.json || exit 0", 9 | "build:copy": "cp bilive/options.default.json build/bilive/", 10 | "build:copy:windows": "copy bilive\\options.default.json build\\bilive\\ /Y", 11 | "clean": "rimraf build", 12 | "start": "node build/app.js" 13 | }, 14 | "author": "lzghzr", 15 | "license": "MIT", 16 | "devDependencies": { 17 | "@types/nedb": "^1.8.7", 18 | "@types/node": "^12.0.8", 19 | "@types/nodemailer": "^4.6.8", 20 | "@types/request": "^2.48.1", 21 | "@types/ws": "^6.0.1", 22 | "npm-run-posix-or-windows": "^2.0.2", 23 | "rimraf": "^2.6.3", 24 | "typescript": "^3.5.2" 25 | }, 26 | "dependencies": { 27 | "nedb": "^1.8.0", 28 | "nodemailer": "^4.7.0", 29 | "request": "^2.88.0", 30 | "ws": "^6.2.1" 31 | }, 32 | "config": { 33 | "commitizen": { 34 | "path": "./node_modules/cz-conventional-changelog" 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* 基本选项 */ 4 | "target": "es2017", /* 指定 ECMAScript 目标版本: "ES3" (默认)、"ES5"、"ES2015"、"ES2016"、"ES2017"、"ES2018" 或 "ESNEXT"。 */ 5 | "module": "commonjs", /* 指定模块代码生成: "none"、"commonjs"、"amd"、"system"、"umd"、"es2015"或 "ESNext"。 */ 6 | // "lib": [], /* 指定要在编译中包括的库文件。 */ 7 | // "allowJs": true, /* 允许编译 JavaScript 文件。 */ 8 | // "checkJs": true, /* .js 文件中的报表出错。 */ 9 | // "jsx": "preserve", /* 指定 JSX 代码生成: "preserve"、"react-native" 或 "react"。 */ 10 | "declaration": false, /* 生成相应的 ".d.ts" 文件。 */ 11 | // "declarationMap": true, /* 为每个相应的 ".d.ts" 文件生成 sourcemap。 */ 12 | "sourceMap": false, /* 生成相应的 ".map" 文件。 */ 13 | // "outFile": "./", /* 连接输出并将其发出到单个文件。 */ 14 | "outDir": "build", /* 将输出结构重定向到目录。 */ 15 | // "rootDir": "./", /* 指定输入文件的根目录。与 --outDir 一起用于控制输出目录结构。 */ 16 | // "composite": true, /* 启用项目编译 */ 17 | "removeComments": true, /* 请勿将注释发出到输出。 */ 18 | // "noEmit": true, /* 请勿发出输出。 */ 19 | // "importHelpers": true, /* 从 "tslib" 导入发出帮助程序。 */ 20 | // "downlevelIteration": true, /* 以 "ES5" 或 "ES3" 设为目标时,对 "for-of"、传播和析构中的可迭代项提供完全支持。 */ 21 | // "isolatedModules": true, /* 将每个文件转换为单独的模块(类似 "ts.transpileModule")。 */ 22 | 23 | /* 严格类型检查选项 */ 24 | "strict": true, /* 启用所有严格类型检查选项。 */ 25 | // "noImplicitAny": true, /* 对具有隐式 "any" 类型的表达式和声明引发错误。 */ 26 | // "strictNullChecks": true, /* 启用严格的 NULL 检查。 */ 27 | // "strictFunctionTypes": true, /* 对函数类型启用严格检查。 */ 28 | // "strictPropertyInitialization": true, /* 启用类中属性初始化的严格检查。 */ 29 | // "noImplicitThis": true, /* 在带隐式“any" 类型的 "this" 表达式上引发错误。 */ 30 | // "alwaysStrict": true, /* 以严格模式进行分析,并为每个源文件发出 "use strict" 指令。 */ 31 | 32 | /* 其他检查 */ 33 | "noUnusedLocals": true, /* 报告未使用的局部变量上的错误。 */ 34 | "noUnusedParameters": true, /* 报告未使用的参数上的错误。 */ 35 | "noImplicitReturns": true, /* 在函数中的所有代码路径并非都返回值时报告错误。 */ 36 | "noFallthroughCasesInSwitch": true, /* 报告 switch 语句中遇到 fallthrough 情况的错误。 */ 37 | 38 | /* 模块分辨率选项 */ 39 | "moduleResolution": "node", /* 指定模块解析策略: "node" (Node.js)或 "classic" (TypeScript pre-1.6)。 */ 40 | // "baseUrl": "./", /* 用于解析非绝对模块名的基目录。 */ 41 | // "paths": {}, /* 一系列条目,这些条目将重新映射导入内容,以查找与 "baseUrl" 有关的位置。 */ 42 | // "rootDirs": [], /* 根文件夹列表,其组合内容表示在运行时的项目结构。 */ 43 | // "typeRoots": [], /* 包含类型定义来源的文件夹列表。 */ 44 | // "types": [], /* 要包含在编译中类型声明文件。 */ 45 | // "allowSyntheticDefaultImports": true, /* 允许从不带默认输出的模块中默认输入。这不会影响代码发出,只是类型检查。 */ 46 | "esModuleInterop": true, /* 通过为所有导入创建命名空间对象来启用 CommonJS 和 ES 模块之间的发出互操作性。表示 "allowSyntheticDefaultImports"。 */ 47 | // "preserveSymlinks": true, /* 不要解析 symlink 的真实路径。 */ 48 | 49 | /* 源映射选项 */ 50 | // "sourceRoot": "./", /* 指定调试调试程序应将 TypeScript 文件放置到的位置而不是源位置。 */ 51 | // "mapRoot": "./", /* 指定调试程序应将映射文件放置到的位置而不是生成的位置。 */ 52 | // "inlineSourceMap": true, /* 发出包含源映射而非包含单独文件的单个文件。 */ 53 | // "inlineSources": true, /* 在单个文件内发出源以及源映射;需要设置 "--inlineSourceMap" 或 "--sourceMap"。 */ 54 | 55 | /* 实验性选项 */ 56 | // "experimentalDecorators": true, /* 对 ES7 修饰器启用实验支持。 */ 57 | // "emitDecoratorMetadata": true, /* 对发出修饰器的类型元数据启用实验支持。 */ 58 | 59 | /* 高级选项 */ 60 | "locale": "zh-cn" /* 向用户显示消息时所用的区域设置(例如,"en-us") */ 61 | } 62 | } --------------------------------------------------------------------------------