您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
在发送前替换哔哩哔哩直播弹幕中的指定字符串。
// ==UserScript== // @name Danmaku replace // @name:zh-CN 弹幕替换器 // @namespace https://github.com/TZFC/Danmaku-replace // @version 4.7 // @description Replace chosen substrings in outgoing Bilibili live-chat messages before they are sent. // @description:zh-CN 在发送前替换哔哩哔哩直播弹幕中的指定字符串。 // @author TZFC // @match https://live.bilibili.com/* // @icon https://www.bilibili.com/favicon.ico // @run-at document-start // @grant none // @license GPL-3.0 // ==/UserScript== (() => { /* ────────────────────────────── SETTINGS ────────────────────────────── */ const originals = ['包子', '男娘','蓝凉','之交','抖音','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z', '川普', '扣扣','胖次','我的名字','比基尼','丑','喷水','出','扶她','大大','插一下','念经','舔','榜','看我','动态','1','2','3','4','5','6','7','8','9','0','面基','榜一']; const targets = ['包了', '侽娘','侽娘','Z交' ,'枓音','𝖺','𝖻','𝖼','𝖽','𝖾','𝖿','𝗀','𝗁','𝗂','𝗃','𝗄','𝗅','𝗆','𝗇','𝗈','𝗉','𝗊','𝗋','𝗌','𝗍','𝗎','𝗏','𝗐','𝗑','𝗒','𝗓','𝖠','𝖡','𝖢','𝖣','𝖤','𝖥','𝖦','𝖧','𝖨','𝖩','𝖪','𝖫','𝖬','𝖭','𝖮','𝖯','𝖰','𝖱','𝖲','𝖳','𝖴','𝖵','𝖶','𝖷','𝖸','𝖹', '川晋', '扣.扣','胖㳄','我の名字','此基尼','吜','喷氺','岀','扶他','大太','插1下','念泾','㖭','搒','着我','动.态', '𝟷','𝟸','𝟹','𝟺','𝟻','𝟼','𝟽','𝟾','𝟿','𝟶','面箕','榜𝟷']; if (originals.length !== targets.length) { console.error('[Keyword Replacer] Array length mismatch!'); return; } /* ───────────────────────── PRECOMPUTE LOOKUPS ───────────────────────── */ const send_path = '/msg/send'; let default_deco_left = localStorage.getItem('deco_left') || ''; let default_deco_right = localStorage.getItem('deco_right') || ''; function escape_regex(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } // Single compiled alternation regex const replace_regex = new RegExp(`(${originals.map(escape_regex).join('|')})`, 'g'); // O(1) mapping for replacements const replace_map = new Map(); for (let i = 0; i < originals.length; i++) { replace_map.set(originals[i], targets[i]); } /* ───────────────────────────── TRANSFORM ────────────────────────────── */ function transform_msg(input) { let content = input; // fast prefix routing by first two characters const p0 = input.charCodeAt(0); const p1 = input.charCodeAt(1); // "#s " if (p0 === 0x23 && p1 === 0x73 && input.charCodeAt(2) === 0x20) { const body = input.slice(3); // collapse spaces to ♫, then insert ♪ between consecutive non-alnum runs content = body.replace(/\s+/g, '♫').replace(/([^a-zA-Z0-9])(?=[^a-zA-Z0-9])/g, '$1♪'); } // "#c " else if (p0 === 0x23 && p1 === 0x63 && input.charCodeAt(2) === 0x20) { content = '⚞' + input.slice(3) + '⚟'; } // "#f " else if (p0 === 0x23 && p1 === 0x66 && input.charCodeAt(2) === 0x20) { content = '꧁' + input.slice(3) + '꧂'; } // "!d " commands else if (p0 === 0x21 && p1 === 0x64 && input.charCodeAt(2) === 0x20) { const cmd = input.slice(3).trim(); if (cmd === 'c') { default_deco_left = '⚞'; default_deco_right = '⚟'; } else if (cmd === 'f') { default_deco_left = '꧁'; default_deco_right = '꧂'; } else if (cmd === 'x') { default_deco_left = ''; default_deco_right = ''; } localStorage.setItem('deco_left', default_deco_left); localStorage.setItem('deco_right', default_deco_right); return ''; // command messages are not sent } // quick rejection: if no possible match substring exists, skip replace call if (replace_regex.test(content)) { // reset lastIndex because .test with /g/ advances it replace_regex.lastIndex = 0; content = content.replace(replace_regex, (m) => replace_map.get(m)); } return default_deco_left + content + default_deco_right; } /* ──────────────────────────── HELPERS ──────────────────────────── */ function same_endpoint(url_like) { const url = typeof url_like === 'string' ? url_like : ''; try { return new URL(url, location.origin).pathname.endsWith(send_path); } catch { return false; } } function patched_body(body) { // only handle FormData; do not attempt to support other types // this follows "do not handle malformed nor null input" if (!(body instanceof FormData)) return body; if (typeof body.has === 'function' && body.has('emoticonOptions')) return body; const msg = body.get('msg'); if (msg) body.set('msg', transform_msg(msg)); return body; } /* ───────────────────────────── fetch HOOK ───────────────────────────── */ const native_fetch = window.fetch; window.fetch = function patched_fetch(input, init) { // Normalize to url string const url = typeof input === 'string' ? input : (input && input.url) || ''; if (!same_endpoint(url)) { return native_fetch.call(this, input, init); } // If init with FormData body if (init && init.body) { const new_body = patched_body(init.body); if (new_body !== init.body) { init = { ...init, body: new_body }; } return native_fetch.call(this, input, init); } // If Request instance with body if (input instanceof Request) { const req = input; // Create a derived Request with patched body (POST chat send path) const derived = new Request(req, { body: patched_body(req.body) }); return native_fetch.call(this, derived); } return native_fetch.call(this, input, init); }; /* ─────────────────────── XMLHttpRequest HOOK ─────────────────────── */ const XHR_OPEN = XMLHttpRequest.prototype.open; const XHR_SEND = XMLHttpRequest.prototype.send; const FLAG_PATCH = Symbol('patch_me'); XMLHttpRequest.prototype.open = function open(method, url, ...rest) { this[FLAG_PATCH] = same_endpoint(url); return XHR_OPEN.call(this, method, url, ...rest); }; XMLHttpRequest.prototype.send = function send(body) { if (this[FLAG_PATCH]) { body = patched_body(body); } return XHR_SEND.call(this, body); }; })();