Greasy Fork is available in English.

Bilibili直播弹幕防吞

检测并显示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)
    })

})();