您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
检测并显示B站被B站吞的直播弹幕
// ==UserScript== // @name Bilibili直播弹幕防吞 // @namespace https://github.com/MicroCBer/BilibiliLiveDanmakuSender // @version 0.1.6 // @description 检测并显示B站被B站吞的直播弹幕 // @author MicroBlock // @match https://live.bilibili.com/** // @icon https://www.google.com/s2/favicons?sz=64&domain=bilibili.com // @grant none // @license GPL-3.0-or-later // ==/UserScript== const FAIL_SEND_TIMEOUT=1000; // ms const ENABLE_REST_API_CHECK=true; // 是否开启双重发送失败检测 (function() { 'use strict'; let packageType={ WS_OP_HEARTBEAT: 2, WS_OP_HEARTBEAT_REPLY: 3, WS_OP_MESSAGE: 5, WS_OP_USER_AUTHENTICATION: 7, WS_OP_CONNECT_SUCCESS: 8, WS_PACKAGE_HEADER_TOTAL_LENGTH: 16, WS_PACKAGE_OFFSET: 0, WS_HEADER_OFFSET: 4, WS_VERSION_OFFSET: 6, WS_OPERATION_OFFSET: 8, WS_SEQUENCE_OFFSET: 12, WS_BODY_PROTOCOL_VERSION_NORMAL: 0, WS_BODY_PROTOCOL_VERSION_BROTLI: 3, WS_HEADER_DEFAULT_VERSION: 1, WS_HEADER_DEFAULT_OPERATION: 1, WS_HEADER_DEFAULT_SEQUENCE: 1, WS_AUTH_OK: 0, WS_AUTH_TOKEN_ERROR: -101 } let headerList=[{ name: "Header Length", key: "headerLen", bytes: 2, offset: packageType.WS_HEADER_OFFSET, value: packageType.WS_PACKAGE_HEADER_TOTAL_LENGTH }, { name: "Protocol Version", key: "ver", bytes: 2, offset: packageType.WS_VERSION_OFFSET, value: packageType.WS_HEADER_DEFAULT_VERSION }, { name: "Operation", key: "op", bytes: 4, offset: packageType.WS_OPERATION_OFFSET, value: packageType.WS_HEADER_DEFAULT_OPERATION }, { name: "Sequence Id", key: "seq", bytes: 4, offset: packageType.WS_SEQUENCE_OFFSET, value: packageType.WS_HEADER_DEFAULT_SEQUENCE }] let decode=function(t) { return decodeURIComponent(window.escape(String.fromCharCode.apply(String, new Uint8Array(t)))) } let convertToObject = function(t) { var e = new DataView(t) , n = { body: [] }; if (n.packetLen = e.getInt32(packageType.WS_PACKAGE_OFFSET), headerList.forEach(function(t) { 4 === t.bytes ? n[t.key] = e.getInt32(t.offset) : 2 === t.bytes && (n[t.key] = e.getInt16(t.offset)) }), n.packetLen < t.byteLength && convertToObject(t.slice(0, n.packetLen)), (window.TextDecoder ? new window.TextDecoder : { decode: function(t) { return decodeURIComponent(window.escape(String.fromCharCode.apply(String, new Uint8Array(t)))) } }), !n.op || packageType.WS_OP_MESSAGE !== n.op && n.op !== packageType.WS_OP_CONNECT_SUCCESS) n.op && packageType.WS_OP_HEARTBEAT_REPLY === n.op && (n.body = { count: e.getInt32(packageType.WS_PACKAGE_HEADER_TOTAL_LENGTH) }); else for (var i = packageType.WS_PACKAGE_OFFSET, s = n.packetLen, a = "", u = ""; i < t.byteLength; i += s) { s = e.getInt32(i), a = e.getInt16(i + packageType.WS_HEADER_OFFSET); try { if (n.ver === packageType.WS_BODY_PROTOCOL_VERSION_NORMAL) { var c = decode(t.slice(i + a, i + s)); u = 0 !== c.length ? JSON.parse(c) : null } else if (n.ver === packageType.WS_BODY_PROTOCOL_VERSION_BROTLI) { var l = t.slice(i + a, i + s) , h = window.BrotliDecode(new Uint8Array(l)); u = convertToObject(h.buffer).body } u && n.body.push(u) } catch (e) { console.err("decode body error:", new Uint8Array(t), n, e) } } return n } let accepted_texts=[],enabled=false let danmaku_local_save=[] let _WebSocket=WebSocket class FakeWs{ constructor(...args){ let ws = new _WebSocket(...args) if(args[0].includes("chat")){ ws.addEventListener("open",()=>{ if(!enabled){ waitfor(".chat-item .danmaku-item-right.v-middle.pointer").then(()=>{ danmaku_local_save=get_danmu(true); enabled=true }) } }) ws.addEventListener("message",(msg)=>{ if(!enabled){ danmaku_local_save=get_danmu(true); enabled=true } let data=(convertToObject(msg.data)) if(data.op===5){ for(let message of data.body){ if(message[0]&&message[0].cmd==="DANMU_MSG"){ accepted_texts.push(message[0].info[1]) } } } }) ws.addEventListener("error",()=>{ enabled=false }) ws.addEventListener("close",()=>{ enabled=false }) } return ws } } WebSocket=FakeWs let dicts=[],sensitiveChars={'.':'*'}; async function addDict(url){ let resp=await(await fetch(url)).text() dicts.push(...resp.replace(/\r/g,"").split("\n")); } addDict("https://cdn.jsdelivr.net/gh/MicroCBer/BilibiliLiveDanmakuSender/dict.txt") function waitfor(selector){ return new Promise((rs)=>{ let handle=setInterval(()=>{ if(document.querySelector(selector)){ rs(); clearInterval(handle); } },100) }) } function get_danmu(received=false){ return [...document.querySelectorAll(".chat-item .danmaku-item-right.v-middle.pointer")].map( v=>({ text:v.innerText, dom:v, uid:v.parentElement.getAttribute("data-uid"), received, time:new Date().getTime() })).filter(v=>v.uid===document.querySelector(".user-panel-ctnr").children[0].getAttribute("href").split("/").pop()) } function send_message(msg){ var inpEle = document.querySelector(".chat-input") var t = inpEle let evt = document.createEvent('HTMLEvents'); evt.initEvent('input', true, true); t.value=msg; t.dispatchEvent(evt) var event = document.createEvent('Event') event.initEvent('keydown', true, false) event = Object.assign(event, { ctrlKey: false, metaKey: false, altKey: false, which: 13, keyCode: 13, key: 'Enter', code: 'Enter' }) inpEle.focus() inpEle.dispatchEvent(event) } waitfor(".chat-item .danmaku-item-right.v-middle.pointer").then(()=>{ async function update_danmu(){ let last=danmaku_local_save[danmaku_local_save.length-1] let now=get_danmu() let pos=now.findLastIndex(v=>v.text===last.text); let updated=now.slice(pos+1) for(let msg of updated){ msg.dom.style.color="#0169ff"; } // Receive messages for(let msg of danmaku_local_save){ let index=accepted_texts.indexOf(msg.text) if(index!=-1&&!msg.received){ accepted_texts.splice(index,1) msg.received=true msg.dom.style.color="" msg.dom.style.background=""; function removeBtn(){ if(msg.dom.parentElement.lastChild.tagName==="BUTTON") msg.dom.parentElement.lastChild.remove(); } removeBtn() removeBtn() } } function auto_avoid_kw(_text,max_length=20){ let text=_text const SEPARATOR="/" if(text.length<=max_length/2)return text.split('').join(SEPARATOR) for(let word of dicts){ if(text.includes(word))text=text.replace(word,word.split('').join(SEPARATOR)) if(text.length==max_length)return text } for(let char in sensitiveChars){ while(text.includes(char))text=text.replace(char,sensitiveChars[char]); } if(text.length>max_length)return null return text } for(let msg of danmaku_local_save){ if(!msg.received&&msg.time<(new Date().getTime()-FAIL_SEND_TIMEOUT)&&!msg.failed){ msg.failed=true if(ENABLE_REST_API_CHECK){ msg.dom.style.background="#feb13a"; msg.dom.style.color="white" let roomId=__NEPTUNE_IS_MY_WAIFU__?.roomInitRes?.data?.room_id || __SSR_INITIAL_STATE__.baseInfoRoom.room_info.room_id let data=await(await fetch("https://api.live.bilibili.com/xlive/web-room/v1/dM/gethistory?roomid="+roomId)).json() if(data.data.room.findIndex(v=>v.text===msg.text)!==-1){ msg.received=true msg.dom.style.color=""; msg.dom.style.background=""; continue; } } msg.dom.style.background="#b22727"; msg.dom.style.color="white" function buildBtn(text,onclick){ let btn=document.createElement("button") btn.innerText=text btn.onclick=onclick return btn } msg.dom.parentElement.appendChild(buildBtn("手动修改",()=>{ let resp=prompt("请输入修改后的弹幕",msg.text) if(resp)send_message(resp) })) if(auto_avoid_kw(msg.text)) msg.dom.parentElement.appendChild(buildBtn("尝试自动修改",()=>{ send_message(auto_avoid_kw(msg.text)) })) } } danmaku_local_save.push(...updated); } setInterval(()=>{ if(!danmaku_local_save.length){ danmaku_local_save=get_danmu(true); } if(enabled){ update_danmu() } },100) }) })();