// ==UserScript==
// @name B站直播免费人气票助手
// @namespace https://greasyfork.org/users/1001518
// @version 0.1
// @description B站直播间自动投免费人气票
// @author DianaBlessU
// @match https://live.bilibili.com/*
// @icon https://bilibili.com/favicon.ico
// @require https://greasyfork.org/scripts/473817-gmx-menu/code/GMX_menu.js?version=1240400
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @license MIT
// ==/UserScript==
(function() {
'use strict';
if (!location.pathname.match(/^\/(\d+)/)) return
const sleep = ms => {
return new Promise(resolve => setTimeout(resolve, ms))
}
function retry(fn, options){
let defaults = {tries: 3, interval: 500, this: null, onerror: null}
options = Object.assign(defaults, options ?? {})
return async function(...args) {
let count = 0
while (true){
try {
return await fn.apply(options.this ?? this, args);
}
catch (err){
count += 1
if (count == options.tries){
throw err
}
options.onerror && options.onerror(err, count, options);
if (options.interval){
await sleep(options.interval);
}
}
}
}
}
class BiliError extends Error {
constructor(code = code, ...params) {
// Pass remaining arguments (including vendor specific ones) to parent constructor
super(...params);
// Maintains proper stack trace for where our error was thrown (only available on V8)
if (Error.captureStackTrace) {
Error.captureStackTrace(this, BiliError);
}
this.name = "BiliError";
// Custom debugging information
this.code = code
}
}
class BiliApi {
_retriables = ["getRoomInitInfo", "getPopularAnchorRank", "getUserPopularTicketsNum", "popularRankFreeScoreIncr", "sendMsg", "likeInteract"]
constructor() {
for (let method of this._retriables){
this[method] = this._retry(this[method])
}
}
_retry(fn, options){
let mods = {onerror: function(err, count, options){
console.log(`${fn.name} 执行失败, 将进行第${count}次重试`)
}}
options = Object.assign(mods, options ?? {})
return retry(fn, options);
}
async getRoomInitInfo(room_id){
let response = await fetch(`https://api.live.bilibili.com/room/v1/Room/room_init?id=${room_id}`, {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
credentials: "include",
})
let data = await response.json() ?? {};
if (data.code == 0){
return data.data;
}else{
throw new BiliError(data.code, 'getRoomInitInfo error: ' + JSON.stringify(data));
}
}
async getPopularAnchorRank (ruid){
const cookie = window.document.cookie;
const uid = cookie.match(/DedeUserID=(\w+)/)[1];
let response = await fetch(`https://api.live.bilibili.com/xlive/general-interface/v1/rank/getPopularAnchorRank?uid=${uid}&ruid=${ruid}&clientType=2`, {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
credentials: "include",
})
let data = await response.json() ?? {};
if (data.code == 0){
return data.data;
}else{
throw new BiliError(data.code, 'getPopularAnchorRank() error: ' + JSON.stringify(data));
}
}
async getUserPopularTicketsNum(ruid){
const cookie = window.document.cookie;
const uid = cookie.match(/DedeUserID=(\w+)/)[1];
let response = await fetch(`https://api.live.bilibili.com/xlive/general-interface/v1/rank/getUserPopularTicketsNum?ruid=${ruid}&source=0`, {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
credentials: "include",
})
let data = await response.json() ?? {};
if (data.code == 0){
return data.data;
}else{
throw new BiliError(data.code, 'getUserPopularTicketsNum() error: ' + JSON.stringify(data));
}
}
async popularRankFreeScoreIncr(ruid){
const cookie = window.document.cookie;
const csrf = cookie.match(/bili_jct=(\w+)/)[1];
let response = await fetch("https://api.live.bilibili.com/xlive/general-interface/v1/rank/popularRankFreeScoreIncr", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: `ruid=${ruid}&csrf_token=${csrf}&csrf=${csrf}&visit_id=`,
credentials: "include",
})
let data = await response.json() ?? {};
if (data.code == 0){
return data.data?.num;
}else{
throw new BiliError(data.code, 'popularRankFreeScoreIncr() error: ' + JSON.stringify(data))
}
}
async sendMsg(roomid, msg){
const cookie = window.document.cookie;
const csrf = cookie.match(/bili_jct=(\w+)/)[1];
let danmakus = [
"(⌒▽⌒).",
"( ̄▽ ̄).",
"(=・ω・=).",
"(`・ω・´).",
"(〜 ̄△ ̄)〜.",
"(・∀・).",
"(°∀°)ノ.",
"( ̄3 ̄).",
"╮( ̄▽ ̄)╭.",
"_(:3」∠)_.",
"(^・ω・^ ).",
"(● ̄(エ) ̄●).",
"ε=ε=(ノ≧∇≦)ノ.",
"⁄(⁄ ⁄•⁄ω⁄•⁄ ⁄)⁄.",
"←◡←.",
]
let formData = new FormData();
formData.set("bubble", 0);
formData.set("color", 16777215);
formData.set("mode", 1);
formData.set("fontsize", 25);
formData.set("csrf", csrf);
formData.set("csrf_token", csrf);
formData.set("msg", msg ?? danmakus[(Math.random() * 100 >> 0) % danmakus.length]);
formData.set("roomid", roomid);
formData.set("rnd", Math.floor(new Date() / 1000));
let response = await fetch("//api.live.bilibili.com/msg/send", {
method: 'POST',
credentials: 'include',
body: formData
})
let data = await response.json() ?? {};
if (data.code == 0){
return data
}else{
throw new BiliError(data.code, 'sendMsg() error: ' + JSON.stringify(data));
}
}
async likeInteract(roomid){
const cookie = window.document.cookie;
const csrf = cookie.match(/bili_jct=(\w+)/)[1];
let response = await fetch("https://api.live.bilibili.com/xlive/web-ucenter/v1/interact/likeInteract", {
"method": "POST",
"headers": {
"content-type": "application/x-www-form-urlencoded",
"sec-ch-ua": "Mozilla/5.0 BiliDroid/6.73.1 (bbcallen@gmail.com) os/android model/Mi 10 Pro mobi_app/android build/6731100 channel/xiaomi innerVer/6731110 osVer/12 network/2",
},
"body": `roomid=${roomid}&csrf_token=${csrf}&csrf=${csrf}&visit_id=`,
"mode": "cors",
"credentials": "include"
})
let data = await response.json() ?? {};
if (data.code == 0){
return data
}else{
throw new BiliError(data.code, 'likeInteract() error: ' + JSON.stringify(data));
}
}
}
class Echo {
speaker = "Echo";
badget_style = "display: inline-block ; background-color: #fc966e ; color: black ; font-weight: bold ; padding: 0px 4px ; border-radius: 3px ;";
body_style = "color: inherit";
constructor(speaker) {
this.speaker = speaker
}
format (str){return `%c${this.speaker}%c ${new Date().toLocaleString()} ${str}`};
log (str){console.log(this.format(str), this.badget_style, this.body_style)};
info (str){console.info(this.format(str), this.badget_style, this.body_style)};
warn (str){console.warn(this.format(str), this.badget_style, this.body_style)};
error (str){console.error(this.format(str), this.badget_style, this.body_style)};
debug (str){console.debug(this.format(str), this.badget_style, this.body_style)};
}
function getPromiseState(p){
const t = {};
return Promise.race([p, t]).then(v => v === t ? "pending" : "fulfilled", () => "rejected")
}
function getDeadline(ts){
return ts - ts % 3600 + 3600
}
function getTimestamp(dt){
return parseInt((dt ? new Date(dt) : new Date()) / 1000)
}
const api = new BiliApi()
class TicketBot {
interval = 300000
constructor(options) {
this.running = false;
this.paused = false;
this.busywith = null;
this.curr_iid = 0;
this.live_time = -62170012800;
this.logger = options?.logger ?? console
}
async _process(forced) {
try{
var room_id = location.pathname.match(/^\/(\d+)/)[1];
let init_info = await api.getRoomInitInfo(room_id);
room_id = init_info.room_id;
if (init_info.live_status !== 1) {
this.logger.log("直播间未开播");
if(!forced) return
}
let live_time = init_info.live_time;
if (this.live_time != live_time) {
this.live_time = live_time
if(init_info.live_status === 1){
this.logger.log(`本场开播时间:${new Date(live_time*1000).toLocaleString()}`)
}
}
await sleep(500);
let anchor_info = await api.getPopularAnchorRank(init_info.uid);
if (!anchor_info.user_medal.level){
this.logger.log("未加入粉丝团,无法获取免费人气票");
return
}
//await api.likeInteract(room_id);
if (!anchor_info.user_medal.is_light) {
this.logger.log("粉丝牌未点亮,尝试发弹幕点亮牌子");
await sleep(500);
await api.sendMsg(room_id);
return
}
if (!forced) {
let ddl0_time = getDeadline(live_time); // 开播后第一轮投票截止时间
// 处于开播后第一轮投票周期内,且开播不到15分钟就截止投票的情况
if (getTimestamp() < ddl0_time + 10 && ddl0_time - live_time < 900) {
this.logger.log("开播时间临近结算时间,待下一个整点后再投票");
return
}
}
await sleep(500);
let ticket_info = await api.getUserPopularTicketsNum(init_info.uid);
if (ticket_info.free_ticket.num > 0) {
this.logger.log(`剩余${ticket_info.free_ticket.num}张免费人气票,即将投出`);
await sleep(500);
let num = await api.popularRankFreeScoreIncr(init_info.uid);
this.logger.log(`成功投出免费人气票${num}张`);
}else{
this.logger.log(`没有余票`);
};
}catch(err){
this.logger.error(err)
}
}
async process(forced) {
this.busywith = this._process(forced);
await this.busywith;
}
async pause() {
if (!this.paused){
this.paused = true;
clearInterval(this.curr_iid);
await this.busywith;
this.running = false;
this.logger.log("已暂停");
}
}
async start() {
if (this.paused){
this.paused = false;
if (await getPromiseState(this.busywith)=="pending") {
this.logger.debug("正在恢复...");
await this.busywith;
await sleep(500);
}
this.loop_forever(true);
this.logger.log("已从暂停状态恢复");
} else {
this.loop_forever();
}
}
loop_forever(overridden){
if (this.running && !overridden) return
this.running = true;
this.curr_iid = setInterval(()=>{
this.process();
}, this.interval)
this.process(); // 设好定时器后立即执行
this.logger.log("主循环已启动");
}
}
const echo = new Echo("TicketBot");
const bot = new TicketBot({logger:echo});
const activity = {
skd_tid : 0,
// for cmd_start
start: function(){
activity.skd_tid && clearTimeout(activity.skd_tid)
bot.start()
},
// for cmd_pause
pause: function(){
activity.skd_tid && clearTimeout(activity.skd_tid)
bot.pause()
},
// for cmd_schedule
schedule:async function(){
let ts = getTimestamp();
let ddl = getDeadline(ts)
await bot.pause();
echo.log(`已创建计划,将在${new Date(ddl*1000).toLocaleString()}启动`);
activity.skd_tid = setTimeout(()=>{
activity.skd_tid = 0;
GMX_menu.isChecked("cmd_schedule") && GMX_menu.triggerSelect("cmd_start")
},(ddl - ts)*1000)
}
}
const gmx_menu_items = [
{name: "cmd_start", text: "启动自动投票", checked: true, group: "activity", callback: activity.start},
{name: "cmd_pause", text: "暂停自动投票", checked: false, group: "activity", callback: activity.pause},
{name: "cmd_schedule", text: "暂停直到整点启动", checked: false, group: "activity", callback: activity.schedule},
{name: "sep_1", separator: true},
{name: "cmd_shoot", text: "⚡️ 立即投票", callback: ()=>bot.process(true)},
]
async function main(){
await sleep(5000);
GMX_menu.install({items: gmx_menu_items})
bot.start();
}
main();
})();