├── .gitignore ├── src ├── util │ ├── utils.js │ └── printer.js ├── client │ ├── default-user-info.json │ ├── tasks │ │ ├── clock.js │ │ ├── dailytask.js │ │ ├── scheduledtask.js │ │ ├── weeklyschedule.js │ │ └── timeperiod.js │ ├── queue.js │ ├── bilibili-session.js │ ├── notifier.js │ ├── default-task-settings.json │ ├── receiver.js │ ├── account.js │ ├── connection.js │ └── account-runner.js ├── net │ ├── httperror.js │ ├── response.js │ ├── request.js │ └── xhr.js ├── settings.json ├── container │ └── queue.js ├── global │ └── config.js ├── task │ ├── task.js │ └── ratelimiter.js ├── main.js ├── bilibili │ └── bilibili-rest.js ├── server │ └── httphost.js └── bilibili.js ├── package.json ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | **/log*.txt 2 | **/*.swp 3 | **/node_modules 4 | **/package-lock.json 5 | **/test* 6 | **/*user*.json 7 | **/.nfs* 8 | **/client/*.json 9 | !src/client/default-task-settings.json 10 | !src/client/default-user-info.json 11 | -------------------------------------------------------------------------------- /src/util/utils.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | const sleep = (time) => { 6 | time = time > 0 ? time : 0; 7 | return new Promise(resolve => setTimeout(resolve, time)); 8 | }; 9 | 10 | module.exports = { 11 | sleep, 12 | }; 13 | 14 | })(); 15 | -------------------------------------------------------------------------------- /src/client/default-user-info.json: -------------------------------------------------------------------------------- 1 | { 2 | "user": { 3 | "username": "", 4 | "password": "" 5 | }, 6 | "app": { 7 | "access_token": "", 8 | "refresh_token": "" 9 | }, 10 | "web": { 11 | "bili_jct": "", 12 | "DedeUserID": "", 13 | "DedeUserID__ckMd5": "", 14 | "sid": "", 15 | "SESSDATA": "" 16 | } 17 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client-lottery", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "src/main.js", 6 | "scripts": {}, 7 | "keywords": [], 8 | "author": "", 9 | "license": "ISC", 10 | "dependencies": { 11 | "colors": "^1.4.0", 12 | "express": "^4.17.1", 13 | "ws": "^7.2.1" 14 | }, 15 | "scripts": { 16 | "start": "node src/main.js" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/net/httperror.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | class HttpError extends Error { 6 | 7 | constructor(...args) { 8 | super(...args); 9 | this.code = 'ERR_HTTP_CONN'; 10 | this.status = 0; 11 | } 12 | 13 | withStatus(httpStatus) { 14 | this.status = httpStatus; 15 | return this; 16 | } 17 | 18 | } 19 | 20 | module.exports = HttpError; 21 | 22 | })(); 23 | -------------------------------------------------------------------------------- /src/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "httpServer": { 3 | "host": "127.0.0.1", 4 | "port": 8899 5 | }, 6 | "wsServer": [ 7 | { 8 | "address": "ws://bili.minamiktr.com/ws" 9 | } 10 | ], 11 | "storm": { 12 | "rate": 0.6 13 | }, 14 | "receiver": { 15 | "janitorInterval": 1, 16 | "expiryThreshold": 5 17 | }, 18 | "notifier": { 19 | "heartbeatInterval": 5, 20 | "midnightCheckInterval": 5 21 | }, 22 | "account": { 23 | "maxRequestsPerSecond": 50, 24 | "maxNumRoomEntered": 30, 25 | "blacklistCheckInterval": 24, 26 | "stormJoinMaxInterval": 60, 27 | "abandonStormAfter": 25 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/util/printer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const colors = require('colors/safe'); 4 | 5 | function cprint(msg, color=colors.white) { 6 | 7 | const time = new Date(); 8 | const year = time.getFullYear(); 9 | const mon = time.getMonth() + 1; 10 | const date_raw = time.getDate(); 11 | const hr = time.getHours(); 12 | const min = time.getMinutes(); 13 | const sec = time.getSeconds(); 14 | const month = mon < 10 ? '0' + mon : mon; 15 | const date = date_raw < 10 ? '0' + date_raw : date_raw; 16 | const hour = hr < 10 ? '0' + hr : hr; 17 | const minute = min < 10 ? '0' + min : min; 18 | const second = sec < 10 ? '0' + sec : sec; 19 | console.log(color(` [${year}-${month}-${date} ${hour}:${minute}:${second}] ${msg}`)); 20 | } 21 | 22 | module.exports = cprint; 23 | -------------------------------------------------------------------------------- /src/client/tasks/clock.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | const chinaTimeMinutesOffset = 60 * 8; // China Standard Time is UTC+8 6 | 7 | class Clock extends Date { 8 | 9 | static today() { 10 | const today = new Clock(); 11 | today.setHours(0); 12 | today.setMinutes(0); 13 | today.setSeconds(0); 14 | today.setMilliseconds(0); 15 | return today; 16 | } 17 | 18 | constructor(...args) { 19 | super(...args); 20 | } 21 | 22 | getDayInChina() { 23 | // Return weekday in China Standard Time 24 | return new Date(this.valueOf() + (this.getTimezoneOffset() + chinaTimeMinutesOffset) * 60 * 1000).getDay(); 25 | } 26 | 27 | } 28 | 29 | module.exports = Clock; 30 | 31 | 32 | })(); 33 | -------------------------------------------------------------------------------- /src/client/tasks/dailytask.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | /** 6 | * Daily task has no bounds 7 | */ 8 | class DailyTask { 9 | 10 | constructor() { 11 | this.callback = null; 12 | } 13 | 14 | registerCallback(callback) { 15 | this.callback = callback; 16 | return this; 17 | } 18 | 19 | execute(...args) { 20 | return this.callback && this.callback(...args); 21 | } 22 | 23 | registered() { 24 | return !(this.callback === null); 25 | } 26 | 27 | json() { 28 | return { 29 | 'type': 'daily', 30 | 'status': (this.registered() ? 1 : 0), 31 | 'timeperiod': null, 32 | }; 33 | } 34 | 35 | } 36 | 37 | module.exports = DailyTask; 38 | 39 | })(); 40 | -------------------------------------------------------------------------------- /src/client/tasks/scheduledtask.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | const DailyTask = require('./dailytask.js'); 6 | 7 | class ScheduledTask extends DailyTask { 8 | 9 | constructor() { 10 | super(); 11 | this.weeklySchedule = null; 12 | } 13 | 14 | updateTimePeriod(weeklySchedule) { 15 | this.weeklySchedule = weeklySchedule; 16 | } 17 | 18 | execute(...args) { 19 | if (this.inBound()) { 20 | return super.execute(...args); 21 | } 22 | } 23 | 24 | inBound() { 25 | let result = false; 26 | if (this.weeklySchedule) { 27 | result = this.weeklySchedule.inBound(); 28 | } 29 | return result; 30 | } 31 | 32 | json() { 33 | let result = super.json(); 34 | result.type = 'scheduled'; 35 | result.timeperiod = this.weeklySchedule.json(); 36 | return result; 37 | } 38 | } 39 | 40 | module.exports = ScheduledTask; 41 | 42 | })(); 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2019] [kotoriのねこ] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/client/queue.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | const { sleep } = require('../util/utils.js'); 6 | 7 | class VirtualQueue { 8 | 9 | constructor(count=100, unit=1000) { 10 | this.MAX = count; 11 | this.slots = 0; 12 | 13 | this.running = true; 14 | this.slotWaiter = Promise.resolve(); 15 | this.resetWaiter = () => {}; 16 | this.slotWaiter = new Promise(resolve => { 17 | this.resetWaiter = resolve; 18 | }); 19 | 20 | (async () => { 21 | while (this.running) { 22 | await sleep(unit); 23 | this.resetWaiter(); 24 | this.slotWaiter = new Promise(resolve => { 25 | this.resetWaiter = resolve; 26 | }); 27 | this.slots = 0; 28 | } 29 | })(); 30 | } 31 | 32 | add() { 33 | return (async () => { 34 | while (this.slots >= this.MAX) { 35 | await this.slotWaiter; 36 | } 37 | ++this.slots; 38 | })(); 39 | } 40 | 41 | } 42 | 43 | module.exports = VirtualQueue; 44 | 45 | })(); 46 | -------------------------------------------------------------------------------- /src/container/queue.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | class LLNode { 6 | 7 | constructor(item) { 8 | this._next = null; 9 | this._item = item; 10 | } 11 | 12 | get next() { 13 | return this._next; 14 | } 15 | 16 | get value() { 17 | return this._item; 18 | } 19 | 20 | set next(n) { 21 | this._next = n; 22 | } 23 | 24 | set value(v) { 25 | this._item = v; 26 | } 27 | 28 | } 29 | 30 | 31 | class Queue { 32 | 33 | constructor() { 34 | this._size = 0; 35 | this._rear = null; 36 | } 37 | 38 | push(item) { 39 | const node = new LLNode(item); 40 | node.next = node; 41 | if (this._rear !== null) { 42 | node.next = this._rear.next; 43 | this._rear.next = node; 44 | } 45 | this._rear = node; 46 | ++this._size; 47 | return this; 48 | } 49 | 50 | pop() { 51 | let result = null; 52 | if (this._rear !== null && this._rear.next !== null) { 53 | result = this._rear.next.value; 54 | if (this._rear !== this._rear.next) { 55 | this._rear.next = this._rear.next.next; 56 | } 57 | else { 58 | this._rear = null; 59 | } 60 | --this._size; 61 | } 62 | return result; 63 | } 64 | 65 | front() { 66 | let result = null; 67 | if (this._rear !== null && this._rear.next !== null) { 68 | result = this._rear.next.value; 69 | } 70 | return result; 71 | } 72 | 73 | get length() { 74 | return this._size; 75 | } 76 | } 77 | 78 | module.exports = Queue; 79 | 80 | })(); 81 | -------------------------------------------------------------------------------- /src/client/bilibili-session.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | const querystring = require('querystring'); 6 | 7 | class AccountSession { 8 | 9 | static parse(colonSeparated) { 10 | return querystring.parse(colonSeparated, '; ', '='); 11 | } 12 | 13 | static stringify(jsonObject) { 14 | return querystring.stringify(jsonObject, '; ', '='); 15 | } 16 | 17 | /** 18 | * @params options Object 19 | * app Object 20 | * web Object 21 | */ 22 | constructor(options) { 23 | this.app = { 24 | 'access_token': '', 25 | 'refresh_token': '', 26 | }; 27 | this.web = { 28 | 'bili_jct': '', 29 | 'DedeUserID': '', 30 | 'DedeUserID__ckMd5': '', 31 | 'sid': '', 32 | 'SESSDATA': '', 33 | }; 34 | if (options) { 35 | const { web, app } = options; 36 | if (web) { 37 | Object.assign(this.web, web); 38 | } 39 | if (app) { 40 | Object.assign(this.app, app); 41 | } 42 | } 43 | this.webCookies = ''; 44 | } 45 | 46 | json() { 47 | const app = this.app; 48 | const web = this.web; 49 | return { app, web }; 50 | } 51 | 52 | isComplete() { 53 | return !!( 54 | this.web['bili_jct'] 55 | && this.web['DedeUserID'] 56 | && this.web['DedeUserID__ckMd5'] 57 | && this.web['sid'] 58 | && this.web['SESSDATA'] 59 | && this.app['access_token'] 60 | && this.app['refresh_token']); 61 | } 62 | 63 | } 64 | 65 | module.exports = AccountSession; 66 | 67 | })(); 68 | -------------------------------------------------------------------------------- /src/global/config.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | 6 | const settings = require('../settings.json'); 7 | 8 | const rand_hex = (len) => { 9 | const items = [ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' ]; 10 | const length = items.length; 11 | if (len === 0) return ''; 12 | 13 | let result = ''; 14 | for (let i = 0; i < len; ++i) { 15 | result = `${result}${items[Math.floor(Math.random()*length)]}`; 16 | } 17 | return result; 18 | }; 19 | 20 | const lh = '127.0.0.1'; 21 | const wsServer = settings.wsServer; 22 | 23 | const statistics = { 24 | 'appId': 1, 25 | 'platform': 3, 26 | 'version': '5.55.1', 27 | 'abtest': '', 28 | }; 29 | const appkey = '1d8b6e7d45233436'; 30 | const appSecret = '560c52ccd288fed045859ed18bffd973'; 31 | const appCommon = { 32 | 'appkey': appkey, 33 | 'build': 5551100, 34 | 'channel': 'bili', 35 | 'device': 'android', 36 | 'mobi_app': 'android', 37 | 'platform': 'android', 38 | 'statistics': JSON.stringify(statistics), 39 | }; 40 | const appHeaders = { 41 | 'Connection': 'close', 42 | 'Content-Type': 'application/x-www-form-urlencoded', 43 | 'User-Agent': 'Mozilla/5.0 BiliDroid/5.55.1 (bbcallen@gmail.com)', 44 | 'env': 'prod', 45 | 'APP-KEY': 'android', 46 | 'Buvid': `XZ${rand_hex(35)}`, 47 | }; 48 | const webHeaders = { 49 | 'Connection': 'close', 50 | 'Content-Type': 'application/x-www-form-urlencoded', 51 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.113 Safari/537.36', 52 | }; 53 | 54 | const error = { 55 | 'count': 0, 56 | }; 57 | 58 | module.exports = { 59 | appCommon, 60 | appHeaders, 61 | appSecret, 62 | webHeaders, 63 | error, 64 | }; 65 | 66 | })(); 67 | -------------------------------------------------------------------------------- /src/task/task.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | class AbstractTask { 6 | 7 | constructor() { 8 | this._time = 0; 9 | this._callback = () => {}; 10 | } 11 | 12 | start() {} 13 | 14 | stop() {} 15 | 16 | get running() { return false } 17 | 18 | get time() { 19 | return this._time; 20 | } 21 | 22 | withCallback(callback, ...args) { 23 | this._callback = callback; 24 | this._args = args; 25 | return this; 26 | } 27 | 28 | withTime(ms) { 29 | ms = ms > 0 ? ms : 0; 30 | this._time = ms; 31 | return this; 32 | } 33 | } 34 | 35 | class RecurrentTask extends AbstractTask { 36 | 37 | constructor() { 38 | super(); 39 | this._stopper = null; 40 | } 41 | 42 | get running() { 43 | return this._stopper !== null; 44 | } 45 | 46 | start() { 47 | if (this._stopper === null) { 48 | this._stopper = setInterval(this._callback, this.time, ...this._args); 49 | } 50 | } 51 | 52 | stop() { 53 | if (this._stopper !== null) { 54 | clearInterval(this._stopper); 55 | this._stopper = null; 56 | } 57 | } 58 | } 59 | 60 | class DelayedTask extends AbstractTask { 61 | 62 | constructor() { 63 | super(); 64 | this._stopper = null; 65 | } 66 | 67 | get running() { 68 | return this._stopper !== null; 69 | } 70 | 71 | start() { 72 | if (this._stopper === null) { 73 | this._stopper = setTimeout(() => { 74 | this._stopper = null; 75 | this._callback(...this._args); 76 | }, this.time); 77 | } 78 | } 79 | 80 | stop() { 81 | if (this._stopper !== null) { 82 | clearTimeout(this._stopper); 83 | this._stopper = null; 84 | } 85 | } 86 | } 87 | 88 | module.exports = { 89 | DelayedTask, 90 | RecurrentTask, 91 | }; 92 | 93 | })(); 94 | -------------------------------------------------------------------------------- /src/client/notifier.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | const Clock = require('./tasks/clock.js'); 6 | const EventEmitter = require('events').EventEmitter; 7 | 8 | class Notifier extends EventEmitter { 9 | 10 | constructor(options) { 11 | super(); 12 | 13 | this.tasks = { 14 | 'liveHeart': null, 15 | 'midnight': null, 16 | }; 17 | this.day = new Clock().getDayInChina(); 18 | this.heartbeatInterval = 1000 * 60 * 5; // By default, send heartbeat every 5 minutes. 19 | this.midnightCheckInterval = 1000 * 60 * 60; // By default, check for midnight every 60 minutes. 20 | 21 | if (options) { 22 | if (options.hasOwnProperty('heartbeatInterval')) { 23 | this.heartbeatInterval = options.heartbeatInterval * 1000 * 60; // heartbeatInterval is in minutes 24 | } 25 | if (options.hasOwnProperty('midnightCheckInterval')) { 26 | this.midnightCheckInterval = options.midnightCheckInterval * 1000 * 60; // midnightCheckInterval is in minutes 27 | } 28 | } 29 | } 30 | 31 | run() { 32 | if (this.tasks['liveHeart'] === null) { 33 | this.tasks['liveHeart'] = setInterval(() => { 34 | this.emit('liveHeart'); 35 | }, this.heartbeatInterval); 36 | } 37 | if (this.tasks['midnight'] === null) { 38 | this.tasks['midnight'] = setInterval(() => { 39 | const day = new Clock().getDayInChina(); 40 | if (this.day !== day) { 41 | this.emit('midnight'); 42 | this.day = day; 43 | } 44 | }, this.midnightCheckInterval); 45 | } 46 | } 47 | 48 | stop() { 49 | Object.keys(this.tasks).forEach(taskname => { 50 | if (this.tasks[taskname] !== null) { 51 | clearInterval(this.tasks[taskname]); 52 | this.tasks[taskname] = null; 53 | } 54 | }); 55 | } 56 | 57 | } 58 | 59 | module.exports = Notifier; 60 | 61 | })(); 62 | -------------------------------------------------------------------------------- /src/client/tasks/weeklyschedule.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | const Clock = require('./clock.js'); 6 | const TimePeriod = require('./timeperiod.js'); 7 | 8 | const chinaTimeMinutesOffset = 60 * 8; // China Standard Time is UTC+8 9 | 10 | class WeeklySchedule { 11 | 12 | constructor(periods) { 13 | const midnight = { hours: 0, minutes: 0 }; 14 | this.originalPeriods = periods; 15 | this.timePeriods = [ [], [], [], [], [], [], [] ]; 16 | periods.forEach(period => { 17 | let currentDayTimePeriod = null; 18 | let nextDayTimePeriod = null; 19 | if (period.from.hours < period.to.hours || (period.from.hours === period.to.hours && period.from.minutes <= period.to.minutes)) { 20 | currentDayTimePeriod = new TimePeriod(period.from, period.to); 21 | } else { 22 | currentDayTimePeriod = new TimePeriod(period.from, midnight); 23 | nextDayTimePeriod = new TimePeriod(midnight, period.to); 24 | } 25 | 26 | (period.weekdays || '0-6').split(',').forEach(dayRange => { 27 | let current = 0; 28 | let end = 0; 29 | if (dayRange.includes('-')) { 30 | let days = dayRange.split('-'); 31 | current = parseInt(days[0]); 32 | end = parseInt(days[1]); 33 | } else { 34 | current = parseInt(dayRange); 35 | end = current; 36 | } 37 | 38 | while (current <= end) { 39 | this.timePeriods[current++].push(currentDayTimePeriod); 40 | if (nextDayTimePeriod != null) { 41 | this.timePeriods[current == 7 ? 0 : current].push(nextDayTimePeriod); 42 | } 43 | } 44 | }); 45 | }); 46 | } 47 | 48 | inBound(time) { 49 | return this.timePeriods[new Clock().getDayInChina()].some(timeperiod => timeperiod.inBound()); 50 | } 51 | 52 | json() { 53 | return this.originalPeriods; 54 | } 55 | } 56 | 57 | module.exports = WeeklySchedule; 58 | 59 | })(); 60 | -------------------------------------------------------------------------------- /src/task/ratelimiter.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | const colors = require('colors/safe'); 6 | const Queue = require('../container/queue.js'); 7 | const { cprint } = require('../util/printer.js'); 8 | const { DelayedTask } = require('./task.js'); 9 | 10 | class RateLimiter { 11 | 12 | constructor(count, milliseconds) { 13 | milliseconds = milliseconds || 0; 14 | this._interval = 1000; 15 | this._limit = Infinity; 16 | this._dispatched = 0; 17 | this._refreshTask = new DelayedTask(); 18 | this._refreshTask.withTime(this._interval).withCallback(() => { 19 | this._dispatched = 0; 20 | this.dispatch(); 21 | if (this._queue.length > 0) { 22 | this._refreshTask.start(); 23 | } 24 | }); 25 | this._running = false; 26 | this._queue = new Queue(); 27 | 28 | if (Number.isInteger(count)) { 29 | count = count > 0 ? count : 0; 30 | if (Number.isInteger(milliseconds) === false) { 31 | milliseconds = this._interval; 32 | } 33 | milliseconds = milliseconds > 0 ? milliseconds : 1; 34 | const rate = this._interval / milliseconds; 35 | this._limit = Math.round(rate * count); 36 | } 37 | } 38 | 39 | add(task) { 40 | this._queue.push(task); 41 | this._refreshTask.start(); 42 | this.dispatch(); 43 | } 44 | 45 | dispatch() { 46 | while (this._dispatched < this._limit && this._queue.length > 0) { 47 | const task = this._queue.pop(); 48 | try { 49 | task && task(); 50 | } 51 | catch (error) { 52 | // TODO: turn this into EventEmitter and emit error? 53 | cprint(`(RateLimiter) - ${error.message}`, colors.red); 54 | } 55 | ++this._dispatched; 56 | } 57 | } 58 | 59 | start() { 60 | if (this._running === false) { 61 | this._running = true; 62 | this._refreshTask.start(); 63 | this.dispatch(); 64 | } 65 | } 66 | 67 | stop() { 68 | if (this._running === true) { 69 | this._refreshTask.stop(); 70 | this._running = false; 71 | } 72 | } 73 | } 74 | 75 | module.exports = RateLimiter; 76 | 77 | })(); 78 | -------------------------------------------------------------------------------- /src/client/tasks/timeperiod.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | const Clock = require('./clock.js'); 6 | 7 | const oneDay = 1000 * 60 * 60 * 24; 8 | const chinaTimeMinutesOffset = 60 * 8; // China Standard Time is UTC+8 9 | 10 | class TimePeriod { 11 | 12 | /** 13 | * @params from { hours: number, minutes: number } Beginning 14 | * @params to { hours: number, minutes: number } End 15 | */ 16 | constructor(from, to) { 17 | this.start = this.end = null; 18 | 19 | if (from) { 20 | this.start = (from.hours * 60 + from.minutes - chinaTimeMinutesOffset) * 60 * 1000; 21 | if (this.start < 0) { 22 | this.start += oneDay; 23 | } 24 | } 25 | 26 | if (to) { 27 | this.end = (to.hours * 60 + to.minutes - chinaTimeMinutesOffset) * 60 * 1000; 28 | if (this.end < 0) { 29 | this.end += oneDay; 30 | } 31 | } 32 | 33 | Object.freeze(this); 34 | } 35 | 36 | inBound(time) { 37 | let result = true; 38 | 39 | if (!time) time = new Clock(); 40 | 41 | if (this.start !== null && this.end !== null) { 42 | // Ex. 17:00 - 5:00 (18:00 - true) 43 | const start = this.start; 44 | const end = this.end; 45 | const t = time % oneDay; 46 | if (start < end) 47 | result = (start <= t && t < end); 48 | else 49 | result = (start <= t || t < end); 50 | } 51 | return result; 52 | } 53 | 54 | json() { 55 | let tp = null; 56 | if (this.start !== null && this.end !== null) { 57 | const from = new Date(this.start); 58 | const from_hours = from.getHours(); 59 | const from_minutes = from.getMinutes(); 60 | const to = new Date(this.end); 61 | const to_hours = to.getHours(); 62 | const to_minutes = to.getMinutes(); 63 | tp = { 64 | 'from': { 65 | 'hours': from_hours, 66 | 'minutes': from_minutes, 67 | }, 68 | 'to': { 69 | 'hours': to_hours, 70 | 'minutes': to_minutes, 71 | } 72 | }; 73 | } 74 | return tp; 75 | } 76 | 77 | } 78 | 79 | module.exports = TimePeriod; 80 | 81 | })(); 82 | -------------------------------------------------------------------------------- /src/client/default-task-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "taskschedules": { 3 | "normal": [ 4 | { 5 | "from": { 6 | "hours": 8, 7 | "minutes": 0 8 | }, 9 | "to": { 10 | "hours": 0, 11 | "minutes": 45 12 | }, 13 | "weekdays": "1-4" 14 | }, 15 | { 16 | "from": { 17 | "hours": 8, 18 | "minutes": 0 19 | }, 20 | "to": { 21 | "hours": 2, 22 | "minutes": 0 23 | }, 24 | "weekdays": "5" 25 | }, 26 | { 27 | "from": { 28 | "hours": 9, 29 | "minutes": 0 30 | }, 31 | "to": { 32 | "hours": 2, 33 | "minutes": 0 34 | }, 35 | "weekdays": "6" 36 | }, 37 | { 38 | "from": { 39 | "hours": 9, 40 | "minutes": 0 41 | }, 42 | "to": { 43 | "hours": 0, 44 | "minutes": 45 45 | }, 46 | "weekdays": "0" 47 | } 48 | ], 49 | "always": [ 50 | { 51 | "from": { 52 | "hours": 0, 53 | "minutes": 0 54 | }, 55 | "to": { 56 | "hours": 23, 57 | "minutes": 59 58 | }, 59 | "weekdays": "0-6" 60 | } 61 | ] 62 | }, 63 | "tasks": { 64 | "pk": { 65 | "type": "scheduled", 66 | "enabled": true, 67 | "schedule": "normal" 68 | }, 69 | "gift": { 70 | "type": "scheduled", 71 | "enabled": true, 72 | "schedule": "normal" 73 | }, 74 | "guard": { 75 | "type": "scheduled", 76 | "enabled": true, 77 | "schedule": "normal" 78 | }, 79 | "storm": { 80 | "type": "scheduled", 81 | "enabled": true, 82 | "schedule": "normal" 83 | }, 84 | "liveheart": { 85 | "type": "scheduled", 86 | "enabled": true, 87 | "schedule": "always" 88 | }, 89 | "livesign": { 90 | "type": "daily", 91 | "enabled": true 92 | }, 93 | "idolclubsign": { 94 | "type": "daily", 95 | "enabled": true 96 | }, 97 | "mainsharevideo": { 98 | "type": "daily", 99 | "enabled": true 100 | }, 101 | "mainwatchvideo": { 102 | "type": "daily", 103 | "enabled": true 104 | }, 105 | "silverbox": { 106 | "type": "daily", 107 | "enabled": true 108 | }, 109 | "doublewatch": { 110 | "type": "daily", 111 | "enabled": true 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/net/response.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | const querystring = require('querystring'); 6 | const cprint = require('../util/printer.js'); 7 | const colors = require('colors/safe'); 8 | 9 | class Response { 10 | 11 | constructor(options) { 12 | const { 13 | url, 14 | status_code, 15 | status_message, 16 | method, 17 | contentType, 18 | headers, 19 | data, } = options; 20 | Object.assign(this, options); 21 | Object.freeze(this); 22 | } 23 | 24 | isOk() { 25 | return this.status_code === 200; 26 | } 27 | 28 | json() { 29 | return JSON.parse(this.text()); 30 | } 31 | 32 | cookies() { 33 | let cookies = {}; 34 | if (this.headers) { 35 | const setCookie = this.headers['set-cookie']; 36 | setCookie && setCookie.forEach(c => { 37 | try { 38 | const cookieJar = c.split('; '); 39 | const cookieItem = cookieJar[0].split('='); 40 | cookies[cookieItem[0]] = cookieItem[1]; 41 | } catch (error) { 42 | cprint(`\n${error.stack}`, colors.red); 43 | } 44 | }); 45 | } 46 | return cookies; 47 | } 48 | 49 | text() { 50 | let data = this.data; 51 | if (this.data instanceof Buffer) { 52 | data = this.data.toString(); 53 | } 54 | return data; 55 | } 56 | 57 | } 58 | 59 | class ResponseBuilder { 60 | 61 | static start() { 62 | return new ResponseBuilder(); 63 | } 64 | 65 | withHttpResponse(httpIncomingMessage) { 66 | this.headers = httpIncomingMessage.headers; 67 | this.status_code = httpIncomingMessage.statusCode; 68 | this.status_message = httpIncomingMessage.statusMessage; 69 | this.contentType = (this.headers && this.headers['content-type']) || ''; 70 | return this; 71 | } 72 | 73 | withUrl(url) { 74 | this.url = url; 75 | return this; 76 | } 77 | 78 | withStatusCode(status_code) { 79 | this.status_code = status_code; 80 | return this; 81 | } 82 | 83 | withStatusMessage(status_message) { 84 | this.status_message = status_message; 85 | return this; 86 | } 87 | 88 | withMethod(method) { 89 | this.method = method; 90 | return this; 91 | } 92 | 93 | withData(data) { 94 | this.data = data; 95 | return this; 96 | } 97 | 98 | withContentType(contentType) { 99 | this.contentType = contentType; 100 | return this; 101 | } 102 | 103 | withHeaders(headers) { 104 | this.headers = headers; 105 | return this; 106 | } 107 | 108 | build() { 109 | this.url = this.url || ''; 110 | this.method = this.method || ''; 111 | this.headers = this.headers || {}; 112 | this.status_code = this.status_code || 500; 113 | this.status_message = this.status_message || ''; 114 | this.contentType = this.contentType || ''; 115 | this.data = this.data || Buffer.alloc(0); 116 | return new Response(this); 117 | } 118 | } 119 | 120 | module.exports = ResponseBuilder; 121 | 122 | })(); 123 | -------------------------------------------------------------------------------- /src/client/receiver.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | const EventEmitter = require('events').EventEmitter; 6 | const http = require('http'); 7 | 8 | const cprint = require('../util/printer.js'); 9 | const colors = require('colors/safe'); 10 | 11 | const Connection = require('./connection.js'); 12 | 13 | const namedGifts = [ 14 | 'guard', 15 | 'storm', 16 | 'pk', 17 | 'gift', 18 | 'anchor' 19 | ]; 20 | 21 | class RaffleReceiver extends EventEmitter { 22 | 23 | constructor(servers, options) { 24 | super(); 25 | 26 | this.connections = []; 27 | if (servers && servers.length > 0) { 28 | servers.forEach(server => this.initServerConnection(server)); 29 | } else { 30 | this.initServerConnection({ 31 | 'host': '127.0.0.1', 32 | 'port': 8999 33 | }); 34 | } 35 | } 36 | 37 | initServerConnection(server) { 38 | let connection = new Connection(server); 39 | connection.on('message', (gift) => this.broadcast(namedGifts.includes(gift.category) ? gift.category : '', gift)); 40 | this.connections.push(connection); 41 | } 42 | 43 | run() { 44 | this.connections.forEach(connection => connection.connect()); 45 | } 46 | 47 | broadcast(eventName, gift) { 48 | cprint( 49 | `${gift['id'].toString().padEnd(16)}` 50 | + `@${gift['roomid'].toString().padEnd(15)}` 51 | + `${gift['type'].padEnd(16)}` 52 | + `${gift['name']}`, 53 | colors.cyan 54 | ); 55 | 56 | this.emit(eventName, gift); 57 | } 58 | 59 | } 60 | 61 | class MultiServerRaffleReceiver extends RaffleReceiver { 62 | 63 | constructor(servers, options) { 64 | super(servers, options); 65 | 66 | this.janitor = null; 67 | this.janitorInterval = 1000 * 60; // By default, clean up evey minute 68 | this.expiryThreshold = 60 * 5; // By default, clean up any expired gift 5 minutes after its expiry 69 | 70 | if (options) { 71 | if (options.hasOwnProperty('janitorInterval')) { 72 | this.janitorInterval = options.janitorInterval * 1000 * 60; // janitorInterval is in minutes 73 | } 74 | if (options.hasOwnProperty('expiryThreshold')) { 75 | this.expiryThreshold = options.expiryThreshold * 60; // expiryThreshold is in minutes 76 | } 77 | } 78 | } 79 | 80 | run() { 81 | super.run(); 82 | this.receivedGifts = new Map(); 83 | namedGifts.forEach(name => { 84 | this.receivedGifts.set(name, new Map()); 85 | }); 86 | this.connections.forEach(connection => connection.connect()); 87 | 88 | if (this.janitor === null) { 89 | this.janitor = setInterval(() => { 90 | const threshold = new Date().valueOf() / 1000 - this.expiryThreshold; 91 | namedGifts.forEach(name => { 92 | const gifts = this.receivedGifts.get(name); 93 | for (const [id, gift] of gifts) { 94 | if (gift.expireAt < threshold) { 95 | gifts.delete(id); 96 | } 97 | } 98 | }); 99 | }, this.janitorInterval); 100 | } 101 | } 102 | 103 | broadcast(eventName, gift) { 104 | const gifts = this.receivedGifts.get(eventName); 105 | if (gifts && !gifts.has(gift.id)) { 106 | gifts.set(gift.id, gift); 107 | super.broadcast(eventName, gift); 108 | } 109 | } 110 | 111 | } 112 | 113 | module.exports = { 114 | RaffleReceiver, 115 | MultiServerRaffleReceiver, 116 | }; 117 | 118 | })(); 119 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | const http = require('http'); 6 | 7 | const cprint = require('./util/printer.js'); 8 | const colors = require('colors/safe'); 9 | 10 | const settings = require('./settings.json'); 11 | const Account = require('./client/account-runner.js'); 12 | const { RaffleReceiver, MultiServerRaffleReceiver } = require('./client/receiver.js'); 13 | const Notifier = require('./client/notifier.js'); 14 | 15 | main(); 16 | 17 | 18 | function main() { 19 | 20 | const receiver = settings.wsServer.length > 1 21 | ? new MultiServerRaffleReceiver(settings.wsServer, settings.receiver) 22 | : new RaffleReceiver(settings.wsServer, settings.receiver); 23 | const notifier = new Notifier(settings.notifier); 24 | 25 | const account = new Account('user.json', settings.account); 26 | account.loadFromFile(); 27 | 28 | setupSchedule({ receiver, notifier, account }); 29 | 30 | /** 读取/登录 */ 31 | try { 32 | if (account.usable === false) { 33 | cprint('Account not usable', colors.yellow); 34 | cprint('-------------账号登录中---------------', colors.yellow); 35 | account.login().then(() => { 36 | cprint('Login successful', colors.green); 37 | 38 | account.loadTasksFromFile(); 39 | executeInitial(account); 40 | }).catch(error => { 41 | cprint(error, colors.red); 42 | }); 43 | } else { 44 | cprint('Account usable', colors.green); 45 | 46 | account.loadTasksFromFile(); 47 | executeInitial(account); 48 | } 49 | 50 | } catch (error) { 51 | console.log(error); 52 | } 53 | // */ 54 | } 55 | 56 | 57 | function setupSchedule(info) { 58 | if (typeof info === 'undefined' || info === null) { 59 | throw new Error('Schedule failed to setup'); 60 | } 61 | 62 | const { account, receiver, notifier } = info; 63 | 64 | (receiver 65 | .on('pk', (g) => account.execute('pk', g)) 66 | .on('gift', (g) => account.execute('gift', g)) 67 | .on('guard', (g) => account.execute('guard', g)) 68 | .on('storm', (g) => { 69 | if (Math.random() <= settings['storm']['rate']) { 70 | account.execute('storm', g); 71 | } 72 | })); 73 | 74 | (notifier 75 | .on('liveHeart', () => account.execute('liveheart')) 76 | .on('midnight', () => { 77 | account.execute('livesign'); 78 | account.execute('idolclubsign'); 79 | account.execute('mainsharevideo'); 80 | account.execute('mainwatchvideo'); 81 | account.execute('doublewatch'); 82 | account.execute('silverbox'); 83 | })); 84 | 85 | receiver.run(); 86 | notifier.run(); 87 | } 88 | 89 | 90 | function registerTasks(account) { 91 | const tp = [{from: { hours: 8, minutes: 0 }, to: { hours: 0, minutes: 45 }}]; 92 | 93 | account.register('pk', { 'timeperiod': tp }); 94 | account.register('gift', { 'timeperiod': tp }); 95 | account.register('guard', { 'timeperiod': tp }); 96 | account.register('storm', { 'timeperiod': tp }); 97 | account.register('liveheart', { 'timeperiod': tp }); 98 | account.register('mainsharevideo'); 99 | account.register('mainwatchvideo'); 100 | account.register('livesign'); 101 | account.register('idolclubsign'); 102 | account.register('doublewatch'); 103 | account.register('silverbox'); 104 | } 105 | 106 | 107 | function executeInitial(account) { 108 | account.refreshToken(); 109 | (async () => { 110 | account.execute('livesign'); 111 | account.execute('idolclubsign'); 112 | account.execute('mainsharevideo'); 113 | account.execute('mainwatchvideo'); 114 | account.execute('doublewatch'); 115 | account.execute('liveheart'); 116 | account.execute('silverbox'); 117 | })(); 118 | } 119 | 120 | })(); 121 | -------------------------------------------------------------------------------- /src/net/request.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | 6 | const querystring = require('querystring'); 7 | 8 | 9 | class Request { 10 | 11 | constructor(options) { 12 | /** 13 | const { 14 | host, 15 | path, 16 | port, 17 | https, 18 | cookies, 19 | method, 20 | params, 21 | data, 22 | headers, 23 | contentType, } = options; 24 | // */ 25 | Object.assign(this, options); 26 | Object.freeze(this); 27 | } 28 | 29 | toHttpOptions() { 30 | const options = { 31 | host: this.host, 32 | port: this.port, 33 | path: this.path, 34 | method: this.method, 35 | headers: this.headers, 36 | }; 37 | return options; 38 | } 39 | 40 | } 41 | 42 | class RequestBuilder { 43 | 44 | static formatParams(params) { 45 | const formattedParams = querystring.stringify(params, '&', '='); 46 | return formattedParams; 47 | } 48 | 49 | static formatCookies(cookies) { 50 | const options = { 51 | 'encodeURIComponent': querystring.unescape, 52 | }; 53 | const formattedCookies = querystring.stringify(cookies, '; ', '=', options); 54 | return formattedCookies; 55 | } 56 | 57 | static start() { 58 | return new RequestBuilder(); 59 | } 60 | 61 | withHost(host) { 62 | this.host = host; 63 | return this; 64 | } 65 | 66 | withPath(path) { 67 | this.path = path; 68 | return this; 69 | } 70 | 71 | withPort(port) { 72 | this.port = +port; 73 | return this; 74 | } 75 | 76 | withMethod(method) { 77 | this.method = method.toUpperCase(); 78 | return this; 79 | } 80 | 81 | withHttps() { 82 | this.https = true; 83 | return this; 84 | } 85 | 86 | withCookies(cookies) { 87 | if (typeof cookies !== 'string' && cookies instanceof String === false) { 88 | cookies = RequestBuilder.formatCookies(cookies); 89 | } 90 | this.cookies = cookies; 91 | return this; 92 | } 93 | 94 | withData(data) { 95 | if (typeof data !== 'string' && data instanceof String === false) { 96 | data = RequestBuilder.formatParams(sort(data)); 97 | } 98 | this.data = data; 99 | return this; 100 | } 101 | 102 | withParams(params) { 103 | if (typeof params !== 'string' && params instanceof String === false) { 104 | params = RequestBuilder.formatParams(sort(params)); 105 | } 106 | this.params = params; 107 | return this; 108 | } 109 | 110 | withHeaders(headers) { 111 | this.headers = headers; 112 | return this; 113 | } 114 | 115 | withContentType(contentType) { 116 | this.contentType = contentType; 117 | return this; 118 | } 119 | 120 | build() { 121 | this.host = this.host || 'localhost'; 122 | this.https = this.https === true; 123 | this.params = this.params || ''; 124 | this.path = this.path || ''; 125 | this.path = (this.params !== '' ? `${this.path}?${this.params}` : this.path); 126 | this.method = this.method || 'GET'; 127 | this.data = this.data || ''; 128 | this.headers = this.headers || { 'Connection': 'close' }; 129 | this.headers['Host'] = this.host; 130 | this.contentType = this.contentType || 'application/x-www-form-urlencoded'; 131 | if (this.cookies) { 132 | const headers = { 'Cookie': this.cookies }; 133 | Object.assign(headers, this.headers); 134 | this.headers = headers; 135 | } 136 | return new Request(this); 137 | } 138 | } 139 | 140 | const sort = (object) => { 141 | const sorted = {}; 142 | Object.keys(object).sort().forEach(key => { 143 | sorted[key] = object[key]; 144 | }); 145 | return sorted; 146 | }; 147 | 148 | module.exports = RequestBuilder; 149 | 150 | })(); 151 | -------------------------------------------------------------------------------- /src/client/account.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | 8 | const cprint = require('../util/printer.js'); 9 | const colors = require('colors/safe'); 10 | 11 | const Bilibili = require('../bilibili.js'); 12 | const AccountSession = require('./bilibili-session.js'); 13 | 14 | class Account { 15 | 16 | constructor(filename) { 17 | this.filename = filename || 'user.json'; 18 | this.username = ''; 19 | this.password = ''; 20 | this.session = new AccountSession(); 21 | this.usable = false; 22 | this.bind(); 23 | 24 | this.loadFromFile(); 25 | } 26 | 27 | bind() { 28 | this.storeSession = this.storeSession.bind(this); 29 | this.checkLoginCode = this.checkLoginCode.bind(this); 30 | this.saveToFile = this.saveToFile.bind(this); 31 | } 32 | 33 | updateLoginInfo(username, password) { 34 | let result = null; 35 | 36 | if (username && password) { 37 | this.username = username; 38 | this.password = password; 39 | result = this.login(); 40 | } else { 41 | result = Promise.reject(); 42 | } 43 | 44 | return Promise.resolve(result); 45 | } 46 | 47 | loadFromFile() { 48 | if (!!this.filename === true) { 49 | let filename = path.resolve(__dirname, this.filename); 50 | if (fs.existsSync(filename) === false) { 51 | filename = path.resolve(__dirname, 'default-user-info.json'); 52 | } 53 | const str = fs.readFileSync(filename); 54 | const data = JSON.parse(str); 55 | const user = data.user; 56 | if (user.username) this.username = user.username; 57 | if (user.password) this.password = user.password; 58 | this.session = new AccountSession(data); 59 | if (this.session.isComplete()) { 60 | this.usable = true; 61 | } else { 62 | this.usable = false; 63 | } 64 | } 65 | } 66 | 67 | login(forceLogin = false) { 68 | 69 | let result = null; 70 | 71 | if (this.session.isComplete() === false || forceLogin) { 72 | // 无可用cookies/tokens, login. 73 | 74 | this.usable = false; 75 | if (!!(this.username && this.password)) { 76 | // 已提供username和password, login 77 | 78 | cprint(`User ${this.username} logging in...`, colors.green); 79 | result = ( 80 | Bilibili.login(this.username, this.password) 81 | .then(this.checkLoginCode) 82 | .then(this.storeSession) 83 | .then(this.saveToFile) 84 | .then(() => this.usable = true)); 85 | } else { 86 | 87 | result = Promise.reject(`用户名/密码未提供 && cookies/tokens读取失败`); 88 | } 89 | } 90 | 91 | return Promise.resolve(result); 92 | } 93 | 94 | refreshToken() { 95 | 96 | Bilibili.refreshToken(this.session).then(resp => { 97 | const code = resp['code']; 98 | 99 | if (code === 0) { 100 | const data = resp['data']; 101 | 102 | const options = { 103 | 'web': this.session['web'], 104 | 'app': data, 105 | }; 106 | this.session = new AccountSession(options); 107 | this.saveToFile(); 108 | } 109 | }).catch(console.log); 110 | } 111 | 112 | checkLoginCode(resp) { 113 | let result = resp; 114 | if (resp) { 115 | const code = resp['code']; 116 | const msg = resp['msg'] || resp['message']; 117 | if (code !== 0) { 118 | result = Promise.reject(`${code} - ${msg}`); 119 | } 120 | } else { 121 | result = Promise.reject(`Login failed - reason unknown`); 122 | } 123 | return result; 124 | } 125 | 126 | storeSession(resp) { 127 | const data = resp['data']; 128 | const app = data['token_info']; 129 | const rawCookies = data['cookie_info']['cookies']; 130 | const web = {}; 131 | rawCookies.forEach(entry => { 132 | web[entry['name']] = entry['value']; 133 | }); 134 | const options = { app, web }; 135 | this.session = new AccountSession(options); 136 | } 137 | 138 | saveToFile() { 139 | const filename = ( 140 | (this.filename && path.resolve(__dirname, this.filename)) 141 | || path.resolve(__dirname, 'user.json')); 142 | 143 | cprint(`Storing login info to ${filename}`, colors.green); 144 | fs.writeFile(filename, this.toFileFormat(), (err) => { 145 | if (err) 146 | cprint(`Error storing user info to file`, colors.red); 147 | }); 148 | } 149 | 150 | info() { 151 | const result = {}; 152 | const username = this.username; 153 | const password = this.password; 154 | result['user'] = { username, password }; 155 | Object.assign(result, this.session.json()); 156 | return result; 157 | } 158 | 159 | toFileFormat() { 160 | return JSON.stringify(this.info(), null, 4); 161 | } 162 | 163 | } 164 | 165 | module.exports = Account; 166 | 167 | })(); 168 | -------------------------------------------------------------------------------- /src/bilibili/bilibili-rest.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | const colors = require('colors/safe'); 6 | 7 | // ------------------------------- Includes ------------------------------- 8 | const http = require('http'); 9 | const https = require('https'); 10 | const crypto = require('crypto'); 11 | const querystring = require('querystring'); 12 | const cprint = require('../util/printer.js'); 13 | const { 14 | appCommon, 15 | appSecret, 16 | appHeaders, 17 | webHeaders, } = require('../global/config.js'); 18 | const RateLimiter = require('../task/ratelimiter.js'); 19 | const Xhr = require('../net/xhr.js'); 20 | const RequestBuilder = require('../net/request.js'); 21 | 22 | 23 | /** Emits requests to the bilibili API */ 24 | class BilibiliRest { 25 | 26 | /** 27 | * Send request, gets json as response 28 | * 发送请求,获取json返回 29 | * 30 | * @param {Request} req - Request details 31 | * @returns {Promise} resolve(JSON) reject(Error) 32 | */ 33 | static request(req) { 34 | 35 | const acceptedCode = [ 200 ]; 36 | const noRetryCode = [ 412 ]; 37 | 38 | const requestUntilDone = async () => { 39 | 40 | let success = false; 41 | let tries = 3; 42 | let result = null; 43 | let err = null; 44 | 45 | while (success === false && tries > 0) { 46 | --tries; 47 | try { 48 | const response = await xhr.request(req); 49 | const statusCode = response.status_code; 50 | const statusMsg = response.status_message; 51 | 52 | if (acceptedCode.includes(statusCode)) { 53 | result = response.json(); 54 | err = null; 55 | success = true; 56 | } else if (noRetryCode.includes(statusCode)) { 57 | result = response; 58 | err = new Error(`Http status ${statusCode}: ${statusMessage}`); 59 | tries = 0; 60 | } else { 61 | err = new Error(`Http status ${statusCode}: ${statusMessage}`); 62 | } 63 | } catch (error) { 64 | err = error; 65 | cprint(`\n${error.stack}`, colors.red); 66 | } 67 | } 68 | 69 | if (err) { 70 | throw err; 71 | } else { 72 | return result; 73 | } 74 | }; 75 | 76 | return requestUntilDone(); 77 | } 78 | 79 | /** app端获取房间内抽奖信息 */ 80 | static appGetRaffleInRoom(roomid) { 81 | const params = {}; 82 | Object.assign(params, appCommon); 83 | params['roomid'] = roomid; 84 | params['ts'] = Number.parseInt(0.001 * new Date()); 85 | 86 | const request = (RequestBuilder.start() 87 | .withHost('api.live.bilibili.com') 88 | .withPath('/xlive/lottery-interface/v1/lottery/getLotteryInfo') 89 | .withMethod('GET') 90 | .withHeaders(appHeaders) 91 | .withParams(params) 92 | .build() 93 | ); 94 | 95 | return BilibiliRest.request(request); 96 | } 97 | 98 | /** Check for lottery in room ``roomid`` 99 | * 100 | */ 101 | static getRaffleInRoom(roomid) { 102 | const params = { 'roomid': roomid, }; 103 | const request = (RequestBuilder.start() 104 | .withHost('api.live.bilibili.com') 105 | .withPath('/xlive/lottery-interface/v1/lottery/Check') 106 | .withMethod('GET') 107 | .withHeaders(webHeaders) 108 | .withParams(params) 109 | .build() 110 | ); 111 | 112 | return BilibiliRest.request(request); 113 | } 114 | 115 | /** 查取视频cid */ 116 | static getVideoCid(aid) { 117 | const jsonp = 'jsonp'; 118 | const params = { 119 | aid, 120 | jsonp, 121 | }; 122 | const request = (RequestBuilder.start() 123 | .withHost('api.bilibili.com') 124 | .withPath('/x/player/pagelist') 125 | .withMethod('GET') 126 | .withHeaders(webHeaders) 127 | .withParams(params) 128 | .build() 129 | ); 130 | 131 | return Bilibili.request(request); 132 | } 133 | 134 | static appSign(string) { 135 | return crypto.createHash('md5').update(string+appSecret).digest('hex'); 136 | } 137 | 138 | static parseAppParams(params) { 139 | const pre_paramstr = BilibiliRest.formatForm(params); 140 | const sign = BilibiliRest.appSign(pre_paramstr); 141 | const paramstr = `${pre_paramstr}&sign=${sign}`; 142 | return paramstr; 143 | } 144 | 145 | static formatCookies(cookies) { 146 | const options = { 147 | 'encodeURIComponent': querystring.unescape, 148 | }; 149 | const formattedCookies = querystring.stringify(cookies, '; ', '=', options); 150 | return formattedCookies; 151 | } 152 | 153 | static formatForm(form) { 154 | const formattedForm = querystring.stringify(form, '&', '='); 155 | return formattedForm; 156 | } 157 | } 158 | 159 | 160 | module.exports = BilibiliRest; 161 | 162 | 163 | /** 164 | * Sort the properties according to alphabetical order 165 | */ 166 | const sort = (object) => { 167 | const sorted = Object.create(null); 168 | Object.keys(object).sort().forEach(key => { 169 | sorted[key] = object[key]; 170 | }); 171 | return sorted; 172 | }; 173 | 174 | const xhr = new Xhr(); 175 | xhr.withRateLimiter(new RateLimiter(30, 1000)); 176 | 177 | const decodeCookies = (cookiestr) => { 178 | if (typeof cookiestr === 'undefinded') return {}; 179 | const decodedCookies = querystring.parse(cookiestr, '; ', '='); 180 | delete decodedCookies['Domain']; 181 | delete decodedCookies['Expires']; 182 | delete decodedCookies['Path']; 183 | return decodedCookies; 184 | }; 185 | 186 | const extractCookies = (response) => { 187 | const setCookies = {}; 188 | const cookies = response.headers['set-cookie']; 189 | if (cookies) { 190 | cookies.forEach(cookiestr => { 191 | const c = decodeCookies(cookiestr); 192 | Object.assign(setCookies, c); 193 | }); 194 | } 195 | return setCookies; 196 | }; 197 | 198 | })(); 199 | 200 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bilibili-raffle-js (b站直播挂机、高能+舰长抽取) 2 | ![Github license](https://img.shields.io/badge/license-MIT-brightgreen) 3 | ![Github nodejs](https://img.shields.io/badge/nodejs-10.16.3-blue) 4 | 5 | ## Info 6 | - 此程序不收集任何用户信息/数据 7 | - 可自建服务器[bilibili-live-monitor-ts](https://github.com/Billyzou0741326/bilibili-live-monitor-ts), 也可以用默认的设定 8 | - 包括[bilibili-live-monitor-ts](https://github.com/Billyzou0741326/bilibili-live-monitor-ts)在内的所有项目永久开源 没有闭源的打算 亦不接受打赏 9 | - 实力不济没能写出更多功能 代码也非常难看23333 10 | - 有bug请务必反馈 Issues就是用来讨论的 11 | - 祝各位白嫖愉快~ 12 | 13 | ## Features 14 | - 主站观看视频 (mainwatchvideo) 15 | - 主站分享视频 (mainsharevideo) 仅模拟 不会真实分享 16 | - 直播签到 (livesign) 17 | - 直播心跳 (liveheart) 18 | - 直播抽奖 (guard, gift, pk) 19 | - 双端观看 (doublewatch) 20 | - 银瓜子领取 (silverbox) 21 | - 友爱社签到 (idolclubsign) 22 | 23 | ## Usage (普通运行) 24 | 1. **src/client/default-user-info.json** 填写用户名/密码 (这个版本用到了app端的access_tokens,所以不能只提供cookies) 25 | 2. `npm install` 安装依赖库 26 | 2. `node src/main.js` 运行 27 | 3. 程序自带抽奖休眠(见[任务设置](#任务设置)) 可以用pm2永久运行程序 28 | 29 | ## pm2运行 30 | 1. `npm install` 安装依赖库 31 | 2. `npm install -g pm2` 全局安装PM2 32 | 3. `pm2 start src/main.js` 运行程序 33 | 4. `pm2 ls` 查看运行状态 名为main那个就是了 (如果状态是errored 用下一步看下日志 反馈issues) 34 | 5. `pm2 logs main --lines 100` 查看100行日志 (**CTRL-C 退出日志状态**) 35 | 6. `pm2 stop main; pm2 delete main` 停止+删除程序进程 36 | 37 | ## Config 38 | 39 | ### 监听server与http任务管理设置 40 | 41 | - **Config file `/src/settings.json`** 42 | - httpServer为账号/任务管理界面设定 (未实现 我太菜了) 43 | - wsServer为舰长+抽奖服务器设定,支持多服务器,每个服务器可单独设置可选参数,例如下示的备用服务器设置可以用不同颜色显示,并且只显示连接和断线信息 44 | - receiver为抽奖监听设定 45 | - notifier为定时任务通知设定 46 | - account为一些账号任务运行杂项设置,包括每秒最大请求数,已进入房间追踪数组最大长度,在访问被拒绝的情况下小黑屋检查间隔时间(单位为小时) 47 | 48 | ```javascript 49 | { 50 | "httpServer": { 51 | "host": "127.0.0.1", 52 | "port": 8899 53 | }, 54 | "wsServer": [ 55 | { 56 | "host": "warpgate.cat.pdx.edu", // 如果自建服务器的话请务必换成自己的ip 本机的话是127.0.0.1 57 | "port": 8999 58 | }, 59 | { 60 | "host": "127.0.0.1", 61 | "port": 8999, 62 | "retries": 1, // 【可选设置】断线后短时间内重试次数。如果短时间内重试失败会等待一段时间后尝试重新连接。默认值为3 63 | "reconnectWaitTime": 60, // 【可选设置】断线后重新连接等待时长(单位为秒)。默认值为10秒 64 | "healthCheckInterval": 10, // 【可选设置】断线检测间隔(单位为秒)。默认值为5秒 65 | "healthCheckThreshold": 30, // 【可选设置】断线检测阈值,从最后一次接收到ping请求算起(单位为秒)。默认值为25秒 66 | "enableConnectionErrorLogging": false, // 【可选设置】是否显示连接错误信息(如果备用服务器不是一直在线的话建议关闭)。默认值为true 67 | "enableConnectionRetryLogging": false, // 【可选设置】是否显示连接重试信息(如果备用服务器不是一直在线的话建议关闭)。默认值为true 68 | "infoColor": "white", // 【可选设置】普通连接信息显示颜色。默认值为green(绿色) 69 | "errorColor": "yellow" // 【可选设置】连接错误信息显示颜色。默认值为red(红色) 70 | } 71 | ], 72 | "storm": { 73 | "rate": 0.6 // 风暴参与几率 (0.6 == 60%) 74 | }, 75 | "receiver": { 76 | "janitorInterval": 1, // 【可选设置】已参与抽奖的礼物ID缓存清理间隔(单位为分)。默认值为1分钟 77 | "expiryThreshold": 5 // 【可选设置】已参与抽奖的礼物ID在缓存内过期后的存留时长(单位为分)。默认值为5分钟 78 | }, 79 | "notifier": { 80 | "heartbeatInterval": 5, // 【可选设置】双端直播心跳发送间隔(单位为分)。默认值为5分钟 81 | "midnightCheckInterval": 60 // 【可选设置】午夜判定检测间隔(单位为分)。默认值为60分钟。如果担心银瓜子最后领取时间(正常3轮54分钟,老爷5轮90分钟)超过心跳任务设置的时间太多(默认工作日0:45,周末2:00),可将本设置缩短,例如设为5分钟 82 | }, 83 | "account": { 84 | "maxRequestsPerSecond": 50, // 【可选设置】每秒最大请求数。默认值为50。设置过大容易造成412风控IP 85 | "maxNumRoomEntered": 30, // 【可选设置】已进入房间追踪数组最大长度。默认值为30。已在数组内的房间再次抽奖时不再发送进入信息。可以简单理解为同时打开n个房间的直播页面 86 | "blacklistCheckInterval": 24, // 【可选设置】在访问被拒绝的情况下小黑屋检查间隔时间(单位为小时)。默认值为24小时。在检测到拒绝访问后不再参与抽奖和领取银瓜子,并且会依此设置等待一段时间后再次检测 87 | "stormJoinMaxInterval": 60, // 【可选设置】节奏风暴领取请求最大间隔(单位为毫秒)。节奏风暴领取时会以快速多重请求方式发送,一般情况下在前一个请求执行完毕后才会发送下一个请求,但在网络延迟大的情况下会以不超过此设置的间隔发送。默认值为60毫秒 88 | "abandonStormAfter": 25 // 【可选设置】如果节奏风暴在设定的时间段内未能成功领取,则放弃重试(单位为秒)。默认值为25秒 89 | } 90 | } 91 | ``` 92 | 93 | ### 账号设置 94 | 95 | - **Config file `/src/client/default-user-info.json`** 96 | - 填入账号信息并成功登录后,程序自动以相同的格式写入 `src/client/user.json`,此后都从`src/client/user.json`读取账号和登录信息 97 | - 与Python版的区别:JS实现包括但不限于主站任务、双端观看功能,因此app的两项也必须填上 (仅填cookies不执行任何任务) 98 | 99 | ```javascript 100 | { 101 | "user": { 102 | "username": "", 103 | "password": "" 104 | }, 105 | "app": { 106 | "access_token": "", 107 | "refresh_token": "" 108 | }, 109 | "web": { 110 | "bili_jct": "", 111 | "DedeUserID": "", 112 | "DedeUserID__ckMd5": "", 113 | "sid": "", 114 | "SESSDATA": "" 115 | } 116 | } 117 | ``` 118 | 119 | ### 任务设置 120 | 121 | - **Config file `/src/client/default-task-settings.json`** 122 | - **"taskschedules" 定义任务执行时间段,可以定义多个不同的时间段并给予不同的名字** 123 | - **"tasks" 定义任务。每个任务 "enabled": true 为开启, false 为关闭。** 124 | - **任务的 "schedule" 可以引用一个在 "taskschedules" 里定义的时间段(必须存在)** 125 | - 默认所有任务开启,所有抽奖、心跳任务于北京时间工作日 08:00 - 0:45 ,周末 09:00 - 02:00 时间段执行 126 | - 修改from、to的hours、minutes数值可以自定义抽奖时间段 127 | - weekdays允许值为0-6,对应星期日,一到六。可用逗号分隔,且可以用连接号定义区间,比如0-2,4,6代表周日到周二,加周四和周六 128 | - 还请不要修改type ~ 129 | 130 | ```javascript 131 | { 132 | "taskschedules": { 133 | "normal": [ 134 | { 135 | "from": { 136 | "hours": 8, 137 | "minutes": 0 138 | }, 139 | "to": { 140 | "hours": 0, 141 | "minutes": 45 142 | }, 143 | "weekdays": "1-4" 144 | }, 145 | { 146 | "from": { 147 | "hours": 8, 148 | "minutes": 0 149 | }, 150 | "to": { 151 | "hours": 2, 152 | "minutes": 0 153 | }, 154 | "weekdays": "5" 155 | }, 156 | { 157 | "from": { 158 | "hours": 9, 159 | "minutes": 0 160 | }, 161 | "to": { 162 | "hours": 2, 163 | "minutes": 0 164 | }, 165 | "weekdays": "6" 166 | }, 167 | { 168 | "from": { 169 | "hours": 9, 170 | "minutes": 0 171 | }, 172 | "to": { 173 | "hours": 0, 174 | "minutes": 45 175 | }, 176 | "weekdays": "0" 177 | } 178 | ], 179 | "always": [ 180 | { 181 | "from": { 182 | "hours": 0, 183 | "minutes": 0 184 | }, 185 | "to": { 186 | "hours": 0, 187 | "minutes": 0 188 | }, 189 | "weekdays": "0-6" 190 | } 191 | ] 192 | }, 193 | "tasks": { 194 | "pk": { 195 | "type": "scheduled", 196 | "enabled": true, 197 | "schedule": "normal" 198 | }, 199 | "gift": { 200 | "type": "scheduled", 201 | "enabled": true, 202 | "schedule": "normal" 203 | }, 204 | "guard": { 205 | "type": "scheduled", 206 | "enabled": true, 207 | "schedule": "normal" 208 | }, 209 | "storm": { 210 | "type": "scheduled", 211 | "enabled": true, 212 | "schedule": "normal" 213 | }, 214 | "liveheart": { 215 | "type": "scheduled", 216 | "enabled": true, 217 | "schedule": "normal" 218 | }, 219 | "livesign": { 220 | "type": "daily", 221 | "enabled": true 222 | }, 223 | "idolclubsign": { 224 | "type": "daily", 225 | "enabled": true 226 | }, 227 | "mainsharevideo": { 228 | "type": "daily", 229 | "enabled": true 230 | }, 231 | "mainwatchvideo": { 232 | "type": "daily", 233 | "enabled": true 234 | }, 235 | "silverbox": { 236 | "type": "daily", 237 | "enabled": true 238 | }, 239 | "doublewatch": { 240 | "type": "daily", 241 | "enabled": true 242 | } 243 | } 244 | } 245 | ``` 246 | 247 | 248 | ## Issues 249 | 有Bug请务必立刻反馈 (有使用方式的疑问或者任何功能方面的建议 也欢迎讨论) 250 | 炸我邮箱 251 | -------------------------------------------------------------------------------- /src/net/xhr.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | const http = require('http'); 6 | const https = require('https'); 7 | const querystring = require('querystring'); 8 | const ResponseBuilder = require('./response.js'); 9 | const HttpError = require('./httperror.js'); 10 | 11 | class Xhr { 12 | 13 | constructor() { 14 | this._rateLimiter = null; 15 | } 16 | 17 | withRateLimiter(limiter) { 18 | this._rateLimiter = limiter; 19 | return this; 20 | } 21 | 22 | request(request) { 23 | let xhr = null; 24 | const options = request.toHttpOptions(); 25 | if (request.https === true) { 26 | xhr = https; 27 | } 28 | else { 29 | xhr = http; 30 | } 31 | 32 | const sendRequest = () => { 33 | 34 | const promise = new Promise((resolve, reject) => { 35 | const req = (xhr.request(options) 36 | .on('timeout', () => req.abort()) 37 | .on('abort', () => reject(new HttpError('Http request aborted'))) 38 | .on('error', () => reject(new HttpError('Http request errored'))) 39 | .on('close', () => reject(new HttpError('Http request closed'))) 40 | .on('response', (response) => { 41 | const code = response.statusCode || 0; 42 | const dataSequence = []; 43 | response.on('aborted', () => reject(new HttpError('Http request aborted'))); 44 | response.on('error', (error) => reject(new HttpError(error.message))); 45 | response.on('data', (data) => dataSequence.push(data)); 46 | 47 | if (code >= 200 && code < 300) { 48 | response.on('end', () => { 49 | let url = `${request.host}${request.path}`; 50 | let method = request.method; 51 | const data = Buffer.concat(dataSequence); 52 | const res = (ResponseBuilder.start() 53 | .withHttpResponse(response) 54 | .withUrl(url) 55 | .withMethod(method) 56 | .withData(data) 57 | .build() 58 | ); 59 | resolve(res); 60 | }); 61 | } 62 | else { 63 | reject((new HttpError(`Http status ${code}`)).withStatus(code)); 64 | } 65 | }) 66 | ); 67 | if (request.data) { 68 | req.write(request.data); 69 | } 70 | req.end(); 71 | }); 72 | 73 | return promise; 74 | }; 75 | 76 | let result = new Promise((resolve) => { 77 | if (this._rateLimiter !== null) { 78 | const task = () => { resolve(sendRequest()) }; 79 | this._rateLimiter.add(task); 80 | } 81 | else { 82 | resolve(sendRequest()); 83 | } 84 | }); 85 | 86 | return result; 87 | } 88 | 89 | get(req) { 90 | let xhr = http; 91 | let agent = httpAgent; 92 | if (req.https === true) { 93 | xhr = https; 94 | agent = httpsAgent; 95 | } 96 | 97 | const options = req.toHttpOptions(); 98 | options['agent'] = agent; 99 | 100 | return new Promise((resolve, reject) => { 101 | 102 | const request = (this.sendRequest(options, xhr, req.data) 103 | .on('abort', () => { 104 | const err = new HttpError('Http request aborted'); 105 | reject(err); 106 | }) 107 | .on('error', error => { 108 | const err = new HttpError(error.message); 109 | reject(err); 110 | }) 111 | .on('close', () => { 112 | const err = new HttpError('Http request closed'); 113 | reject(err); 114 | }) 115 | .on('response', response => { 116 | const code = response.statusCode; 117 | 118 | const dataSequence = []; 119 | response.on('data', data => dataSequence.push(data)); 120 | response.on('error', error => reject(error)); 121 | 122 | if (code === 200) { 123 | response.on('end', () => resolve( 124 | this.makeResponse( 125 | response, request, Buffer.concat(dataSequence)))); 126 | } else { 127 | const err = (new HttpError(`http status ${code}`) 128 | .withStatus(code)); 129 | reject(err); 130 | } 131 | }) 132 | ); 133 | }); 134 | } 135 | 136 | post(req) { 137 | let xhr = http; 138 | let agent = httpAgent; 139 | if (req.https === true) { 140 | xhr = https; 141 | agent = httpsAgent; 142 | } 143 | 144 | const options = req.toHttpOptions(); 145 | options['agent'] = agent; 146 | 147 | return new Promise((resolve, reject) => { 148 | 149 | const request = (this.sendRequest(options, xhr, req.data) 150 | .on('timeout', () => { 151 | request.abort(); 152 | }) 153 | .on('abort', () => { 154 | const err = new HttpError('Http request aborted'); 155 | reject(err); 156 | }) 157 | .on('error', error => { 158 | const err = new HttpError(error.message); 159 | reject(err); 160 | }) 161 | .on('close', () => { 162 | const err = new HttpError('Http request closed'); 163 | reject(err); 164 | }) 165 | .on('response', response => { 166 | const code = response.statusCode; 167 | 168 | const dataSequence = []; 169 | response.on('data', data => dataSequence.push(data)); 170 | response.on('aborted', () => reject(new HttpError('Http request aborted'))); 171 | response.on('error', error => reject(error)); 172 | 173 | if (code === 200) { 174 | response.on('end', () => resolve( 175 | this.makeResponse( 176 | response, request, Buffer.concat(dataSequence)))); 177 | } else { 178 | const err = (new HttpError(`http stauts ${code}`) 179 | .withStatus(code)); 180 | reject(err); 181 | } 182 | })); 183 | }); 184 | } 185 | 186 | sendRequest(options, xhr, data) { 187 | if (!xhr) xhr = https; 188 | let request = (xhr.request(options)); 189 | if (data) { 190 | request.write(data); 191 | } 192 | request.end(); 193 | return request; 194 | } 195 | 196 | makeResponse(incomingMessage, request, data) { 197 | let url = ''; 198 | let method = ''; 199 | if (request) { 200 | url = `${request.host}${request.path}`; 201 | method = request.method; 202 | } 203 | return (ResponseBuilder.start() 204 | .withHttpResponse(incomingMessage) 205 | .withUrl(url) 206 | .withMethod(method) 207 | .withData(data) 208 | .build()); 209 | }; 210 | } 211 | 212 | 213 | module.exports = Xhr; 214 | 215 | 216 | /** https agent to handle request sending */ 217 | const httpsAgent = (() => { 218 | const options = { 219 | 'keepAlive': true, 220 | 'maxFreeSockets': 20, 221 | }; 222 | return new https.Agent(options); 223 | })(); 224 | 225 | /** http agent to handle request sending */ 226 | const httpAgent = (() => { 227 | const options = { 228 | 'keepAlive': true, 229 | 'maxFreeSockets': 64, 230 | }; 231 | return new http.Agent(options); 232 | })(); 233 | 234 | 235 | 236 | })(); 237 | -------------------------------------------------------------------------------- /src/server/httphost.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | const cprint = require('../util/printer.js'); 6 | const colors = require('colors/safe'); 7 | 8 | const express = require('express'); 9 | const bodyParser = require('body-parser'); 10 | 11 | const Account = require('../client/account.js'); 12 | const Clock = require('../client/tasks/clock.js'); 13 | const TimePeriod = require('../client/tasks/timeperiod.js'); 14 | 15 | 16 | const dailyTasks = [ 17 | 'livesign', 18 | 'liveheart', 19 | 'idolclubsign', 20 | 'mainsharevideo', 21 | 'mainwatchvideo', 22 | 'doublewatch', 23 | ]; 24 | 25 | const scheduledTasks = [ 26 | 'pk', 27 | 'gift', 28 | 'guard', 29 | 'storm', 30 | ]; 31 | 32 | 33 | class HttpHost { 34 | 35 | constructor(user) { 36 | this.bind(); 37 | 38 | this.user = user || null; 39 | this._app = express(); 40 | this._router = express.Router(); 41 | 42 | this.jsonParser = bodyParser.json(); 43 | 44 | this.routes = { 45 | '/info': { 46 | 'get': this.getInfo, 47 | 'post': this.postInfo, 48 | }, 49 | '/tasks': { 50 | 'get': this.getTasks, 51 | }, 52 | '/register/task': { 53 | 'post': this.registerTask, 54 | }, 55 | '/unregister/task': { 56 | 'post': this.unregisterTask, 57 | }, 58 | }; 59 | this.jsonVerifier = { 60 | '/register': this.registerVerifier, 61 | '/unregister': this.unregisterVerifier, 62 | '/info': this.infoVerifier, 63 | }; 64 | 65 | this.setup(); 66 | } 67 | 68 | app() { 69 | return this._app; 70 | } 71 | 72 | setup() { 73 | Object.entries(this.routes).forEach(entry => { 74 | const path = entry[0]; 75 | const settings = entry[1]; 76 | if (typeof settings['get'] !== 'undefined') 77 | this._router.get(path, settings['get']); 78 | if (typeof settings['post'] !== 'undefined') 79 | this._router.post(path, this.jsonParser, settings['post']); 80 | }); 81 | this._app.use('/', this._router); 82 | this._app && this._app.use(this.handleError); 83 | this._router && this._router.use(this.handleError); 84 | } 85 | 86 | handleError(error, request, response, next) { 87 | if (error) { 88 | cprint(`Error(API): ${error.type}`, colors.red); 89 | console.log(error); 90 | response.jsonp({ 'code': 1, 'msg': error.type }); 91 | } else { 92 | next(); 93 | } 94 | } 95 | 96 | bind() { 97 | this.getInfo = this.getInfo.bind(this); 98 | this.postInfo = this.postInfo.bind(this); 99 | this.getTasks = this.getTasks.bind(this); 100 | this.registerTask = this.registerTask.bind(this); 101 | this.unregisterTask = this.unregisterTask.bind(this); 102 | 103 | this.verifyUser = this.verifyUser.bind(this); 104 | this.infoVerivier = this.infoVerifier.bind(this); 105 | this.registerVerifier = this.registerVerifier.bind(this); 106 | this.unregisterVerifier = this.unregisterVerifier.bind(this); 107 | 108 | this.handleError = this.handleError.bind(this); 109 | } 110 | 111 | getInfo(request, response) { 112 | let info = null; 113 | 114 | if (this.user !== null) { 115 | info = this.user.info(); 116 | } 117 | 118 | const result = { 119 | 'code': 0, 120 | 'msg': 'success', 121 | 'data': info, 122 | }; 123 | 124 | response.jsonp(result); 125 | } 126 | 127 | getTasks(request, response) { 128 | let tasks = null; 129 | 130 | if (this.user !== null) { 131 | tasks = this.user.tasks(); 132 | } 133 | 134 | const result = { 135 | 'code': 0, 136 | 'msg': 'success', 137 | 'data': tasks, 138 | }; 139 | 140 | response.jsonp(result); 141 | } 142 | 143 | postInfo(request, response) { 144 | const json = request.body; 145 | const result = {}; 146 | 147 | if (this.infoVerifier(json)) { 148 | const username = json['username']; 149 | const password = json['password']; 150 | (this.user.updateLoginInfo(username, password) 151 | .then(() => { 152 | result['code'] = 0; 153 | result['msg'] = 'Login success'; 154 | }) 155 | .catch(error => { 156 | result['code'] = 1; 157 | result['msg'] = 'Login failed'; 158 | }) 159 | .then(() => { 160 | // returning result here 161 | response.jsonp(result); 162 | }) 163 | .catch(error => {})); 164 | } 165 | } 166 | 167 | infoVerifier(json) { 168 | const usernameOk = typeof json['username'] !== 'undefined'; 169 | const passwordOk = typeof json['password'] !== 'undefined'; 170 | return usernameOk && passwordOk; 171 | } 172 | 173 | registerTask(request, response) { 174 | const json = request.body; 175 | const result = {}; 176 | 177 | if (this.registerVerifier(json)) { 178 | 179 | const taskname = json['taskname']; 180 | const rawTimeperiod = json['timeperiod']; 181 | let timeperiod = null; 182 | 183 | if (rawTimeperiod) { 184 | timeperiod = new TimePeriod(rawTimeperiod.from, rawTimeperiod.to); 185 | } 186 | 187 | const updated = this.user.register(json['taskname'], { timeperiod }); 188 | 189 | if (updated === true) { 190 | result['code'] = 0; 191 | result['msg'] = 'success'; 192 | } else { 193 | result['code'] = 1; 194 | result['msg'] = 'update failed'; 195 | } 196 | 197 | } else { 198 | result['code'] = 1; 199 | result['msg'] = 'JSON format error'; 200 | } 201 | 202 | response.jsonp(result); 203 | } 204 | 205 | registerVerifier(json) { 206 | 207 | const taskname = json['taskname']; 208 | const timepeiod = json['timeperiod']; 209 | const tasknameOk = typeof taskname !== 'undefined'; 210 | const timeperiodOk = typeof timeperiod !== 'undefined' && timeperiod !== null; 211 | 212 | let fromOk = false; 213 | let toOk = false; 214 | let hoursOk = false; 215 | let minutesOk = false; 216 | let finalOk = false; 217 | 218 | if (timeperiodOk) { 219 | const from = json['timeperiod']['from']; 220 | const to = json['timeperiod']['to']; 221 | fromOk = typeof from !== 'undefined'; 222 | toOk = typeof to !== 'undefined'; 223 | 224 | Object.keys(json['timeperiod']).forEach(time => { 225 | if (hoursOk && minutesOk) { 226 | hoursOk = hoursOk && Number.isInteger(time['hours']); 227 | } 228 | if (hoursOk && minutesOk) { 229 | minutesOk = minutesOk && Number.isInteger(time['minutes']); 230 | } 231 | }); 232 | } 233 | 234 | 235 | if (dailyTasks.includes(taskname)) { 236 | finalOk = true; 237 | } else if (scheduledTasks.includes(taskname)) { 238 | finalOk = fromOk && toOk && hoursOk && minutesOk; 239 | } 240 | 241 | return finalOk; 242 | } 243 | 244 | unregisterTask(request, response) { 245 | const json = request.body; 246 | const result = {}; 247 | 248 | if (this.unregisterVerifier(json)) { 249 | 250 | this.user.unregister(json['taskname']); 251 | result['code'] = 0; 252 | result['msg'] = 'success'; 253 | } else { 254 | result['code'] = 1; 255 | result['msg'] = 'JSON format error'; 256 | } 257 | 258 | response.jsonp(result); 259 | } 260 | 261 | unregisterVerifier(json) { 262 | const taskname = json['taskname']; 263 | const finalOk = dailyTasks.includes(taskname) || scheduledTasks.includes(taskname); 264 | return finalOk; 265 | } 266 | 267 | verifyUser(request, response, next) { 268 | if (this.user && this.user instanceof Account) { 269 | next(); 270 | } else { 271 | const result = { 272 | 'code': 1, 273 | 'msg': 'User failed to establish', 274 | }; 275 | reponse.jsonp(result); 276 | } 277 | } 278 | 279 | } 280 | 281 | module.exports = HttpHost; 282 | 283 | })(); 284 | -------------------------------------------------------------------------------- /src/client/connection.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | const EventEmitter = require('events').EventEmitter; 6 | const ws = require('ws'); 7 | 8 | const cprint = require('../util/printer.js'); 9 | const colors = require('colors/safe'); 10 | 11 | const State = { 12 | INIT: 0, 13 | CONNECTING: 1, 14 | CONNECTED: 2, 15 | CONNECT_PENDING: 3, 16 | CLOSING: 4, 17 | CLOSED: 5, 18 | CLOSE_PENDING: 6, 19 | ERROR: 7, 20 | DISCONNECTING: 8, 21 | DISCONNECTED: 9, 22 | DISCONNECT_PENDING_CONNECTION: 10, 23 | DISCONNECT_PENDING_CLOSE: 11 24 | }; 25 | 26 | class Connection extends EventEmitter { 27 | 28 | constructor(server) { 29 | super(); 30 | 31 | this.numRetries = 3; // By default, retry connection for 3 times before wait 32 | if (server.hasOwnProperty('retries')) { 33 | this.numRetries = server.retries; 34 | } 35 | this.reconnectWaitTime = 1000 * 10; // By default, try to reconnect after 10 seconds 36 | if (server.hasOwnProperty('reconnectWaitTime')) { 37 | this.reconnectWaitTime = server.reconnectWaitTime * 1000; // reconnectWaitTime is in seconds 38 | } 39 | this.healthCheckInterval = 1000 * 5; // By default, health check is performed every 5 seconds 40 | if (server.hasOwnProperty('healthCheckInterval')) { 41 | this.healthCheckInterval = server.healthCheckInterval * 1000; // healthCheckInterval is in seconds 42 | } 43 | this.healthCheckThreshold = 1000 * 25; // By default, health check will fail if last ping time was more than 25 seconds ago 44 | if (server.hasOwnProperty('healthCheckThreshold')) { 45 | this.healthCheckThreshold = server.healthCheckThreshold * 1000; // healthCheckThreshold is in seconds 46 | } 47 | this.enableConnectionErrorLogging = true; 48 | if (server.hasOwnProperty('enableConnectionErrorLogging')) { 49 | this.enableConnectionErrorLogging = server.enableConnectionErrorLogging; 50 | } 51 | this.enableConnectionRetryLogging = true; 52 | if (server.hasOwnProperty('enableConnectionRetryLogging')) { 53 | this.enableConnectionRetryLogging = server.enableConnectionRetryLogging; 54 | } 55 | this.infoColor = colors[server.infoColor || 'green']; 56 | this.errorColor = colors[server.errorColor || 'red']; 57 | 58 | this.ws = null; 59 | this.wsAddress = `${server.address}`; 60 | this.retries = this.numRetries; 61 | this.state = State.INIT; 62 | 63 | this.onOpen = this.onOpen.bind(this); 64 | this.onPing = this.onPing.bind(this); 65 | this.onClose = this.onClose.bind(this); 66 | this.onError = this.onError.bind(this); 67 | this.onMessage = this.onMessage.bind(this); 68 | 69 | this.reconnectTask = null; 70 | this.healthCheckTask = null; 71 | } 72 | 73 | reset() { 74 | switch (this.state) { 75 | case State.CLOSED: 76 | case State.CLOSING: 77 | case State.DISCONNECTED: 78 | case State.DISCONNECT_PENDING_CLOSE: 79 | this.ws = null; 80 | if (this.healthCheckTask !== null) { 81 | clearInterval(this.healthCheckTask); 82 | this.healthCheckTask = null; 83 | } 84 | break; 85 | 86 | default: 87 | // Shouldn't happen 88 | cprint(`Connection.reset(): unexpected state ${this.state}`, this.errorColor); 89 | } 90 | } 91 | 92 | connect() { 93 | switch (this.state) { 94 | case State.INIT: 95 | case State.CLOSED: 96 | this.state = State.CONNECTING; 97 | this.ws = new ws(this.wsAddress); 98 | this.ws.on('open', this.onOpen); 99 | this.ws.on('ping', this.onPing); 100 | this.ws.on('error', this.onError); 101 | this.ws.on('close', this.onClose); 102 | this.ws.on('message', this.onMessage); 103 | break; 104 | 105 | case State.CLOSING: 106 | // Wait until socket is closed before re-connecting 107 | this.state = State.CONNECT_PENDING; 108 | break; 109 | 110 | case State.CLOSE_PENDING: 111 | // Still connecting, remove the pending close request 112 | this.state = State.CONNECTING; 113 | break; 114 | 115 | case State.CONNECTING: 116 | case State.CONNECTED: 117 | case State.CONNECT_PENDING: 118 | // Do nothing 119 | break; 120 | 121 | default: 122 | // Shouldn't happen 123 | cprint(`Connection.connect(): unexpected state ${this.state}`, this.errorColor); 124 | } 125 | } 126 | 127 | disconnect() { 128 | // disconnect() is similar to close(), but with different states so connection won't be retried after socket is closed 129 | switch (this.state) { 130 | case State.CONNECTED: 131 | this.state = State.DISCONNECTING; 132 | this.close(); 133 | break; 134 | 135 | case State.CONNECTING: 136 | case State.CLOSE_PENDING: 137 | this.state = State.DISCONNECT_PENDING_CONNECTION; 138 | break; 139 | 140 | case State.CLOSING: 141 | case State.CONNECT_PENDING: 142 | this.state = State.DISCONNECT_PENDING_CLOSE; 143 | break; 144 | 145 | case State.DISCONNECTING: 146 | case State.DISCONNECTED: 147 | case State.DISCONNECT_PENDING_CLOSE: 148 | case State.DISCONNECT_PENDING_CONNECTION: 149 | // Do nothing 150 | break; 151 | 152 | case State.INIT: 153 | case State.CLOSED: 154 | this.state = State.DISCONNECTED; 155 | this.reset(); 156 | break; 157 | 158 | default: 159 | // Shouldn't happen 160 | cprint(`Connection.disconnect(): unexpected state ${this.state}`, this.errorColor); 161 | } 162 | } 163 | 164 | close() { 165 | switch (this.state) { 166 | case State.CONNECTED: 167 | case State.ERROR: 168 | this.state = State.CLOSING; 169 | this.ws && this.ws.close(); 170 | this.reset(); 171 | break; 172 | 173 | case State.DISCONNECTING: 174 | this.state = State.DISCONNECT_PENDING_CLOSE; 175 | this.ws && this.ws.close(); 176 | this.reset(); 177 | break; 178 | 179 | case State.CONNECTING: 180 | // Wait until socket is connected before closing 181 | this.state = State.CLOSE_PENDING; 182 | break; 183 | 184 | case State.CONNECT_PENDING: 185 | // Still closing, remove the pending connect request 186 | this.state = State.CLOSING; 187 | break; 188 | 189 | case State.CLOSING: 190 | case State.CLOSED: 191 | case State.CLOSE_PENDING: 192 | // Do nothing 193 | break; 194 | 195 | default: 196 | // Shouldn't happen 197 | cprint(`Connection.close(): unexpected state ${this.state}`, this.errorColor); 198 | } 199 | } 200 | 201 | onOpen() { 202 | cprint(`[ Server ] Established connection with ${this.wsAddress}`, this.infoColor); 203 | this.lastPing = new Date(); 204 | this.retries = this.numRetries; 205 | if (this.reconnectTask !== null) { 206 | clearInterval(this.reconnectTask); 207 | this.reconnectTask = null; 208 | } 209 | 210 | switch (this.state) { 211 | case State.CONNECTING: 212 | this.state = State.CONNECTED; 213 | break; 214 | 215 | case State.CLOSE_PENDING: 216 | // There is a pending close request, execute it 217 | this.state = State.CONNECTED; 218 | this.close(); 219 | break; 220 | 221 | case State.DISCONNECT_PENDING_CONNECTION: 222 | // There is a pending disconnect request, execute it 223 | this.state = State.DISCONNECTING; 224 | this.close(); 225 | break; 226 | 227 | default: 228 | // Shouldn't happen 229 | cprint(`Connection.onOpen(): unexpected state ${this.state}`, this.errorColor); 230 | } 231 | 232 | if (this.healthCheckTask === null) { 233 | this.healthCheckTask = setInterval(() => { 234 | if (new Date() - this.lastPing > this.healthCheckThreshold) { 235 | cprint(`[ Server ] Health check failed for ${this.wsAddress}, will try to reconnect`, this.errorColor); 236 | this.close(); 237 | } 238 | }, this.healthCheckInterval); 239 | } 240 | 241 | this.emit('connection', true); 242 | } 243 | 244 | onError(error) { 245 | if (this.enableConnectionErrorLogging) { 246 | cprint(`Error in monitor-server: ${error.message}`, this.errorColor); 247 | } 248 | 249 | switch (this.state) { 250 | case State.CONNECTING: 251 | case State.CONNECTED: 252 | case State.CLOSE_PENDING: 253 | this.state = State.ERROR; 254 | this.close(); 255 | 256 | case State.CLOSING: 257 | case State.DISCONNECT_PENDING_CLOSE: 258 | // Already closing. Do nothing. 259 | break; 260 | 261 | case State.DISCONNECT_PENDING_CONNECTION: 262 | this.state = State.DISCONNECTING; 263 | this.close(); 264 | break; 265 | 266 | default: 267 | // Shouldn't happen 268 | cprint(`Connection.onError(): unexpected state ${this.state}`, this.errorColor); 269 | } 270 | 271 | this.emit('connection', false); 272 | } 273 | 274 | onClose(code, reason) { 275 | switch (this.state) { 276 | case State.CONNECTED: 277 | // Only display error if disconnected by server 278 | cprint(`[ Server ] Lost connection to ${this.wsAddress}`, this.errorColor); 279 | 280 | // At least retry once if disconnected by server 281 | if (this.retries < 1) { 282 | this.retries = 1; 283 | } 284 | 285 | case State.CLOSING: 286 | case State.CONNECT_PENDING: 287 | // If not disconnecting, retry connection with or without a pending connect request 288 | this.state = State.CLOSED; 289 | this.reset(); 290 | 291 | if (this.retries > 0) { 292 | this.retries--; 293 | if (this.enableConnectionRetryLogging) { 294 | cprint(`[ Server ] Trying to reconnect to ${this.wsAddress} ...`, this.infoColor); 295 | } 296 | this.connect(); 297 | } else if (this.reconnectTask === null) { 298 | this.reconnectTask = setInterval(() => { 299 | if (this.enableConnectionRetryLogging) { 300 | cprint(`[ Server ] Trying to reconnect to ${this.wsAddress} ...`, this.infoColor); 301 | } 302 | this.connect(); 303 | }, this.reconnectWaitTime); 304 | } 305 | break; 306 | 307 | case State.DISCONNECT_PENDING_CLOSE: 308 | // Disconnecting, will not retry connection 309 | this.state = State.DISCONNECTED; 310 | this.reset(); 311 | cprint(`[ Server ] Abandoned connection to ${this.wsAddress}`, this.infoColor); 312 | break; 313 | 314 | default: 315 | // Shouldn't happen 316 | cprint(`Connection.onClose(): unexpected state ${this.state}`, this.errorColor); 317 | } 318 | 319 | this.emit('connection', false); 320 | } 321 | 322 | onMessage(data) { 323 | const body = data.toString('utf8'); 324 | 325 | this.emit('message', JSON.parse(body)); 326 | } 327 | 328 | onPing() { 329 | this.lastPing = new Date(); 330 | this.ws && this.ws.pong(); 331 | } 332 | } 333 | 334 | module.exports = Connection; 335 | })(); 336 | -------------------------------------------------------------------------------- /src/bilibili.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | // ------------------------------- Includes ------------------------------- 6 | const crypto = require('crypto'); 7 | const querystring = require('querystring'); 8 | const { 9 | appCommon, 10 | appSecret, 11 | appHeaders, 12 | webHeaders, } = require('./global/config.js'); 13 | 14 | const RequestBuilder = require('./net/request.js'); 15 | const BilibiliRest = require('./bilibili/bilibili-rest.js'); 16 | 17 | 18 | /** Emits requests to the bilibili API */ 19 | class Bilibili extends BilibiliRest { 20 | 21 | 22 | /** --------------------------APP----------------------------- */ 23 | 24 | /** 登录接口 */ 25 | static login(username, password) { 26 | return Bilibili.obtainLoginKey().then(resp => { 27 | if (resp['code'] !== 0) { 28 | return Promise.reject(resp['message']); 29 | } 30 | 31 | // ----------------------RSA----------------------- 32 | const hash = resp['data']['hash']; 33 | const key = resp['data']['key']; 34 | const encryptionSettings = { 35 | key: key, 36 | padding: crypto.constants.RSA_PKCS1_PADDING, 37 | }; 38 | const encryptedForm = crypto.publicEncrypt( 39 | encryptionSettings, Buffer.from(hash + password)); 40 | const hashedPasswd = encryptedForm.toString('base64'); 41 | 42 | const data = {}; 43 | Object.assign(data, appCommon); 44 | data['username'] = username; 45 | data['password'] = hashedPasswd; 46 | data['ts'] = Number.parseInt(0.001 * new Date()); 47 | 48 | const headers = Object.assign(new Object(), appHeaders); 49 | 50 | const payload = Bilibili.parseAppParams(sort(data)); 51 | 52 | const request = (RequestBuilder.start() 53 | .withHost('passport.bilibili.com') 54 | .withPath('/api/v3/oauth2/login') 55 | .withMethod('POST') 56 | .withHeaders(headers) 57 | .withData(payload) 58 | .withContentType('application/x-www-form-urlencoded') 59 | .withHttps() 60 | .build() 61 | ); 62 | 63 | return Bilibili.request(request); 64 | }); 65 | } 66 | 67 | /** 向b站申请key和hash用以加密密码 */ 68 | static obtainLoginKey() { 69 | const data = {}; 70 | Object.assign(data, appCommon); 71 | data['appkey'] = appCommon['appkey']; 72 | data['ts'] = Number.parseInt(0.001 * new Date()); 73 | const payload = Bilibili.parseAppParams(sort(data)); 74 | 75 | const headers = Object.assign(new Object(), appHeaders); 76 | 77 | const request = (RequestBuilder.start() 78 | .withHost('passport.bilibili.com') 79 | .withPath('/api/oauth2/getKey') 80 | .withMethod('POST') 81 | .withHeaders(headers) 82 | .withData(payload) 83 | .withContentType('application/x-www-form-urlencoded') 84 | .withHttps() 85 | .build() 86 | ); 87 | 88 | return Bilibili.request(request); 89 | } 90 | 91 | static refreshToken(session) { 92 | const access_token = session['app']['access_token']; 93 | const refresh_token = session['app']['refresh_token']; 94 | const data = {}; 95 | Object.assign(data, appCommon); 96 | data['access_token'] = access_token; 97 | data['refresh_token'] = refresh_token; 98 | const payload = Bilibili.parseAppParams(sort(data)); 99 | 100 | const headers = Object.assign(new Object(), appHeaders); 101 | 102 | const request = (RequestBuilder.start() 103 | .withHost('passport.bilibili.com') 104 | .withPath('/api/oauth2/refreshToken') 105 | .withMethod('POST') 106 | .withHeaders(headers) 107 | .withData(payload) 108 | .withContentType('application/x-www-form-urlencoded') 109 | .withHttps() 110 | .build() 111 | ); 112 | 113 | return Bilibili.request(request); 114 | } 115 | 116 | 117 | /** 118 | * @params session Object 119 | * @params info Object 120 | * roomid Int 房间号 121 | */ 122 | static appGetInfoByUser(session, info) { 123 | const { roomid } = info; 124 | const data = Object.assign(new Object(), appCommon); 125 | data['actionKey'] = 'appkey'; 126 | data['room_id'] = roomid; 127 | data['ts'] = Math.floor(0.001 * new Date().valueOf()); 128 | data['access_key'] = session['app']['access_token']; 129 | const paramstr = Bilibili.parseAppParams(sort(data)); 130 | 131 | const headers = Object.assign(new Object(), appHeaders); 132 | 133 | const request = (RequestBuilder.start() 134 | .withHost('api.live.bilibili.com') 135 | .withPath('/xlive/app-room/v1/index/getInfoByUser') 136 | .withMethod('GET') 137 | .withHeaders(headers) 138 | .withParams(paramstr) 139 | .build() 140 | ); 141 | 142 | return Bilibili.request(request); 143 | } 144 | 145 | 146 | /** 147 | * @params session Object 148 | * @params info Object 149 | * roomid Int 房间号 150 | */ 151 | static appLiveOnlineHeart(session, info) { 152 | const { roomid } = info; 153 | const data = { 154 | 'room_id': roomid, 155 | 'scale': 'xhdpi', 156 | }; 157 | const payload = Bilibili.formatForm(data); 158 | 159 | const params = {}; 160 | const access_key = session['app']['access_token']; 161 | Object.assign(params, appCommon); 162 | params['access_key'] = access_key; 163 | params['ts'] = Number.parseInt(0.001 * new Date()); 164 | const paramstr = Bilibili.parseAppParams(sort(params)); 165 | 166 | const headers = Object.assign(new Object(), appHeaders); 167 | 168 | const request = (RequestBuilder.start() 169 | .withHost('api.live.bilibili.com') 170 | .withPath('/heartbeat/v1/OnLine/mobileOnline') 171 | .withMethod('POST') 172 | .withHeaders(headers) 173 | .withParams(paramstr) 174 | .withData(payload) 175 | .withContentType('application/x-www-form-urlencoded') 176 | .build() 177 | ); 178 | 179 | return Bilibili.request(request); 180 | } 181 | 182 | static checkSilverBox(session) { 183 | const params = {}; 184 | const access_key = session['app']['access_token']; 185 | Object.assign(params, appCommon); 186 | params['access_key'] = access_key; 187 | params['ts'] = Number.parseInt(+new Date() / 1000); 188 | const paramstr = Bilibili.parseAppParams(params); 189 | 190 | const headers = Object.assign(new Object(), appHeaders); 191 | 192 | const request = (RequestBuilder.start() 193 | .withHost('api.live.bilibili.com') 194 | .withPath('/lottery/v1/SilverBox/getCurrentTask') 195 | .withMethod('GET') 196 | .withHeaders(headers) 197 | .withParams(paramstr) 198 | .build() 199 | ); 200 | 201 | return Bilibili.request(request); 202 | } 203 | 204 | /** 205 | * @params access_key 206 | * @params info Object 207 | * time_start Int 银瓜子时段起始 208 | * time_end Int 银瓜子时段终末 209 | */ 210 | static getSilverBox(session, info) { 211 | const { time_start, time_end } = info; 212 | const access_key = session['app']['access_token']; 213 | const params = {}; 214 | Object.assign(params, appCommon); 215 | params['access_key'] = access_key; 216 | params['time_start'] = time_start; 217 | params['time_end'] = time_end; 218 | params['ts'] = Number.parseInt(+new Date() / 1000); 219 | const paramstr = Bilibili.parseAppParams(params); 220 | 221 | const headers = Object.assign(new Object(), appHeaders); 222 | 223 | const request = (RequestBuilder.start() 224 | .withHost('api.live.bilibili.com') 225 | .withPath('/lottery/v1/SilverBox/getAward') 226 | .withMethod('GET') 227 | .withHeaders(headers) 228 | .withParams(paramstr) 229 | .build() 230 | ); 231 | 232 | return Bilibili.request(request); 233 | } 234 | 235 | /** 236 | * @params access_key 237 | * @params info Object 238 | * info. group_id Int 应援团id 239 | * info. owner_id Int 应援对象id 240 | */ 241 | static loveClubSign(session, info) { 242 | const params = {}; 243 | const { group_id, owner_id } = info; 244 | const access_key = session['app']['access_token']; 245 | Object.assign(params, appCommon); 246 | params['access_key'] = access_key; 247 | params['group_id'] = group_id; 248 | params['owner_id'] = owner_id; 249 | params['ts'] = Number.parseInt(0.001 * new Date()); 250 | const paramstr = Bilibili.parseAppParams(params); 251 | 252 | const headers = Object.assign(new Object(), appHeaders); 253 | 254 | const request = (RequestBuilder.start() 255 | .withHost('api.vc.bilibili.com') 256 | .withPath('/link_setting/v1/link_setting/sign_in') 257 | .withMethod('GET') 258 | .withHeaders(headers) 259 | .withParams(paramstr) 260 | .build() 261 | ); 262 | 263 | return Bilibili.request(request); 264 | } 265 | 266 | 267 | /** 268 | * @params access_key String 269 | * @params info Object 270 | * info. aid Int 视频id 271 | */ 272 | static shareVideo(session, info) { 273 | const { aid } = info; 274 | const access_key = session['app']['access_token']; 275 | const data = {}; 276 | Object.assign(data, appCommon); 277 | data['access_key'] = access_key; 278 | data['aid'] = aid; 279 | data['ts'] = Number.parseInt(+new Date() / 1000); 280 | data['share_channel'] = 'qq'; 281 | data['share_trace_id'] = crypto.randomBytes(16).toString('hex'); 282 | data['from'] = 'main.ugc-video-detail.0.0'; 283 | const payload = Bilibili.parseAppParams(sort(data)); 284 | 285 | const headers = Object.assign(new Object(), appHeaders); 286 | 287 | const request = (RequestBuilder.start() 288 | .withHost('app.bilibili.com') 289 | .withPath('/x/v2/view/share/complete') 290 | .withMethod('POST') 291 | .withHeaders(headers) 292 | .withData(payload) 293 | .withContentType('application/x-www-form-urlencoded') 294 | .build() 295 | ); 296 | 297 | return Bilibili.request(request); 298 | } 299 | 300 | /** 直播间历史模仿 */ 301 | static appRoomEntry(session, roomid) { 302 | const access_key = session['app']['access_token']; 303 | const data = {}; 304 | Object.assign(data, appCommon); 305 | data['access_key'] = access_key; 306 | data['actionKey'] = 'appkey'; 307 | data['device'] = 'android'; 308 | data['jumpFrom'] = 0; 309 | data['room_id'] = roomid; 310 | data['ts'] = Number.parseInt(0.001 * new Date()); 311 | const payload = Bilibili.parseAppParams(sort(data)); 312 | 313 | const headers = Object.assign(new Object(), appHeaders); 314 | 315 | const request = (RequestBuilder.start() 316 | .withHost('api.live.bilibili.com') 317 | .withPath('/room/v1/Room/room_entry_action') 318 | .withMethod('POST') 319 | .withHeaders(headers) 320 | .withData(payload) 321 | .withContentType('application/x-www-form-urlencoded') 322 | .build() 323 | ); 324 | 325 | return Bilibili.request(request); 326 | } 327 | 328 | /** 直播间历史模仿2 */ 329 | static appRoomLiveTrace(session, roomid) { 330 | roomid = roomid || 164725; 331 | const data = {}; 332 | const access_key = session['app']['access_token']; 333 | Object.assign(data, appCommon); 334 | data['actionKey'] = 'appkey'; 335 | data['access_key'] = access_key; 336 | data['area_id'] = data['parent_id'] = data['seq_id'] = 0; 337 | data['buvid'] = 'XYFFB38F026C47196F273167295B14721F489'; 338 | data['device'] = 'android'; 339 | data['is_patch'] = 0; 340 | data['room_id'] = roomid; 341 | data['heart_beat'] = JSON.stringify([]); 342 | data['ts'] = Number.parseInt(0.001 * new Date()); 343 | data['client_ts'] = data['ts'] + 19; 344 | data['uuid'] = getUUID(); 345 | const payload = Bilibili.parseAppParams(sort(data)); 346 | 347 | const headers = Object.assign(new Object(), appHeaders); 348 | 349 | const request = (RequestBuilder.start() 350 | .withHost('live-trace.bilibili.com') 351 | .withPath('/xlive/data-interface/v1/heartbeat/mobileEntry') 352 | .withMethod('POST') 353 | .withHeaders(headers) 354 | .withData(payload) 355 | .withContentType('application/x-www-form-urlencoded') 356 | .build() 357 | ); 358 | 359 | return Bilibili.request(request); 360 | } 361 | 362 | /** 363 | * @static 364 | * @param {Object} session 365 | * @param {Object} giftData 366 | * @param {Integer} giftData.id 367 | * @param {Integer} giftData.roomid 368 | * @param {String} giftData.type 369 | */ 370 | static appJoinGift(session, giftData) { 371 | const { id, roomid, type } = giftData; 372 | const access_key = session['app']['access_token']; 373 | const data = {}; 374 | Object.assign(data, appCommon); 375 | data['access_key'] = access_key; 376 | data['actionKey'] = 'appkey'; 377 | data['device'] = 'android'; 378 | data['raffleId'] = id; 379 | data['roomid'] = roomid; 380 | data['type'] = type; 381 | data['ts'] = Number.parseInt(0.001 * new Date()); 382 | const payload = Bilibili.parseAppParams(sort(data)); 383 | 384 | const headers = Object.assign(new Object(), appHeaders); 385 | 386 | const request = (RequestBuilder.start() 387 | .withHost('api.live.bilibili.com') 388 | .withPath('/xlive/lottery-interface/v4/smalltv/Getaward') 389 | .withMethod('POST') 390 | .withHeaders(headers) 391 | .withData(payload) 392 | .withContentType('application/x-www-form-urlencoded') 393 | .build() 394 | ); 395 | 396 | return Bilibili.request(request); 397 | } 398 | 399 | 400 | /** 401 | * @static 402 | * @param {Object} session 403 | * @param {Object} pkData 404 | * @param {Integer} pkData.id 405 | * @param {Integer} pkData.roomid 406 | */ 407 | static appJoinPK(session, pkData) { 408 | const { id, roomid } = pkData; 409 | const access_key = session['app']['access_token']; 410 | const data = {}; 411 | Object.assign(data, appCommon); 412 | data['access_key'] = access_key; 413 | data['actionKey'] = 'appkey'; 414 | data['device'] = 'android'; 415 | data['id'] = id; 416 | data['roomid'] = roomid; 417 | data['ts'] = Number.parseInt(0.001 * new Date()); 418 | const payload = Bilibili.parseAppParams(sort(data)); 419 | 420 | const headers = Object.assign(new Object(), appHeaders); 421 | 422 | const request = (RequestBuilder.start() 423 | .withHost('api.live.bilibili.com') 424 | .withPath('/xlive/lottery-interface/v1/pk/join') 425 | .withMethod('POST') 426 | .withHeaders(headers) 427 | .withData(payload) 428 | .withContentType('application/x-www-form-urlencoded') 429 | .build() 430 | ); 431 | 432 | return Bilibili.request(request); 433 | } 434 | 435 | 436 | /** 437 | * @static 438 | * @param {Object} session 439 | * @param {Object} guardData 440 | * @param {Integer} guardData.id 441 | * @param {Integer} guardData.roomid 442 | * @param {String} guardData.type 443 | */ 444 | static appJoinGuard(session, guardData) { 445 | const { id, roomid, type } = guardData; 446 | const access_key = session['app']['access_token']; 447 | const data = {}; 448 | Object.assign(data, appCommon); 449 | data['access_key'] = access_key; 450 | data['actionKey'] = 'appkey'; 451 | data['device'] = 'android'; 452 | data['id'] = id; 453 | data['roomid'] = roomid; 454 | data['type'] = type || 'guard'; 455 | data['ts'] = Number.parseInt(0.001 * new Date()); 456 | const payload = Bilibili.parseAppParams(sort(data)); 457 | 458 | const headers = Object.assign(new Object(), appHeaders); 459 | 460 | const request = (RequestBuilder.start() 461 | .withHost('api.live.bilibili.com') 462 | .withPath('/xlive/lottery-interface/v2/Lottery/join') 463 | .withMethod('POST') 464 | .withHeaders(headers) 465 | .withData(payload) 466 | .withContentType('application/x-www-form-urlencoded') 467 | .build() 468 | ); 469 | 470 | return Bilibili.request(request); 471 | } 472 | 473 | /** 474 | * @static 475 | * @param {Object} session 476 | * @param {Object} stormData 477 | * @param {Integer} stormData.id 478 | */ 479 | static appJoinStorm(session, stormData) { 480 | const access_key = session['app']['access_token']; 481 | const { id } = stormData; 482 | const data = {}; 483 | Object.assign(data, appCommon); 484 | data['access_key'] = access_key; 485 | data['actionKey'] = 'appkey'; 486 | data['device'] = 'android'; 487 | data['id'] = id; 488 | data['ts'] = Math.floor(0.001 * new Date()); 489 | const payload = Bilibili.parseAppParams(sort(data)); 490 | 491 | const headers = Object.assign(new Object(), appHeaders); 492 | 493 | const request = (RequestBuilder.start() 494 | .withHost('api.live.bilibili.com') 495 | .withPath('/xlive/lottery-interface/v1/storm/Join') 496 | .withMethod('POST') 497 | .withHeaders(headers) 498 | .withData(payload) 499 | .withContentType('application/x-www-form-urlencoded') 500 | .build() 501 | ); 502 | 503 | return Bilibili.request(request); 504 | } 505 | // */ 506 | 507 | 508 | static appSign(string) { 509 | return crypto.createHash('md5').update(string+appSecret).digest('hex'); 510 | } 511 | 512 | static parseAppParams(params) { 513 | const pre_paramstr = Bilibili.formatForm(params); 514 | const sign = Bilibili.appSign(pre_paramstr); 515 | const paramstr = `${pre_paramstr}&sign=${sign}`; 516 | return paramstr; 517 | } 518 | 519 | 520 | /** --------------------------WEB----------------------------- */ 521 | 522 | static mainTaskInfo(session) { 523 | const request = (RequestBuilder.start() 524 | .withHost('account.bilibili.com') 525 | .withPath('/home/reward') 526 | .withMethod('GET') 527 | .withHeaders(webHeaders) 528 | .withCookies(session['web']) 529 | .build() 530 | ); 531 | 532 | return Bilibili.request(request); 533 | } 534 | 535 | static liveTaskInfo(session) { 536 | const request = (RequestBuilder.start() 537 | .withHost('api.live.bilibili.com') 538 | .withPath('/i/api/taskInfo') 539 | .withMethod('GET') 540 | .withHeaders(webHeaders) 541 | .withCookies(session['web']) 542 | .build() 543 | ); 544 | 545 | return Bilibili.request(request); 546 | } 547 | 548 | static liveSignInfo(session) { 549 | const request = (RequestBuilder.start() 550 | .withHost('api.live.bilibili.com') 551 | .withPath('/sign/GetSignInfo') 552 | .withMethod('GET') 553 | .withHeaders(webHeaders) 554 | .withCookies(session['web']) 555 | .build() 556 | ); 557 | 558 | return Bilibili.request(request); 559 | } 560 | 561 | static liveSign(session) { 562 | const request = (RequestBuilder.start() 563 | .withHost('api.live.bilibili.com') 564 | .withPath('/sign/doSign') 565 | .withMethod('GET') 566 | .withHeaders(webHeaders) 567 | .withCookies(session['web']) 568 | .build() 569 | ); 570 | 571 | return Bilibili.request(request); 572 | } 573 | 574 | static webGetInfoByUser(session, info) { 575 | const { roomid } = info; 576 | const params = {}; 577 | params['room_id'] = roomid; 578 | 579 | 580 | const request = (RequestBuilder.start() 581 | .withHost('api.live.bilibili.com') 582 | .withPath('/xlive/web-room/v1/index/getInfoByUser') 583 | .withMethod('GET') 584 | .withCookies(session['web']) 585 | .withHeaders(webHeaders) 586 | .withParams(params) 587 | .build() 588 | ); 589 | 590 | return Bilibili.request(request); 591 | } 592 | 593 | static webLiveOnlineHeart(session) { 594 | const data = { 595 | 'csrf': session['web']['bili_jct'], 596 | 'csrf_token': session['web']['bili_jct'], 597 | 'visit_id': '', 598 | }; 599 | 600 | const request = (RequestBuilder.start() 601 | .withHost('api.live.bilibili.com') 602 | .withPath('/User/userOnlineHeart') 603 | .withMethod('POST') 604 | .withHeaders(webHeaders) 605 | .withCookies(session['web']) 606 | .withData(data) 607 | .withContentType('application/x-www-form-urlencoded') 608 | .build() 609 | ); 610 | 611 | return Bilibili.request(request); 612 | } 613 | 614 | static liveDoubleWatch(session) { 615 | const csrf = session['web']['bili_jct']; 616 | const data = { 617 | 'task_id': 'double_watch_task', 618 | 'csrf': csrf, 619 | 'csrf_token': csrf, 620 | }; 621 | 622 | const request = (RequestBuilder.start() 623 | .withHost('api.live.bilibili.com') 624 | .withPath('/activity/v1/task/receive_award') 625 | .withMethod('POST') 626 | .withHeaders(webHeaders) 627 | .withCookies(session['web']) 628 | .withData(data) 629 | .withContentType('application/x-www-form-urlencoded') 630 | .build() 631 | ); 632 | 633 | return Bilibili.request(request); 634 | } 635 | 636 | static loveClubList(session) { 637 | const params = { 638 | 'build': 0, 639 | 'mobi_app': 'web', 640 | }; 641 | const request = (RequestBuilder.start() 642 | .withHost('api.vc.bilibili.com') 643 | .withPath('/link_group/v1/member/my_groups') 644 | .withMethod('GET') 645 | .withParams(params) 646 | .withCookies(session['web']) 647 | .withHeaders(webHeaders) 648 | .build() 649 | ); 650 | 651 | return Bilibili.request(request); 652 | } 653 | 654 | 655 | /** 656 | * @params info Object 657 | * aid String 视频id 658 | * cid String ? 659 | */ 660 | static watchVideo(session, info) { 661 | const { aid, cid } = info; 662 | const data = {}; 663 | data['aid'] = aid; 664 | data['cid'] = cid; 665 | data['mid'] = session['web']['DedeUserID']; 666 | data['played_time'] = 30; 667 | data['real_time'] = 30; 668 | data['type'] = 3; 669 | data['dt'] = 2; 670 | data['play_type'] = 3; 671 | data['start_ts'] = Number.parseInt(0.001 * new Date()) - data['played_time']; 672 | const payload = Bilibili.parseAppParams(sort(data)); 673 | 674 | const request = (RequestBuilder.start() 675 | .withHost('api.bilibili.com') 676 | .withPath('/x/report/web/heartbeat') 677 | .withMethod('POST') 678 | .withContentType('application/x-www-form-urlencoded') 679 | .withData(data) 680 | .withCookies(session['web']) 681 | .withHeaders(webHeaders) 682 | .build() 683 | ); 684 | 685 | return Bilibili.request(request); 686 | } 687 | 688 | } 689 | 690 | 691 | module.exports = Bilibili; 692 | 693 | 694 | /** 695 | * Sort the properties according to alphabetical order 696 | */ 697 | const sort = (object) => { 698 | const sorted = Object.create(null); 699 | Object.keys(object).sort().forEach(key => { 700 | sorted[key] = object[key]; 701 | }); 702 | return sorted; 703 | }; 704 | 705 | const getUUID = () => { 706 | // Format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 707 | const parts = [ 708 | crypto.randomBytes(4).toString('hex'), 709 | crypto.randomBytes(2).toString('hex'), 710 | crypto.randomBytes(2).toString('hex'), 711 | crypto.randomBytes(2).toString('hex'), 712 | crypto.randomBytes(6).toString('hex'), 713 | ]; 714 | const uuid = parts.join('-'); 715 | return uuid; 716 | }; 717 | 718 | })(); 719 | -------------------------------------------------------------------------------- /src/client/account-runner.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | 'use strict'; 4 | 5 | const path = require('path'); 6 | const fs = require('fs'); 7 | 8 | const VirtualQueue = require('./queue.js'); 9 | const Account = require('./account.js'); 10 | const Bilibili = require('../bilibili.js'); 11 | const ScheduledTask = require('./tasks/scheduledtask.js'); 12 | const DailyTask = require('./tasks/dailytask.js'); 13 | const WeeklySchedule = require('./tasks/weeklyschedule.js'); 14 | const Clock = require('./tasks/clock.js'); 15 | const { sleep, } = require('../util/utils.js'); 16 | 17 | const cprint = require('../util/printer.js'); 18 | const colors = require('colors/safe'); 19 | 20 | class AccountRunner extends Account { 21 | 22 | constructor(filename, options) { 23 | super(filename); 24 | this._tasks = { 25 | 'pk': new ScheduledTask(), 26 | 'gift': new ScheduledTask(), 27 | 'storm': new ScheduledTask(), 28 | 'guard': new ScheduledTask(), 29 | 'livesign': new DailyTask(), 30 | 'liveheart': new ScheduledTask(), 31 | 'idolclubsign': new DailyTask(), 32 | 'mainsharevideo': new DailyTask(), 33 | 'mainwatchvideo': new DailyTask(), 34 | 'silverbox': new DailyTask(), 35 | 'doublewatch': new DailyTask(), 36 | }; 37 | this._stormPending = false; 38 | this.tasksFilename = 'task-for-' + this.filename; 39 | this.blacklisted = false; 40 | this.blacklistCheckInterval = 1000 * 60 * 60 * 24; // By default, when blacklisted, check back after 24 hours 41 | this.nextBlacklistCheckTime = null; 42 | this.stormJoinMaxInterval = 60; // By default, max interval between storm 'join' request is 60 ms, if internet is slower than that 43 | this.abandonStormAfter = 1000 * 25; // By default, abandon a storm after 25 seconds 44 | this.roomEntered = new Set(); 45 | this.maxNumRoomEntered = 30; // By default, only keep at most 30 rooms in entered queue 46 | let maxRequestsPerSecond = 50; 47 | 48 | if (options) { 49 | if (options.hasOwnProperty('maxRequestsPerSecond')) { 50 | maxRequestsPerSecond = options.maxRequestsPerSecond; 51 | } 52 | if (options.hasOwnProperty('maxNumRoomEntered')) { 53 | this.maxNumRoomEntered = options.maxNumRoomEntered; 54 | } 55 | if (options.hasOwnProperty('blacklistCheckInterval')) { 56 | this.blacklistCheckInterval = options.blacklistCheckInterval * 1000 * 60 * 60; // blacklistCheckInterval is in hours 57 | } 58 | if (options.hasOwnProperty('stormJoinMaxInterval')) { 59 | this.stormJoinMaxInterval = options.stormJoinMaxInterval; // stormJoinMaxInterval is in milliseconds 60 | } 61 | if (options.hasOwnProperty('abandonStormAfter')) { 62 | this.abandonStormAfter = options.abandonStormAfter * 1000; // abandonStormAfter is in seconds 63 | } 64 | } 65 | 66 | this.q = new VirtualQueue(maxRequestsPerSecond, 1000); 67 | } 68 | 69 | bind() { 70 | super.bind(); 71 | this.reportHttpError = this.reportHttpError.bind(this); 72 | this.mainWatchVideo = this.mainWatchVideo.bind(this); 73 | this.mainShareVideo = this.mainShareVideo.bind(this); 74 | this.liveSign = this.liveSign.bind(this); 75 | this.liveHeart = this.liveHeart.bind(this); 76 | this.liveSilverBox = this.liveSilverBox.bind(this); 77 | this.liveDoubleWatch = this.liveDoubleWatch.bind(this); 78 | this.idolClubSign = this.idolClubSign.bind(this); 79 | this.joinPK = this.joinPK.bind(this); 80 | this.joinGift = this.joinGift.bind(this); 81 | this.joinGuard = this.joinGuard.bind(this); 82 | this.joinStorm = this.joinStorm.bind(this); 83 | } 84 | 85 | register(taskname, options) { 86 | const task = this._tasks[taskname]; 87 | 88 | if (typeof task === 'undefined') 89 | return null; 90 | 91 | const actions = { 92 | 'pk': this.joinPK, 93 | 'gift': this.joinGift, 94 | 'storm': this.joinStorm, 95 | 'guard': this.joinGuard, 96 | 'livesign': this.liveSign, 97 | 'liveheart': this.liveHeart, 98 | 'idolclubsign': this.idolClubSign, 99 | 'mainsharevideo': this.mainShareVideo, 100 | 'mainwatchvideo': this.mainWatchVideo, 101 | 'silverbox': this.liveSilverBox, 102 | 'doublewatch': this.liveDoubleWatch, 103 | }; 104 | 105 | task.registerCallback(actions[taskname]); 106 | 107 | if (options) { 108 | task.updateTimePeriod && task.updateTimePeriod(new WeeklySchedule(options.timeperiod)); 109 | } 110 | 111 | return true; 112 | } 113 | 114 | unregister(taskname) { 115 | const task = this._tasks[taskname]; 116 | 117 | if (typeof task === 'undefined') 118 | return null; 119 | 120 | switch (taskname.toUpperCase()) { 121 | case 'PK': 122 | case 'GIFT': 123 | case 'GUARD': 124 | case 'STORM': 125 | case 'LIVEHEART': 126 | this._tasks[taskname] = new ScheduledTask(); 127 | break; 128 | case 'LIVESIGN': 129 | case 'IDOLCLUBSIGN': 130 | case 'MAINSHAREVIDEO': 131 | case 'MAINWATCHVIDEO': 132 | case 'SILVERBOX': 133 | case 'DOUBLEWATCH': 134 | this._tasks[taskname] = new DailyTask(); 135 | break; 136 | } 137 | } 138 | 139 | info() { 140 | return super.info(); 141 | } 142 | 143 | tasks() { 144 | let result = {}; 145 | Object.entries(this._tasks).forEach(entry => { 146 | result[entry[0]] = entry[1].json(); 147 | }); 148 | return result; 149 | } 150 | 151 | execute(taskname, ...args) { 152 | const task = this._tasks[taskname]; 153 | 154 | const ratelimitedTask = [ 155 | 'gift', 156 | 'guard', 157 | 'pk', 158 | ]; 159 | 160 | return (async () => { 161 | if (ratelimitedTask.includes(taskname)) { 162 | await this.q.add(); 163 | } 164 | return task && task.execute(...args); 165 | })(); 166 | } 167 | 168 | reportHttpError(error) { 169 | cprint(`Error - ${error}`, colors.red); 170 | return null; 171 | } 172 | 173 | checkErrorResponse(msg) { 174 | if (msg.includes('访问被拒绝')) { 175 | this.blacklisted = true; 176 | 177 | // If we still have a next check time defined in the future, just leave it alone. 178 | if (this.nextBlacklistCheckTime === null || this.nextBlacklistCheckTime <= new Date()) { 179 | this.nextBlacklistCheckTime = new Date(new Date().getTime() + this.blacklistCheckInterval); 180 | } 181 | 182 | return Promise.reject(`${msg} -- 已进小黑屋`); 183 | } 184 | 185 | if (msg.includes('请先登录')) { 186 | cprint('-------------账号重新登录中---------------', colors.yellow); 187 | return this.login(true); 188 | } 189 | 190 | if (msg.includes('请稍后再试')) { 191 | // Just retry 192 | cprint(`${msg} -- 将重新获取`, colors.yellow); 193 | return Promise.resolve(true); 194 | } 195 | 196 | return Promise.reject(msg); 197 | } 198 | 199 | checkBlacklisted() { 200 | if (this.blacklisted) { 201 | if (new Date() < this.nextBlacklistCheckTime) { 202 | cprint('已进小黑屋,暂停执行', colors.grey); 203 | } else { 204 | // May be released by now, so reset the flag to try again. 205 | this.blacklisted = false; 206 | 207 | // Update next check time based on previous check time, not current time. 208 | this.nextBlacklistCheckTime = new Date(this.nextBlacklistCheckTime.getTime() + this.blacklistCheckInterval); 209 | } 210 | } 211 | 212 | return this.blacklisted; 213 | } 214 | 215 | /** 主站观看视频 */ 216 | mainWatchVideo() { 217 | if (this.usable === false) return null; 218 | 219 | const info = { 220 | 'aid': 70160595, 221 | 'cid': 121541439, 222 | }; 223 | 224 | return Bilibili.watchVideo(this.session, info).then(resp => { 225 | 226 | if (resp.code !== 0) { 227 | const msg = resp.message || resp.msg || ''; 228 | return Promise.reject(`视频观看失败: (${resp.code})${msg}`); 229 | } 230 | 231 | cprint('视频观看成功', colors.green); 232 | 233 | }).catch(this.reportHttpError); 234 | } 235 | 236 | /** 主站分享视频 */ 237 | mainShareVideo(aid=70160595) { 238 | if (this.usable === false) return null; 239 | 240 | return Bilibili.shareVideo(this.session, { aid }).then(resp => { 241 | 242 | if (resp.code !== 0) { 243 | const msg = resp.message || resp.msg || ''; 244 | return Promise.reject(`视频分享失败: (${resp.code})${msg}`); 245 | } 246 | 247 | cprint(`视频分享: ${resp.data.toast}`, colors.green); 248 | 249 | }).catch(this.reportHttpError); 250 | } 251 | 252 | /** 直播区签到 */ 253 | liveSign() { 254 | if (this.usable === false) return null; 255 | 256 | return Bilibili.liveSignInfo(this.session).then(resp => { 257 | 258 | const signed = resp['data']['status']; 259 | 260 | if (signed === 0) { 261 | return Bilibili.liveSign(this.session); 262 | } else { 263 | cprint(`直播签到奖励已领取: ${resp.data.text}`, colors.grey); 264 | } 265 | 266 | }).then(resp => { 267 | if (!resp) return; 268 | 269 | const code = resp['code']; 270 | 271 | if (code === 0) { 272 | cprint(`直播签到奖励: ${resp.data.text}`, colors.green); 273 | } else { 274 | const msg = resp['msg'] || resp['message'] || ''; 275 | cprint(`直播签到失败: (${code})${msg}`, colors.red); 276 | } 277 | }).catch(this.reportHttpError); 278 | } 279 | 280 | /** 银瓜子 */ 281 | liveSilverBox() { 282 | if (this.usable === false) return null; 283 | 284 | if (this.checkBlacklisted()) return null; 285 | 286 | const execute = async () => { 287 | 288 | let errCount = 0; 289 | while (true) { 290 | // 1. Get SilverBox status 291 | let resp = await Bilibili.checkSilverBox(this.session); 292 | let data = resp.data; 293 | if (typeof data.time_start === 'undefined') { 294 | 295 | let msg = resp.message || resp.msg || ''; 296 | if (msg.match(/今天.*领完/) !== null) { 297 | cprint(msg, colors.grey); 298 | break; 299 | } else { 300 | let showError = false; 301 | await this.checkErrorResponse(msg).catch((err) => { 302 | showError = true; 303 | msg = err; 304 | }); 305 | if (showError && ++errCount >= 3) { 306 | cprint(`银瓜子领取失败: (${resp.code})${msg}`, colors.red); 307 | break; 308 | } 309 | 310 | continue; 311 | } 312 | } 313 | 314 | const diff = data.time_end - new Date() / 1000; 315 | //cprint(`下一个银瓜子宝箱需要等待${diff.toFixed(3)}秒`, colors.yellow); 316 | 317 | // 2. Get SilverBox award 318 | await sleep(diff * 1000); 319 | resp = await Bilibili.getSilverBox(this.session, data); 320 | data = resp.data; 321 | if (resp.code === 0) { 322 | cprint(`银瓜子: ${data.silver} (+${data.awardSilver})`, colors.green); 323 | if (data.isEnd !== 0) break; 324 | } else { 325 | let msg = resp.message || resp.msg || ''; 326 | let showError = false; 327 | await this.checkErrorResponse(msg).catch((err) => { 328 | showError = true; 329 | msg = err; 330 | }); 331 | if (showError) { 332 | cprint(`银瓜子领取失败: (${resp.code})${msg}`, colors.red); 333 | break; 334 | } 335 | } 336 | } 337 | }; 338 | 339 | return execute().catch(this.reportHttpError); 340 | } 341 | 342 | /** 双端心跳 */ 343 | liveHeart(roomid=164725) { 344 | if (this.usable === false) return null; 345 | 346 | (Promise.all([ 347 | Bilibili.appGetInfoByUser(this.session, { roomid }).catch(console.log), 348 | Bilibili.webGetInfoByUser(this.session, { roomid }).catch(console.log), 349 | ]) 350 | .then(() => { 351 | return Bilibili.webLiveOnlineHeart(this.session, { roomid }); 352 | }) 353 | .then(resp => { 354 | const code = resp['code']; 355 | if (code !== 0) { 356 | const msg = resp['msg'] || resp['message'] || ''; 357 | cprint(`(web)心跳发送失败 (${code})${msg}`, colors.red); 358 | } 359 | return Bilibili.appLiveOnlineHeart(this.session, { roomid }); 360 | }) 361 | .then(resp => { 362 | const code = resp['code']; 363 | if (code !== 0) { 364 | const msg = resp['msg'] || resp['message'] || ''; 365 | cprint(`(app)心跳发送失败 (${code})${msg}`, colors.red); 366 | } 367 | }) 368 | .catch(this.reportHttpError)); 369 | } 370 | 371 | /** 双端领取 */ 372 | liveDoubleWatch() { 373 | if (this.usable === false) return null; 374 | 375 | const execute = async () => { 376 | while (true) { 377 | // await throws error when a promise is rejected. 378 | // execute().catch(...) will handle the error thrown 379 | const taskResp = await Bilibili.liveTaskInfo(this.session); 380 | const doubleStatus = taskResp['data']['double_watch_info']['status']; 381 | const awards = taskResp['data']['double_watch_info']['awards']; 382 | 383 | if (doubleStatus === 1) { 384 | const awardResp = await Bilibili.liveDoubleWatch(this.session); 385 | if (awardResp.code !== 0) { 386 | const msg = awardResp.message || awardResp.msg || ''; 387 | cprint(`双端观看奖励领取失败: (${awardResp.code})${msg}`, colors.red); 388 | } else { 389 | const awardTexts = awards.map(award => `${award.name}(${award.num})`); 390 | const awardText = '双端观看奖励: ' + awardTexts.join(' '); 391 | cprint(awardText, colors.green); 392 | } 393 | break; 394 | } else if (doubleStatus === 2) { 395 | const awardTexts = awards.map(award => `${award.name}(${award.num})`); 396 | const awardText = '双端观看奖励已领取: ' + awardTexts.join(' '); 397 | cprint(awardText, colors.grey); 398 | break; 399 | } else if (doubleStatus === 0) { 400 | cprint('双端观看未完成', colors.grey); 401 | } 402 | 403 | // Need to watch on web & app for at least 5 minutes. To be sure, wait for 6 minutes. 404 | await sleep(1000 * 60 * 6); 405 | } 406 | }; 407 | 408 | return execute().catch(this.reportHttpError); 409 | } 410 | 411 | /** 友爱社签到 */ 412 | idolClubSign() { 413 | if (this.usable === false) return null; 414 | 415 | return Bilibili.loveClubList(this.session).then((respData) => { 416 | 417 | const groups = respData['data']['list']; 418 | const groupInfoList = groups && groups.map((entry) => { 419 | return { 420 | 'group_id': entry['group_id'], 421 | 'owner_id': entry['owner_uid'], 422 | }; 423 | }); 424 | // console.log(groupInfoList); 425 | return groupInfoList; 426 | 427 | }).then((groupInfoList) => { 428 | 429 | const resultList = groupInfoList.map((groupInfo) => { 430 | return Bilibili.loveClubSign(this.session, groupInfo); 431 | }); 432 | return resultList; 433 | 434 | }).then((groupSignResultList) => { 435 | 436 | groupSignResultList.forEach(request => { 437 | request.then((result) => { 438 | const code = result['code']; 439 | if (code === 0) { 440 | cprint('Idol club sign success', colors.green); 441 | } else { 442 | cprint('Idol club sign failed', colors.red); 443 | } 444 | }).catch(this.reportHttpError); 445 | }); 446 | 447 | }).catch(this.reportHttpError); 448 | } 449 | 450 | postRoomEntry(roomid) { 451 | 452 | let promise = null; 453 | 454 | if (this.roomEntered.has(roomid) === false) { 455 | this.roomEntered.add(roomid); 456 | promise = (async () => { 457 | if (this.roomEntered.size > this.maxNumRoomEntered) { 458 | this.roomEntered = new Set([...this.roomEntered].slice(this.roomEntered.size / 2)); 459 | } 460 | 461 | await Bilibili.appRoomEntry(this.session, roomid); 462 | })(); 463 | } else { 464 | promise = Promise.resolve(); 465 | } 466 | 467 | return promise; 468 | } 469 | 470 | joinPK(pk) { 471 | if (this.usable === false) return null; 472 | 473 | if (this.checkBlacklisted()) return null; 474 | 475 | const execute = async () => { 476 | await this.postRoomEntry(pk.roomid); 477 | while (true) { 478 | const resp = await Bilibili.appJoinPK(this.session, pk); 479 | if (resp.code === 0) { 480 | cprint(`${resp.data.award_text}`, colors.green); 481 | break; 482 | } else { 483 | let msg = resp.message || resp.msg || ''; 484 | let showError = false; 485 | await this.checkErrorResponse(msg).catch((err) => { 486 | showError = true; 487 | msg = err; 488 | }); 489 | if (showError) { 490 | cprint(`${pk.id} 获取失败: ${msg}`, colors.red); 491 | break; 492 | } 493 | } 494 | 495 | } 496 | }; 497 | 498 | return execute().catch(this.reportHttpError); 499 | } 500 | 501 | joinGift(gift) { 502 | if (this.usable === false) return null; 503 | 504 | if (this.checkBlacklisted()) return null; 505 | 506 | const execute = async () => { 507 | await this.postRoomEntry(gift.roomid); 508 | while (true) { 509 | const resp = await Bilibili.appJoinGift(this.session, gift); 510 | if (resp.code === 0) { 511 | cprint(`${resp.data.gift_name}+${resp.data.gift_num}`, colors.green); 512 | break; 513 | } else { 514 | let msg = resp.message || resp.msg || ''; 515 | let showError = false; 516 | await this.checkErrorResponse(msg).catch((err) => { 517 | showError = true; 518 | msg = err; 519 | }); 520 | if (showError) { 521 | cprint(`${gift.name} ${gift.id} 获取失败: ${msg}`, colors.red); 522 | break; 523 | } 524 | } 525 | 526 | } 527 | }; 528 | 529 | return execute().catch(this.reportHttpError); 530 | } 531 | 532 | joinGuard(guard) { 533 | if (this.usable === false) return null; 534 | 535 | if (this.checkBlacklisted()) return null; 536 | 537 | const execute = async () => { 538 | await this.postRoomEntry(guard.roomid); 539 | while (true) { 540 | const resp = await Bilibili.appJoinGuard(this.session, guard); 541 | if (resp.code === 0) { 542 | cprint(`${resp.data.message}`, colors.green); 543 | break; 544 | } else { 545 | let msg = resp.message || resp.msg || ''; 546 | let showError = false; 547 | await this.checkErrorResponse(msg).catch((err) => { 548 | showError = true; 549 | msg = err; 550 | }); 551 | if (showError) { 552 | cprint(`${guard.name} ${guard.id} 获取失败: ${msg}`, colors.red); 553 | break; 554 | } 555 | } 556 | 557 | } 558 | }; 559 | 560 | return execute().catch(this.reportHttpError); 561 | } 562 | 563 | joinStorm(storm) { 564 | if (this.usable === false) return null; 565 | 566 | if (this._stormPending) return null; 567 | 568 | if (this.checkBlacklisted()) return null; 569 | 570 | this._stormPending = true; 571 | const start = new Date(); 572 | const tasks = []; // An array of `join` promises 573 | let quit = false; 574 | let i = 0; 575 | let message = ''; 576 | let claimed = false; 577 | let setQuitFlagWhenTimeout = null; 578 | 579 | const join = async () => { 580 | while (!quit) { 581 | const t = Bilibili.appJoinStorm(this.session, storm); 582 | tasks.push(t); 583 | ++i; 584 | await Promise.race( [ t.catch(), sleep(this.stormJoinMaxInterval) ] ); // whichever is done first, 60 ms or `join` request 585 | } 586 | }; 587 | const isDone = (resp) => { 588 | const msg = resp['msg'] || resp['message'] || ''; 589 | if (resp['code'] === 0) { 590 | const giftName = resp['data']['gift_name']; 591 | const giftNum = resp['data']['gift_num']; 592 | const awardText = `${giftName}+${giftNum}`; 593 | message = awardText; 594 | claimed = true; 595 | } 596 | else if (msg.includes('已经领取')) { 597 | claimed = true; 598 | } 599 | return claimed; 600 | }; 601 | const setQuitFlag = async () => { 602 | try { 603 | while (!quit) { 604 | const results = await Promise.all(tasks); 605 | quit = quit || results.some(response => isDone(response)); 606 | } 607 | const results = await Promise.all(tasks); 608 | results.every(response => isDone(response)); 609 | } 610 | catch (error) { 611 | quit = true; 612 | message = `(Storm) - ${error}`; 613 | } 614 | if (setQuitFlagWhenTimeout !== null) { 615 | clearTimeout(setQuitFlagWhenTimeout); 616 | setQuitFlagWhenTimeout = null; 617 | } 618 | }; 619 | 620 | const execute = async () => { 621 | setQuitFlagWhenTimeout = setTimeout(() => { 622 | quit = true; 623 | setQuitFlagWhenTimeout = null; 624 | }, this.abandonStormAfter); 625 | 626 | await Promise.all( [ join(), setQuitFlag() ] ); 627 | 628 | let color = colors.green; 629 | if (claimed) { 630 | message = message || '亿圆已领取'; 631 | } else { 632 | message = message || `风暴 ${storm.id} 已经超过${this.abandonStormAfter / 1000}秒,放弃领取`; 633 | color = colors.red; 634 | } 635 | 636 | cprint(message, color); 637 | cprint(`Executed ${i} times`, colors.green); 638 | this._stormPending = false; 639 | }; 640 | 641 | execute(); 642 | } 643 | 644 | saveTasksToFile() { 645 | const filename = ( 646 | (this.tasksFilename && path.resolve(__dirname, this.tasksFilename)) 647 | || path.resolve(__dirname, 'task-for-user.json')); 648 | 649 | cprint(`Storing task info to ${filename}`, colors.green); 650 | const tasksInfo = JSON.stringify(this.tasks(), null, 4); 651 | fs.writeFile(filename, tasksInfo, (err) => { 652 | if (err) 653 | cprint(`Error storing task info to file`, colors.red); 654 | }); 655 | } 656 | 657 | loadTasksFromFile() { 658 | let filename = path.resolve(__dirname, 'default-task-settings.json'); 659 | if (!!this.tasksFilename === true 660 | && fs.existsSync(path.resolve(__dirname, this.tasksFilename))) { 661 | filename = path.resolve(__dirname, this.tasksFilename); 662 | } 663 | const str = fs.readFileSync(filename); 664 | const data = JSON.parse(str); 665 | const schedules = new Map(); 666 | for (const [name, schedule] of Object.entries(data.taskschedules)) { 667 | if (Array.isArray(schedule)) { 668 | schedules.set(name, schedule); 669 | } 670 | } 671 | for (const [taskname, settings] of Object.entries(data.tasks)) { 672 | if (settings.enabled) { 673 | const type = settings.type; 674 | if (type === 'daily') { 675 | this.register(taskname); 676 | } 677 | else if (type === 'scheduled') { 678 | if (settings.schedule) { 679 | if (schedules.has(settings.schedule)) { 680 | this.register(taskname, { 'timeperiod': schedules.get(settings.schedule) }); 681 | } else { 682 | cprint(`Error in config: task ${taskname} - cannot find schedule name ${settings.schedule}`, colors.red); 683 | } 684 | } 685 | } 686 | } 687 | } 688 | } 689 | 690 | } 691 | 692 | module.exports = AccountRunner; 693 | 694 | })(); 695 | --------------------------------------------------------------------------------