Bili Keyword Replacer

Replaces chosen substrings in outgoing Bilibili live-chat messages before they are sent

// ==UserScript==
// @name         Bili Keyword Replacer
// @namespace    https://github.com/TZFC/Danmaku-replace
// @version      4.0
// @description  Replaces chosen substrings in outgoing Bilibili live-chat messages before they are sent
// @author       TZFC
// @match        https://live.bilibili.com/*
// @run-at       document-start
// @grant        none
// @license      MIT
// ==/UserScript==

(() => {
  /* ────────────────────────────── SETTINGS ────────────────────────────── */
  /* Write the same number of items in both arrays, 1-to-1 correspondence. */
  const to_be_replace_list = ['包子', '男娘','蓝凉','之交','抖音','y','m','p','r','e','o','x','k','i', 's', 't', 'u', 'r', '川普', '64','89','扣扣','胖次','我的名字','比基尼','丑','喷水','出','扶她','大大','插一下','念经似','舔','榜','看我','动态'];  // ← originals
  const target_list        = ['包了', '侽娘','侽娘','Z交' ,'某音','у','м','р','г','е','о','х','κ','і', 'ṡ', 'ṭ', 'ụ', 'ṟ', '川晋', 'б4','ȣ9','扣.扣','胖㳄','我の名字','此基尼','吜','喷氺','岀','扶他','大太','插1下','念泾似','㖭','搒','着我','动.态']; // ← replacements
  /* ────────────────────────── CHECK ────────────────────────── */
  if (to_be_replace_list.length !== target_list.length) {
    console.error('[Keyword Replacer] Array length mismatch!');
    return;
  }

  const SEND_PATH = '/msg/send';
  let DEFAULT_DECO_LEFT = localStorage.getItem('deco_left') || '';
  let DEFAULT_DECO_RIGHT = localStorage.getItem('deco_right') || '';

  const esc = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  const pattern = new RegExp(
    '(' + to_be_replace_list.map(esc).join('|') + ')',
    'g'
  );

  function transformMsg(str) {
    var returned_content = str;
    if (str.startsWith('#s ')) {
      const content = str.slice(3);
      returned_content = content
        .replace(/\s+/g, '♫')
        .replace(/([^a-zA-Z0-9])(?=[^a-zA-Z0-9])/g, '$1♪'); 
    }
  
    if (str.startsWith('#c ')) {
      const content = str.slice(3);
      returned_content = '⚞'+content+'⚟'; 
    }
  
    if (str.startsWith('#f ')) {
      const content = str.slice(3);
      returned_content = '꧁'+content+'꧂'; 
    }

    if (str.startsWith('!d ')) {
      const content = str.slice(3);
      const command = content.trim();
      if (command==='c') {
        DEFAULT_DECO_LEFT = '⚞';
        DEFAULT_DECO_RIGHT = '⚟';
      }
      if (command==='f') {
        DEFAULT_DECO_LEFT = '꧁';
        DEFAULT_DECO_RIGHT = '꧂';
      }
      if (command==='x') {
        DEFAULT_DECO_LEFT = '';
        DEFAULT_DECO_RIGHT = '';
      }
      localStorage.setItem('deco_left', DEFAULT_DECO_LEFT);
      localStorage.setItem('deco_right', DEFAULT_DECO_RIGHT);
      return '';
    }
  
    returned_content = returned_content.replace(pattern, m => {
      const idx = to_be_replace_list.findIndex(src => src === m);
      return target_list[idx];
    });

    return DEFAULT_DECO_LEFT + returned_content + DEFAULT_DECO_RIGHT;
}


  function sameEndpoint(url) {
    try { return new URL(url, location.origin).pathname.endsWith(SEND_PATH); }
    catch { return false; }
  }

  function patchedBody(body) {
    if (!body) return body;

    if (typeof body.has === 'function' && body.has('emoticonOptions')) {
    return body;
    }
    
    const m = body.get('msg');
    if (m) body.set('msg', transformMsg(m));
    return body;
  }

  /* ─────────────────────── fetch() HOOK ─────────────────────── */
  const nativeFetch = window.fetch;
  window.fetch = function (input, init = {}) {
    const url = typeof input === 'string' ? input
              : input instanceof Request   ? input.url
              : '';
    if (sameEndpoint(url)) {
      if (init.body) {
        init.body = patchedBody(init.body);
      } else if (input instanceof Request) {
        init = { ...input, body: patchedBody(input.body) };
        input = url;
      }
    }
    return nativeFetch.call(this, input, init);
  };

  /* ───────────── XMLHttpRequest HOOK (fallback path) ────────── */
  const XHROpen = XMLHttpRequest.prototype.open;
  const XHRSend = XMLHttpRequest.prototype.send;

  XMLHttpRequest.prototype.open = function (method, url, ...rest) {
    this.__patchMe__ = sameEndpoint(url);
    return XHROpen.call(this, method, url, ...rest);
  };

  XMLHttpRequest.prototype.send = function (body) {
    if (this.__patchMe__) body = patchedBody(body);
    return XHRSend.call(this, body);
  };
})();