- // ==UserScript==
- // @name b站直播聊天室弹幕发送增强
- // @namespace http://tampermonkey.net/
- // @version 0.4.2
- // @description 原理是分开发送。接管了发送框,会提示屏蔽词
- // @author Pronax
- // @include /https:\/\/live\.bilibili\.com\/(blanc\/)?\d+/
- // @icon http://bilibili.com/favicon.ico
- // @grant GM_addStyle
- // @run-at document-end
- // @require https://greasyfork.org/scripts/439903-blive-room-info-api/code/blive_room_info_api.js?version=1037039
- // ==/UserScript==
-
- // todo 分段指针
- // todo 接管全屏输入栏
- // todo 自动判断屏蔽词 23237777
-
- ; (async function () {
- 'use strict';
-
- if (!document.cookie.match(/bili_jct=(\w*); /)) { return; }
-
- // proxyFetch(); // 魔改fetch
-
- let jct = document.cookie.match(/bili_jct=(\w*); /)[1];
- let roomId = await ROOM_INFO_API.getRid();
- let toastCount = 0;
- let isProcessing = false;
- let formData = new FormData();
- formData.set("bubble", 0);
- formData.set("color", 16777215);
- formData.set("mode", 1);
- formData.set("fontsize", 25);
- formData.set("roomid", roomId);
-
- const LIMIT = await ROOM_INFO_API.getDanmuLength(roomId);
-
- const riverCrabs = { "168": "f", "母鸡": "f", "神仙水": "f", "小赤佬": "f", "速器": "fire", "商丘": "fire", "慎判": "fire", "代练": "f", "违规直播": "f", "低俗": "f", "系统": "f", "渣女": "f", "肥": "fire", "墙了": "f", "变质": "f", "小熊": "f", "疫情": "f", "感染": "f", "分钟": "f", "爽死": "f", "黑历史": "f", "超度": "f", "渣男": "f", "和谐": "f", "河蟹": "f", "敏感": "f", "你妈": "f", "代孕": "f", "硬了": "f", "抖音": "f", "保卫": "f", "被gan": "f", "寄吧": "f", "郭楠": "f", "里番": "f", "小幸运": "f", "试看": "f", "加QQ": "f", "警察": "f", "营养": "f", "资料": "f", "家宝": "f", "饿死": "f", "不认字": "f", "横幅": "f", "hentai": "f", "诱惑": "f", "垃圾": "f", "福报": "f", "拉屎": "f", "顶不住": "f", "一口气": "f", "苏联": "f", "哪个平": "f", "老鼠台": "f", "顶得住": "f", "gay": "f", "黑幕": "f", "蜀黍我啊": "f", "梯子": "f", "美国": "f", "米国": "f", "未成年": "f", "爪巴": "f", "包子": "fire", "党": "fire", "89": "fire", "戏精": "fire", "八九": "fire", "八十九": "fire", "你画我猜": "fire", "叔叔我啊": "fire", "爬": "fire" };
- let wordTree = {};
- initTree();
-
- // 弹出框CSS
- GM_addStyle(".link-toast.error{left:40px;right:40px;white-space:normal;margin:auto;text-align:center;box-shadow:0 .2em .1em .1em rgb(255 100 100/20%)}");
- // 原始粉丝牌
- // GM_addStyle("#control-panel-ctnr-box .medal-section{padding-left:0}");
- // 发送按钮 CSS
- GM_addStyle(".chat-input-focus button.right-action-btn{background-color:var(--brand_pink);}.chat-input-ctnr-new button.right-action-btn{min-width: 50px;}.chat-input-ctnr-new button.right-action-btn:hover{background-color:var(--brand_pink);}.chat-input-ctnr-new div.right-actions{margin-right:5px;}");
- // 输入框及母盒子
- GM_addStyle("#control-panel-ctnr-box{padding:10px 8px 8px}#control-panel-ctnr-box>.chat-input-ctnr-new{margin-top:10px;align-items:center;height:fit-content !important;border-radius:10px;min-height:52px;}");
- // 插件输入框及背景框的公共CSS
- GM_addStyle("div.chat-input-ctnr-new>.chat-input-new.default-height{height:fit-content !important;}.chat-input-new>.textarea-panel.input-area{scrollbar-width:thin;overflow-x:hidden;}.chat-input-new>.textarea-panel.input-area::-webkit-scrollbar{width:4px;}#liveDanmuInputBackground,#liveDanmuInputArea.focus{line-height:19px;height:87px;margin-left:10px;}");
- // @别人的提示标志
- GM_addStyle("#liveDanmuAtLabel,.at #liveDanmuInputBackground::before{content:'@'attr(data-at);border-radius:2px;background-color:var(--Pi4_u);box-shadow:0 0 0 1px var(--Pi4_u);padding:0 3px;margin:0 5px 0 3px;color:var(--text_white)}");
-
- // 用于回复@别人
- let at = {
- username: undefined,
- uid: undefined,
- calDom: undefined,
- set user(username) {
- this.username = username;
- if (!this.username) {
- this.uid = undefined;
- let inputParentBox = document.querySelector(".chat-input-new");
- requestAnimationFrame(() => {
- inputParentBox.classList.remove("at");
- });
- inputArea.dom.style.textIndent = '';
- return;
- }
- // 计算字符长度赋值给text-indent
- at.calDom.innerText = "@" + this.username;
- // 查找UID
- let danmuDom = document.querySelector(`.chat-item[data-uname="${this.username}"]`);
- if (danmuDom && danmuDom.dataset.uid) {
- this.uid = danmuDom.dataset.uid;
- let inputParentBox = document.querySelector(".chat-input-new");
- requestAnimationFrame(() => {
- inputParentBox.classList.add("at");
- });
- inputArea.dom.dataset.at = this.username;
- inputArea.bgDom.dataset.at = this.username;
- let width = parseInt(getComputedStyle(at.calDom).width) + 15; // 我也不知道为什么加这个数
- inputArea.dom.style.textIndent = width + 'px';
- } else {
- console.warn(`初始化@数据失败:无法找到${this.username}的弹幕`);
- }
- }
- }
- let inputArea = {
- dom: undefined,
- bgDom: undefined,
- limitHintDom: undefined,
- _textValue: '',
- _htmlValue: '',
- _internalTimeout: undefined,
- set textValue(val) {
- this._textValue = val;
- this.bgDom.innerText = this._textValue;
- this.bgDom.scrollTop = this.dom.scrollTop;
- // 屏蔽词过滤
- clearTimeout(this._internalTimeout);
- this._internalTimeout = setTimeout(() => {
- inputArea.htmlValue = filter(inputArea.textValue);
- }, 170);
- // 指示器内容更新
- if (this.textValue.length > LIMIT) {
- this.limitHintDom.classList.add("over");
- } else {
- this.limitHintDom.classList.remove("over");
- }
- this.limitHintDom.innerText = `${this.textValue.length}/${LIMIT}`;
- },
- get textValue() { return this._textValue; },
- set htmlValue(val) {
- this._htmlValue = val;
- this.bgDom.innerHTML = this._htmlValue;
- },
- get htmlValue() { return this._htmlValue; },
- }
-
- let deadLine = Date.now() + 7000;
- let itv = setInterval(() => {
- if (Date.now() > deadLine) {
- clearInterval(itv);
- console.log("弹幕发送增强-无法找到加载点");
- }
- let textarea = document.querySelector(".chat-input-new>.textarea-panel");
- if (textarea) {
- console.log("弹幕发送增强-加载完毕");
- clearInterval(itv);
-
- // 原版打标签
- textarea.id = "originInputArea";
-
- // SC的大按钮改小按钮
- // GM_addStyle(".icon-left-part-new .super-chat{width: 65px !important;}");
- // let scText = document.querySelector(".super-chat-text");
- // scText && (scText.innerText = "SC");
-
- // 长度提示
- GM_addStyle(".input-limit-hint{display:none}.chat-input-focus .text-limit-hint{opacity:1}.text-limit-hint{opacity:0;z-index:2;font-size:12px;line-height:19px;color:var(--Ga3);bottom:0;right:12px}.text-limit-hint.over{color:var(--brand_blue)}");
- inputArea.limitHintDom = document.createElement("span");
- inputArea.limitHintDom.className = "text-limit-hint none-select p-absolute";
- inputArea.limitHintDom.innerText = "0/" + LIMIT;
- textarea.parentNode.after(inputArea.limitHintDom);
-
- // 背景
- GM_addStyle("#liveDanmuInputBackground{opacity:0;left:0;top:50%;transform:translateY(-50%);color:transparent;overflow-y:auto;}.chat-input-focus #liveDanmuInputBackground{opacity:1}");
- inputArea.bgDom = document.createElement("div");
- inputArea.bgDom.id = "liveDanmuInputBackground";
- inputArea.bgDom.className = "textarea-panel p-absolute dp-i-block input-area";
- for (let item of Object.keys(textarea.dataset)) {
- inputArea.bgDom.dataset[item] = textarea.dataset[item];
- }
- textarea.after(inputArea.bgDom);
-
- // 输入框
- GM_addStyle("#liveDanmuInputArea{padding:5px 0;z-index:1;position:relative;background-color:transparent;resize:none;}");
- GM_addStyle(".f-word{background-color:var(--Ly4)}.fire-word{background-color:var(--Or5)}");
- GM_addStyle("#liveDanmuInputArea.default{text-indent:.5rem;width: 97%;}");
- // 用input配合一个div背景。如果用contentEditable来搞,容易搞坏光标定位和编辑栈,已放弃
- inputArea.dom = textarea.cloneNode();
- inputArea.dom.id = "liveDanmuInputArea";
- inputArea.dom.classList.add("input-area");
- inputArea.dom.classList.add("dp-block");
- inputArea.dom.classList.remove("default-height");
-
- inputArea.dom.addEventListener("scroll", (e) => {
- inputArea.bgDom.scrollTop = e.target.scrollTop;
- });
- inputArea.dom.addEventListener("focus", (e) => {
- e.target.classList.add("focus");
- e.target.classList.remove("default");
- textarea.dispatchEvent(new Event('focus'));
- // document.querySelector(".chat-input-ctnr-new").classList.add("chat-input-focus");
-
- });
- inputArea.dom.addEventListener("blur", (e) => {
- e.target.classList.add("default");
- e.target.classList.remove("focus");
- textarea.dispatchEvent(new Event('blur'));
- // document.querySelector(".chat-input-ctnr-new").classList.remove("chat-input-focus");
- });
- inputArea.dom.addEventListener("input", (e) => {
- inputArea.textValue = e.target.value;
- });
- inputArea.dom.addEventListener("keydown", (e) => {
- if (e.key == 'Enter' || e.keyCode == 13) {
- // 回车在输入框里没有任何作用,发出去会变成一个空格,且好像会造成奇怪的bug,这里直接拦截掉
- e.preventDefault();
- e.stopPropagation();
- // 回车发送
- if (e.shiftKey || e.metaKey || e.ctrlKey || e.altKey) {
- } else {
- dealDanmu(e.target);
- }
- } else if (e.key == 'Backspace' || e.keyCode == 8) {
- const startOffset = inputArea.dom.selectionStart; // 光标开始位置
- const endOffset = inputArea.dom.selectionEnd; // 光标结束位置(如果有选中的内容)
- // 在首位删除时,如果存在@标识,那就抹掉
- if (startOffset === 0 && endOffset === 0 && document.querySelector(".chat-input-new").classList.contains("at")) {
- at.user = undefined;
- }
- }
- });
- let sendBtn = document.querySelector(".chat-input-ctnr-new .right-action-btn");
- if (sendBtn) {
- sendBtn.addEventListener("click", (e) => {
- dealDanmu(inputArea.dom);
- });
- }
- textarea.after(inputArea.dom);
-
- // 适配新版小表情
- GM_addStyle(".emotion-recent-wrap{display:none !important;}"); // 最近使用 懒,一刀切了吧
- let emojiBtn = document.querySelector(".emoticons-panel");
- emojiBtn.addEventListener("click", () => {
- let deadline = Date.now() + 1000;
- (function init() {
- let emojiPanel = document.querySelector(".emoji-wrap");
- if (emojiPanel) {
- emojiPanel.onclick = e => {
- let target = e.target;
- if (e.target.tagName == "IMG") {
- target = e.target.parentNode;
- }
- inputArea.dom.value = inputArea.dom.value + target.title;
- inputArea.dom.dispatchEvent(new Event('input'));
- cleanOriginInput();
- }
- } else if (Date.now() < deadline) {
- requestIdleCallback(init, { timeout: 1000 });
- }
- })();
- });
-
- // @别人
- setTimeout(() => { // 页面刚加载完毕时,只有一个菜单,没有@按钮,所以注册一个监听器等首次打开菜单时才注册事件到@按钮上
- let observer = new MutationObserver(function (mutations) {
- let danmuMenuAt = document.querySelector(".danmaku-menu .at-this-guy");
- if (danmuMenuAt) {
- danmuMenuAt.addEventListener("click", async (e) => {
- let menu = e.target.closest(".danmaku-menu");
- let username = menu.querySelector(".username").innerText;
- at.user = username;
-
- inputArea.dom.classList.add("focus");
- inputArea.dom.classList.remove("default");
-
- cleanOriginInput(textarea);
- });
- // 首次触发后@按钮就一直在,不需要监听DOM了
- observer.disconnect();
- observer = null;
- }
- });
- observer.observe(document.querySelector(".danmaku-menu"), {
- childList: true,
- subtree: true
- });
- }, 3000);
- // @别人 用来测量字符长度的小框
- GM_addStyle("#liveDanmuAtLabel{opacity:0;z-index:-1;width:auto;left:0;top: 50%;transform: translateY(-50%);}.chat-input-ctnr-new:not(.chat-input-focus) .at #liveDanmuAtLabel{opacity:1;z-index:0;}");
- at.calDom = document.createElement("span");
- at.calDom.id = "liveDanmuAtLabel";
- at.calDom.removeAttribute("placeholder");
- at.calDom.className = "p-absolute none-select at-label";
- for (let item of Object.keys(textarea.dataset)) {
- at.calDom.dataset[item] = textarea.dataset[item];
- }
- textarea.before(at.calDom);
-
- textarea.style.display = "none";
- }
- }, 100);
-
- async function dealDanmu(textarea) {
- let msg = textarea.value;
- if (!msg) { toast("请输入内容", 1500, "info"); return; }
- if (isProcessing) { toast("弹幕正在发送中", 1500, "info"); return; }
- isProcessing = true;
- let page = 1;
- let segment = LIMIT;
- if (msg.length > segment) {
- // 自动平均每条弹幕的长度
- while (msg.length / segment % 1 < 0.7 && msg.length / segment % 1 != 0) {
- segment--;
- }
- page = Math.ceil(msg.length / segment);
- console.log(`长度:${msg.length} 间隔:${segment} 分页:${page}`);
- }
- let count = 0;
- do {
- let str = msg.substr(0, segment);
- // console.log("发送", str); await sleep(count++ ? 500 + Math.random() * 1000 >> 1 : 0);
- let result = await sendMsg(str, count++ ? 500 + Math.random() * 1000 >> 1 : 0);
- if (at.uid) { at.user = undefined; } // 首条@过后就不@了
- msg = msg.substr(segment);
- textarea.value = msg;
- textarea.dispatchEvent(new Event('input'));
- } while (msg.length > 0);
- isProcessing = false;
- };
-
- function filter(str) {
- let result = testStr(str);
- for (let word of result) {
- str = str.replaceAll(word.w, `<span class="${word.t}-word">${word.w}</span>`);
- }
- str = str.replaceAll(/\n/g, "<br>"); // 替换换行
- // str = str.replaceAll(/\s/g, " "); // 替换空格,不替换的话多空格在HTML会缩成1个
- return str;
- }
-
- // 清理原版的输入框内容 如果有内容会卡在输入状态,粉丝牌就不见了
- function cleanOriginInput(textarea) {
- if (!textarea) {
- textarea = document.querySelector("#originInputArea");
- }
- if (textarea) {
- textarea.value = '';
- textarea.dispatchEvent(new Event('input'));
- }
- }
-
- async function sendMsg(msg, timer = 500) {
- return new Promise((resolve, reject) => {
- setTimeout(() => {
- jct = document.cookie.match(/bili_jct=(\w*); /)[1];
- formData.set("csrf", jct);
- formData.set("csrf_token", jct);
- formData.set("msg", msg);
- formData.set("rnd", Math.floor(new Date() / 1000));
- if (at.uid) { // 补充@别人的参数
- formData.set("reply_mid", at.uid);
- } else {
- formData.delete("reply_mid");
- }
- fetch("//api.live.bilibili.com/msg/send", {
- credentials: 'include',
- method: 'POST',
- body: formData
- })
- .then(response => response.json())
- .then(result => {
- if (result.code != 0 || result.msg != "") {
- switch (result.msg) {
- case "f":
- result.msg = "含有敏感词";
- break;
- case "fire":
- result.msg = "弹幕含有违禁词汇";
- break;
- case "k":
- result.msg = "内容含有房间屏蔽词";
- break;
- default:
- result.msg = result.message;
- }
- if (result.code == -111) {
- jct = document.cookie.match(/bili_jct=(\w*); /)[1];
- formData.set("csrf", jct);
- formData.set("csrf_token", jct);
- }
- toast(result.msg);
- isProcessing = false;
- reject(result);
- } else {
- resolve(true);
- }
- })
- .catch(err => {
- console.log("发送弹幕出错:", err);
- toast(err.msg || err.message);
- isProcessing = false;
- reject(err);
- });
- }, timer);
- });
- }
-
- function testStr(str) {
- let result = [];
- for (let index = 0; index < str.length; index++) {
- let r = check(wordTree, str, index);
- if (r.i >= 0) {
- result.push({
- s: index,
- e: r.i,
- w: r.w,
- t: r.t
- });
- index = r.i - 1; // 减一用于抵消++
- }
- }
- return result;
-
- function check(obj, str, index, word = "") {
- let letter = str[index];
- if (str.length > index && obj[letter]) {
- word += letter;
- if (obj[letter].end) {
- return { i: index + 1, w: word, t: obj[letter].type };
- }
- return check(obj[letter], str, index + 1, word);
- } else {
- return { i: -1, w: word };
- }
- }
- }
-
- function initTree() {
- for (const item of Object.keys(riverCrabs)) {
- init(wordTree, item, 0);
- }
-
- function init(obj, str, index) {
- if (!obj[str[index]]) {
- obj[str[index]] = {};
- }
- if (str.length - 1 == index) {
- obj[str[index]].end = true;
- obj[str[index]].type = riverCrabs[str];
- } else {
- Reflect.deleteProperty(obj[str[index]], "end");
- obj[str[index]] = init(obj[str[index]], str, index + 1);
- }
- return obj;
- }
- }
-
- function toast(msg, time = 2000, type = "error") {
- let id = Math.random() * 1000 >> 1;
- let dom = document.createElement("span");
- dom.innerHTML = `<div class="link-toast ${type} link-toast-${id}" style="bottom:${105 + toastCount++ * 50}px"><span class="toast-text">${msg}</span></div>`;
- dom = dom.firstChild;
- let panel = document.querySelector("#chat-control-panel-vm");
- panel.append(dom);
- setTimeout(() => {
- toastCount--;
- dom.remove();
- }, time);
- }
-
- // 魔改fetch,拦截B站自己的弹幕发送请求
- // 2024-9-9 作废:暂时没法修改输入框的限长
- // function proxyFetch() {
- // const { fetch: originalFetch } = unsafeWindow;
- // unsafeWindow.fetch = async (...args) => {
- // let [resource, config] = args;
- // let isDanmu = resource.startsWith("//api.live.bilibili.com/msg/send");
- // if (isDanmu) {
- // // console.log("请求", resource, config);
- // // 图片表情
- // let emotion = config && config.data && config.data.emoticonOptions;
- // if (!emotion) {
- // // 拦截弹幕请求,直接返回一个假的
- // return new Response("{\"code\":1}", { status: 200 });
- // }
- // }
- // const response = await originalFetch(resource, config);
- // if (isDanmu) {
- // // console.log("返回", response);
- // }
- // return response;
- // };
- // }
-
- })();
-
- // 大表情
- // bubble: 0
- // msg: upower_[Minicatty_吃瓜]
- // color: 16777215
- // mode: 1
- // dm_type: 1
- // emoticonOptions: [object Object]
- // fontsize: 25
- // rnd: 1725808681
- // roomid: 2323777
- // csrf: 1
- // csrf_token: 1
-
- // @人
- // bubble: 0
- // msg: [狗叫]123
- // color: 16777215
- // mode: 1
- // room_type: 0
- // jumpfrom: 0
- // reply_mid: 23237777
- // reply_attr: 0
- // replay_dmid: 42945fa0764e7be6388b4dfd3666ddb356
- // statistics: {"appId":100,"platform":5}
- // fontsize: 25
- // rnd: 1725808681
- // roomid: 2323777
- // csrf: 1
- // csrf_token: 1
-
- // 普通弹幕
- // bubble: 0
- // msg: [狗叫]123
- // color: 16777215
- // mode: 1
- // room_type: 0
- // jumpfrom: 0
- // reply_mid: 0
- // reply_attr: 0
- // replay_dmid:
- // statistics: {"appId":100,"platform":5}
- // fontsize: 25
- // rnd: 1725808681
- // roomid: 2323777
- // csrf: 1
- // csrf_token: 1