快捷弹幕

B站直播间发送快捷弹幕

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name         快捷弹幕
// @namespace    http://tampermonkey.net/
// @version      0.5.5
// @description  B站直播间发送快捷弹幕
// @author       mianju
// @include      /https?:\/\/live\.bilibili\.com\/(blanc\/)?\d+\??.*/
// @require      https://cdn.staticfile.org/axios/0.27.2/axios.min.js
// @grant        none
// ==/UserScript==

(function () {
  function main() {
    initLib()
    initCss()
    waitForLoaded(() => {
      initUi()
    })
  }

  function initLib() {
    let scriptElement = document.createElement('script')
    scriptElement.src = 'https://cdn.staticfile.org/vue/2.6.9/vue.min.js'
    document.head.appendChild(scriptElement)

    let linkElement = document.createElement('link')
    linkElement.rel = 'stylesheet'
    linkElement.href = 'https://cdn.bootcdn.net/ajax/libs/element-ui/2.15.14/theme-chalk/index.css'
    document.head.appendChild(linkElement)

    scriptElement = document.createElement('script')
    scriptElement.src = 'https://cdn.bootcdn.net/ajax/libs/element-ui/2.15.14/index.js'
    document.head.appendChild(scriptElement)
  }

  function initCss() {
    let css = `
      .el-select .el-input {
          width: 80px;
        }
        .input-with-select .el-input-group__prepend {
          background-color: #fff;
        }
    `
    let styleElement = document.createElement('style')
    styleElement.innerText = css
    document.head.appendChild(styleElement)
  }

  function waitForLoaded(callback, timeout = 10 * 1000) {
    let startTime = new Date()
    function poll() {
      if (isLoaded()) {
        callback()
        return
      }

      if (new Date() - startTime > timeout) {
        return
      }
      setTimeout(poll, 1000)
    }
    poll()
  }

  function isLoaded() {
    if (window.ELEMENT === undefined) {
      return false
    }
    if (document.querySelector('#control-panel-ctnr-box') === null) {
      return false
    }
    return true
  }
  function initUi() {
      var father = document.getElementsByClassName('icon-left-part')[0];
      let quickDanmakuElement = document.createElement('div');
      father.appendChild(quickDanmakuElement);

      new Vue({
          el: quickDanmakuElement,
          template: `
          <span>
        <template style="width: 200px" >
            <el-select v-model="value" placeholder="选择" @change="sendDanmaku" style="width: 80px;height: 24px" size="mini">
                <el-option-group
                    v-for="group in options"
                    :key="group.label"
                    :label="group.label">
                    <el-option
                    v-for="danmaku in group.danmakus"
                    :key="danmaku.label"
                    :label="danmaku.label"
                    :value="danmaku.label">
                    </el-option>
                </el-option-group>
            </el-select>
            <el-button icon="el-icon-message" size="mini" circle @click="sendDanmaku(value)"></el-button>
            <el-popover
                placement="bottom"
                title="快捷弹幕设置"
                width="300px"
                trigger="click"
                >
                <span class="demonstration">添加: </span>
                <el-tooltip class="item" effect="dark" content="添加表情包教程:https://bbs.nga.cn/read.php?pid=594434351&opt=128" placement="top">
                <el-input placeholder="请输入内容" v-model="danmaku4add" class="input-with-select" maxlength="20" size="mini" style="width: 300px">
                    <el-select v-model="select" slot="prepend" placeholder="请选择">
                        <el-option label="当前" value="cur"></el-option>
                        <el-option label="所有" value="all"></el-option>
                    </el-select>
                    <el-button slot="append" icon="el-icon-check" @click="addDanmaku" size="mini"></el-button>
                </el-input>
                </el-tooltip>
                <div class="block" style="margin-top: 10px">
                    <span class="demonstration">删除: </span>
                    <el-cascader
                      size="mini"
                      v-model="danmakus4del"
                      :options="options"
                      :props="{ expandTrigger: 'hover','children': 'danmakus','multiple': 'true','emitPath':'false' }"
                      ></el-cascader>
                      <el-button type="danger" icon="el-icon-delete" size="mini" @click="delDanmakus"></el-button>
                </div>
                <el-button slot="reference" icon="el-icon-setting" size="mini" circle></el-button>
                <div style="margin-top: 10px">
                    自动发送:
                    <el-radio-group v-model="freq" size="mini">
                         <el-radio-button label="高频"></el-radio-button>
                         <el-radio-button label="正常"></el-radio-button>
                         <el-radio-button label="低频"></el-radio-button>
                    </el-radio-group>
                    <el-switch
                        size="mini"
                        v-model="value4switch"
                        :disabled="flag4switch"
                        @change="autoSend">
                    </el-switch>
                </div>
            </el-popover>
        </template>
    </span>
          `,
          data: {
                options: [
                    {
                        label: '所有直播间',
                        value: 'all',
                        danmakus: []
                    },
                    {
                        label: '当前直播间',
                        value: 'cur',
                        danmakus: []
                    }
                ],
                value: '',
                danmaku4add: '',
                select: 'cur',
                danmakus4del: [],
                roomId: 0,
                allRoomDanmakus: [],
                curRoomDanmakus: [],
                key4all: 'allRoomDanmukus',
                key4cur: '',
                value4switch: false,
                interval: null,
                freq: '正常',
                freqopts: {'高频':[30,6000],'正常':[15,12000],'低频':[10,18000]},
                chatInput: null
            },
            methods: {
                "delDanmakus": function () {
                    function del(val,danmakus) {
                        let index = danmakus.indexOf(val);
                        danmakus.splice(index,1);
                    };
                    for (let i=0;i<this.danmakus4del.length;i++){
                        let danmaku = this.danmakus4del[i];
                        if (danmaku[0] == "cur") {
                            del(danmaku[1],this.curRoomDanmakus);
                        } else {
                            del(danmaku[1],this.allRoomDanmakus);
                        }
                    }
                    localStorage.setItem(this.key4cur,JSON.stringify(this.curRoomDanmakus));
                    localStorage.setItem(this.key4all,JSON.stringify(this.allRoomDanmakus));
                    this.danmakus4del = [];
                    this.$message.success("删除成功");
                    this.handleDanmakus();
                },
                "getRoomId": function () {
                    if (window.__NEPTUNE_IS_MY_WAIFU__) {
                        this.roomId = window.__NEPTUNE_IS_MY_WAIFU__.roomInfoRes.data.room_info.room_id;
                    } else {
                        let url = document.URL;
                        var re = /\/\d+/.exec(url);
                        this.roomId = re[0].substr(1);
                    }
                },
                "getDanmakus": function (key) {
                    if (localStorage.getItem(key)){
                        return JSON.parse(localStorage.getItem(key));
                    }
                    return [];
                },
                "handleDanmakus": function () {
                    function handle(danmakus4opt,danmakus4room){
                        danmakus4opt.splice(0);
                        for (let i=0;i<danmakus4room.length;i++) {
                            danmakus4opt.push({label:danmakus4room[i],value:danmakus4room[i]});
                        }
                    }
                    handle(this.options[0].danmakus,this.allRoomDanmakus);
                    handle(this.options[1].danmakus,this.curRoomDanmakus);
                },
                "addDanmaku": function () {
                    function add(key,danmakus,danmaku){
                        danmakus.push(danmaku);
                        localStorage.setItem(key,JSON.stringify(danmakus));
                    };
                    if (this.select == "cur") {
                        add(this.key4cur,this.curRoomDanmakus,this.danmaku4add);
                    } else {
                        add(this.key4all,this.allRoomDanmakus,this.danmaku4add);
                    }
                    this.danmaku4add = '';
                    this.$message.success("添加成功");
                    this.handleDanmakus();
                },
                "sendDanmaku": function (danmaku4send) {
                    var patt = /(room|official)(_\d+){1,2}/
                    var jct = getCookie('bili_jct');
                    var date = new Date();
                    var data = new FormData();
                    data.append('bubble',0);
                    data.append('color',16777215);
                    data.append('fontsize',25);
                    data.append('mode',1);
                    data.append('msg',danmaku4send);
                    data.append('rnd',parseInt(date.getTime()/1000));
                    data.append('roomid',this.roomId);
                    data.append('csrf',jct);
                    data.append('csrf_token',jct);
                    if (patt.test(danmaku4send)) {
                        data.set('msg',patt.exec(danmaku4send)[0])
                        data.append('dm_type',1);
                    }
                    let apiClient = axios.create({
                        baseURL: 'https://api.live.bilibili.com',
                        withCredentials: true
                    });
                    apiClient.post('/msg/send',data).then((res)=>{
                        if (res.data.code == 0) {
                            switch (res.data.msg) {
                                case '':
                                    this.$message.success('发送成功 - ' + danmaku4send);
                                    break;
                                /*case 'f':
                                    this.$message.error('发送失败 - 包含B站屏蔽词: ' + danmaku4send);
                                    break;
                                case 'k':
                                    this.$message.error('发送失败 - 包含直播间屏蔽词: ' + danmaku4send);
                                    break;*/
                                case 'same restriction':
                                    this.$message.error('发送失败,该弹幕已被限制,请选择其它弹幕!');
                                    break;
                                case 'max limit exceeded':
                                    this.$message.error('发送失败,弹幕池达到上限!');
                                    break;
                                default:
                                    this.$message.error('发送失败 - ' + res.data.message);
                            }
                        } else {
                            this.$message.error('发送失败 - ' + res.data.message);
                        }
                    }).catch(()=>{
                        this.$message.error('发送失败 - ' + danmaku4send);
                    });
                },
                "autoSend": function(flag) {
                    if (flag) {
                        this.$message.info('自动发送已开启');
                        let count = 0;
                        let _this = this
                        let opt = this.freqopts[this.freq]
                        this.interval = setInterval(
                            ()=>{
                                count++;
                                if (count == opt[0]) {
                                    _this.value4switch = false;
                                    clearInterval(_this.interval)
                                    _this.$message.info('自动发送已关闭');
                                }
                                _this.sendDanmaku(_this.value);
                            },opt[1]
                        );
                    } else {
                        this.$message.info('自动发送已关闭');
                        clearInterval(this.interval);
                    }
                },
                "sendListener": function() {
                    var _this = this;
                    function keydown(event) {
                        if (event.keyCode == 13) {
                            if (_this.chatInput.chatInput != '') {
                                _this.sendDanmaku(_this.chatInput.chatInput);
                            }
                            setTimeout(_this.chatInput.clearInput, 10);
                        }
                    };
                    this.chatInput.onKeyDown = keydown;
                }
            } ,
            created: function () {
                this.getRoomId();
                this.key4cur = this.roomId + '-danmukus';
                this.curRoomDanmakus = this.getDanmakus(this.key4cur);
                this.allRoomDanmakus = this.getDanmakus(this.key4all);
                this.handleDanmakus();
                this.chatInput = document.querySelector('#control-panel-ctnr-box').__vue__;
                this.sendListener();
            },
            computed: {
                flag4switch: function() {
                    return (this.value == '')
                }
            }
      });
  }
  function getCookie(cname){
    var name = cname + "=";
    var ca = document.cookie.split(';');
    for(var i=0; i<ca.length; i++) {
        var c = ca[i].trim();
        if (c.indexOf(name)==0) { return c.substring(name.length,c.length); }
    }
    return "";
  }
  main()
})();