您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
通过UID、关键词或正则表达式过滤哔站直播间的SC
当前为
// ==UserScript== // @name Bilibili直播SC过滤 // @namespace https://github.com/journey-ad // @version 0.3 // @description 通过UID、关键词或正则表达式过滤哔站直播间的SC // @author journey-ad // @icon https://www.google.com/s2/favicons?domain=bilibili.com // @include /https?:\/\/live\.bilibili\.com\/(blanc\/)?\d+\??.*/ // @require https://cdn.jsdelivr.net/npm/vue@2 // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/ajaxhook.min.js // @require https://gf.qytechs.cn/scripts/417560-bliveproxy/code/bliveproxy.js?version=984333 // @grant none // @run-at document-end // ==/UserScript== (function () { 'use strict'; const __SCRIPT_VERSION = '0.3'; let store = null, blockUser = [], // 屏蔽用户列表 uidList = [], // 屏蔽用户UID列表 blockContent = [], // 屏蔽内容列表 pattern = null // 最终生成的正则表达式 function initApp() { store = new MyStorage('__SC_BLOCK_DATA') // 初始化存储池 blockUser = store.get('blockUser', blockUser) blockContent = store.get('blockContent', blockContent) updateBlockList() // 更新屏蔽列表 // hook初始化接口,过滤sc数据 ah.proxy({ onRequest: (config, handler) => { // Ajax-hook库的bug // 什么都不做也要加上onRequest方法,不然会丢掉header导致csrf校验失败 handler.next(config); }, onResponse: (response, handler) => { if (response.config.url.includes('/xlive/web-room/v1/index/getInfoByRoom')) { // console.log('======HOOK=======', response) const _resp = JSON.parse(response.response) // 过滤初始化数据的sc if (_resp?.data?.super_chat_info?.message_list) { _resp.data.super_chat_info.message_list = scFilter(_resp.data.super_chat_info.message_list) } response.response = JSON.stringify(_resp) } handler.next(response); } }) if (window?.__SSR_INITIAL_STATE__?.baseInfoRoom?.super_chat_info?.message_list) { window.__SSR_INITIAL_STATE__.baseInfoRoom.super_chat_info.message_list = scFilter(window.__SSR_INITIAL_STATE__.baseInfoRoom.super_chat_info.message_list) } if (window?.__NEPTUNE_IS_MY_WAIFU__?.roomInfoRes?.data?.super_chat_info?.message_list) { window.__NEPTUNE_IS_MY_WAIFU__.roomInfoRes.data.super_chat_info.message_list = scFilter(window.__NEPTUNE_IS_MY_WAIFU__.roomInfoRes.data.super_chat_info.message_list) } function scFilter(list) { return list.filter(item => { return !check({ type: 'SC', uid: item.uid, name: item.user_info.uname, msg: item.message }) }) } // 通过sc右上角菜单屏蔽 $(document).on('click', '#pay-note-panel-vm .card-list .card-item-box', function (e) { // 等一会详情dom加载,应该有更好的方法 setTimeout(() => { $('#pay-note-panel-vm .detail-info .card-detail').on('click', '.more', function (e) { // 已经添加过屏蔽按钮 if ($('#pay-note-panel-vm .card-detail .danmaku-menu .add-blocklist').length > 0) return const cardEl = $(this).closest('.card-detail')[0] const cardVM = cardEl.__vue__ const scData = cardVM?.currentCardData || null // 从挂载的vue实例拿到sc数据 if (!scData) return const { uid, userInfo, message } = scData const { uname } = userInfo const roomid = window.BilibiliLive.ROOMID // 从全局变量拿到直播间号 const menuEl = $(cardEl).find('.danmaku-menu') const menuItem = $(`<div class="none-select"><a class="clickable bili-link pointer add-blocklist"><span>添加UID到黑名单</span></a></div>`) menuItem.find('.add-blocklist').data('scData', { uid, uname, message, roomid }) // 插入菜单项 menuEl.append(menuItem) $('#pay-note-panel-vm .card-detail .danmaku-menu').one('click', '.add-blocklist', function (e) { cardVM.showInfo = false const scData = $(this).data('scData') // 添加屏蔽 addBlock('user', scData) // 隐藏这个uid所有sc hideSC(uid) }) }) }, 200); }) } function initSettingPanel() { const cssText = `.sc-block-setting { display: none; position: absolute; right: 3%; bottom: 32px; width: 94%; background: #fff; border-radius: 6px; padding: 6px 6px; box-sizing: border-box; color: #444; font-size: 12px; border: 1px solid #e7e7e7; box-shadow: 0px 1px 10px #e9e9e9; z-index: 100; } .sc-block-setting * { box-sizing: border-box; } .sc-block-setting ::-webkit-scrollbar { width: 4px !important; height: 4px !important; } .sc-block-setting ::-webkit-scrollbar-button { width: 0; height: 0; } .sc-block-setting ::-webkit-scrollbar-thumb { background: #e1e1e1 !important; border-radius: 4px; } .sc-block-setting fieldset { margin: 0; padding: 0 4px; border: 1px solid #efefef; } .sc-block-setting fieldset legend { padding: 0 4px; margin-bottom: 2px; } .sc-block-setting input[type="text"] { display: block; appearance: none; width: 100%; height: 22px; line-height: 22px; padding: 0 6px; border: 1px solid #999; border-radius: 2px; outline: 0; } .sc-block-setting input[type="text"]::-webkit-input-placeholder { color: #ccc; } .sc-block-setting input[type="text"]:focus { border-color: #4caf50; } .sc-block-setting button { display: flex; justify-content: center; align-items: center; appearance: none; margin-left: 5px; height: 22px; padding: 0 7px; font-size: 12px; color: #137cbd; background: #fff; border: 1px solid #23ade5; border-radius: 3px; line-height: 1; user-select: none; cursor: pointer; transition: all 0.12s ease-in-out; } .sc-block-setting button:hover { color: #fff; background: #23ade5; } .sc-block-setting .header-bar { display: flex; justify-content: space-between; align-items: center; font-size: 14px; padding: 0 0 6px; border-bottom: 1px solid #efefef; } .sc-block-setting .header-bar h4 { font-size: 14px; margin: 0; } .sc-block-setting .header-bar .btn { cursor: pointer; user-select: none; font-size: 0; margin-right: 2px; } .sc-block-setting .header-bar .btn svg { fill: #444; } .sc-block-setting .header-bar .setting-btn { margin-left: auto; margin-right: 8px; } .sc-block-setting .setting-content { display: flex; flex-wrap: wrap; justify-content: flex-start; overflow: hidden; } .sc-block-setting .setting-content .setting-item, .sc-block-setting .setting-content .func-item { float: left; display: flex; align-items: center; height: 24px; margin-top: 4px; } .sc-block-setting .setting-content .setting-item { margin: 0 5px; } .sc-block-setting .setting-content .setting-item input[type="checkbox"] { cursor: pointer; } .sc-block-setting .setting-content .setting-item label { margin-left: 2px; cursor: pointer; user-select: none; } .sc-block-setting .section { margin-top: 10px; } .sc-block-setting .section .empty { display: flex; justify-content: center; align-items: center; height: 40px; color: #5e5e5e; } .sc-block-setting .keyword-wrap { display: flex; justify-content: space-between; align-items: center; } .sc-block-setting .keyword-wrap .add-keyword, .sc-block-setting .keyword-wrap .add-uid { flex: none; } .sc-block-setting .block-list { min-height: 50px; max-height: 122px; margin-top: 8px; overflow-y: auto; } .sc-block-setting .block-list.list-content .block-item { display: flex; justify-content: space-between; align-items: center; } .sc-block-setting .block-list.list-content .block-item.is-regex { color: #ff9800; background: #fff9c5; } .sc-block-setting .block-list.list-content .block-item.is-regex::after { content: "[正则]"; position: absolute; top: 50%; right: 28px; transform: translateY(-50%); color: #ccc; } .sc-block-setting .block-list.list-content .block-item.is-regex span { width: 190px; } .sc-block-setting .block-list.list-content .block-item button { margin-right: 6px; } .sc-block-setting .block-list.list-user .block-item button { position: absolute; right: 6px; top: 12px; } .sc-block-setting .block-list .block-item { position: relative; padding: 2px 6px; padding-right: 0; } .sc-block-setting .block-list .block-item:nth-of-type(odd) { background-color: #f9f9f9; border-top: 1px solid #FAFAFA; } .sc-block-setting .block-list .block-item:hover button { opacity: 1; } .sc-block-setting .block-list .block-item a { color: #23ade5; cursor: pointer; } .sc-block-setting .block-list .block-item span { width: 210px; margin-right: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .sc-block-setting .block-list .block-item button { flex: none; width: 16px; height: 16px; border-radius: 50%; font-size: 10px; margin-left: 4px; opacity: 0; transition: 0.2s opacity ease-in-out; } .sc-block-setting .block-list .block-item .user-info, .sc-block-setting .block-list .block-item .meta-info { line-height: 16px; } .sc-block-setting .block-list .block-item .meta-info { font-size: 11px; color: #9f9f9f; } .sc-block-setting .block-list .block-item .meta-info a { color: #9f9f9f; } .sc-block-setting .block-list .block-item .message { height: 24px; line-height: 24px; width: 235px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }`; const template = ` <div id="sc-block-setting-vm" class="sc-block-setting"> <div class="header-bar"> <h4>SC屏蔽助手 v${__SCRIPT_VERSION}</h4> <div class="btn setting-btn" @click="toggleSetingPanel"><svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="15" height="15"><path d="M633.6 938.667c-8.533 0-17.067-6.4-21.333-14.934-14.934-44.8-55.467-72.533-102.4-72.533s-87.467 29.867-102.4 72.533c-4.267 10.667-14.934 17.067-25.6 14.934-74.667-21.334-140.8-61.867-194.134-117.334-8.533-8.533-8.533-21.333 0-27.733 17.067-19.2 25.6-42.667 25.6-68.267 0-57.6-46.933-104.533-106.666-104.533H96c-10.667 2.133-21.333-6.4-23.467-17.067C66.133 573.867 64 544 64 514.133c0-44.8 6.4-89.6 21.333-132.266 2.134-8.534 10.667-14.934 21.334-14.934 59.733 0 108.8-46.933 108.8-104.533 0-17.067-4.267-32-10.667-46.933-6.4-10.667-4.267-21.334 2.133-27.734 53.334-49.066 117.334-83.2 185.6-102.4 10.667-2.133 19.2 2.134 25.6 10.667 19.2 36.267 55.467 57.6 96 57.6s76.8-21.333 96-57.6c4.267-8.533 14.934-12.8 25.6-10.667 68.267 19.2 132.267 53.334 185.6 102.4 6.4 6.4 8.534 17.067 4.267 25.6-6.4 14.934-10.667 29.867-10.667 46.934 0 57.6 46.934 104.533 106.667 104.533 8.533 0 19.2 6.4 21.333 14.933C955.733 422.4 964.267 467.2 964.267 512c0 29.867-2.134 59.733-8.534 89.6-2.133 10.667-12.8 19.2-23.466 17.067H921.6c-59.733 0-106.667 46.933-106.667 104.533 0 25.6 8.534 49.067 25.6 68.267 6.4 8.533 6.4 21.333 0 27.733C780.8 874.667 714.667 915.2 640 938.667h-6.4zM512 808.533c57.6 0 108.8 32 134.4 83.2 53.333-19.2 102.4-49.066 145.067-87.466C776.533 780.8 768 753.067 768 725.333c0-78.933 64-145.066 145.067-147.2 4.266-21.333 4.266-42.666 4.266-64 0-36.266-4.266-72.533-14.933-106.666-74.667-6.4-134.4-70.4-134.4-147.2 0-17.067 2.133-34.134 8.533-49.067-40.533-34.133-89.6-61.867-140.8-78.933C608 172.8 563.2 198.4 512 198.4s-96-25.6-123.733-66.133c-51.2 17.066-100.267 42.666-140.8 78.933 6.4 14.933 8.533 32 8.533 49.067 0 76.8-59.733 140.8-134.4 147.2-8.533 34.133-14.933 70.4-14.933 106.666 0 21.334 2.133 42.667 4.266 64C192 578.133 256 644.267 256 723.2c0 27.733-8.533 55.467-23.467 78.933 40.534 38.4 91.734 68.267 145.067 87.467 25.6-49.067 76.8-81.067 134.4-81.067z" fill="#444"/><path d="M512 682.667c-93.867 0-170.667-76.8-170.667-170.667S418.133 341.333 512 341.333 682.667 418.133 682.667 512 605.867 682.667 512 682.667zM512 384c-70.4 0-128 57.6-128 128s57.6 128 128 128 128-57.6 128-128-57.6-128-128-128z"/></svg></div> <div class="btn close-btn" @click="closeSetting"><svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="15" height="15"><path d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9c-4.4 5.2-.7 13.1 6.1 13.1h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"/></svg></div> </div> <div class="setting-content" v-if="showSetting"> <div class="setting-item"> <input type="checkbox" v-model="setting.showRef" id="sc-block-show-ref" /> <label for="sc-block-show-ref">显示来源</label> </div> <div class="setting-item"> <input type="checkbox" v-model="setting.danmaku" id="sc-block-danmaku" /> <label for="sc-block-danmaku">过滤弹幕</label> </div> <div class="setting-item"> <input type="checkbox" v-model="setting.syncSite" id="sc-block-sync-site" /> <label for="sc-block-sync-site">同时添加到网站</label> </div> <div class="func-item"> <button class="btn clear-btn" @click="reset">清空记录</button> <button class="btn sync-btn" @click="handleSyncSiteShield">从网站同步</button> </div> </div> <fieldset class="section"> <legend>关键词屏蔽</legend> <div class="keyword-wrap"> <input type="text" class="sc-block-keyword" v-model.trim="keyword" placeholder="输入要屏蔽的关键词 支持正则" tabindex="1"> <button class="add-keyword" @click="handleAdd('content')" tabindex="2">添加</button> </div> <div class="block-list list-content" v-if="blockContent.length"> <div class="block-item" :class="{ 'is-regex': isVaildRegex(item) }" v-for="(item, index) in blockContent"> <span>{{ item }}</span> <button @click="handleRemove('content', index)">X</button> </div> </div> <div class="empty" v-else>暂无内容</div> </fieldset> <fieldset class="section"> <legend>UID屏蔽</legend> <div class="keyword-wrap"> <input type="text" class="sc-block-uid" v-model.trim="uid" placeholder="输入要屏蔽的UID" tabindex="3"> <button class="add-uid" @click="handleAdd('uid')" tabindex="4">添加</button> </div> <div class="block-list list-user" v-if="blockUser.length"> <div class="block-item" v-for="(item, index) in blockUser"> <button @click="handleRemove('user', index)">X</button> <div class="user-info"> <span class="name"><a :href="'https://space.bilibili.com/'+item.uid" target="_blank">{{ item.uname }}</a></span> <span class="uid">UID: {{ item.uid }}</span> </div> <div class="meta-info"> <span class="room">直播间 <a :href="'https://live.bilibili.com/'+item.roomid" target="_blank">{{ item.roomid }}</a></span> <span class="date">{{ item.ts | dateFmt('yyyy-MM-dd hh:mm:ss') }}</span> </div> <div class="message" :title="item.ref" v-if="setting.showRef">{{ item.ref }}</div> </div> </div> <div class="empty" v-else>暂无内容</div> </fieldset> </div>`; const appConf = { data: { uid: '', // 屏蔽UID keyword: '', // 屏蔽关键词 blockUser, // 屏蔽用户列表 blockContent, // 屏蔽内容列表 roomid: window.BilibiliLive.ROOMID, // 直播间号 token: '', // CSRF Token showSetting: false, // 是否显示扩展设置 // 设置项 setting: { showRef: false, // 显示屏蔽来源 danmaku: true, // 同时过滤弹幕 syncSite: false, // 同时添加到网站 }, }, watch: { setting: { handler() { this.saveSetting(); }, deep: true, } }, created() { this.setting = store.get('setting', this.setting); this.handleBroadcast(); const token = document.cookie.match(/bili_jct=([0-9a-fA-F]{32})/); if (token) { this.token = token[1] } else { return this.toast('找不到令牌', 'error'); } }, methods: { closeSetting() { $('#sc-block-setting-vm').fadeOut(200); this.saveSetting(); }, toggleSetingPanel() { this.showSetting = !this.showSetting; }, saveSetting() { store.set('setting', this.setting); }, handleAdd(type) { if (type === 'content') { if (this.keyword === '') { return; } if (this.setting.syncSite) { this.addSiteShield('content', this.keyword); } addBlock('content', this.keyword); this.keyword = ''; } else if (type === 'uid') { if (this.uid === '') { return; } if (this.setting.syncSite) { this.addSiteShield('uid', this.uid); } this.fetchUserInfo(this.uid) .then(({ name }) => { addBlock('user', { uid: this.uid, uname: name, roomid: this.roomid, message: '[通过UID手动屏蔽]' }) this.uid = ''; }) .catch(err => { this.toast(err.message, 'error'); }) } }, handleRemove(type, index) { if (type === 'content') { removeBlock('content', index); } else if (type === 'user') { removeBlock('user', index); } }, // 同步屏蔽列表 handleSyncSiteShield() { this.fetchSiteShield(this.roomid) .then(({ users, keywords }) => { const newBlockUser = users.map(user => { return { uid: user.uid, uname: user.uname, roomid: this.roomid, message: '[同步自网站屏蔽列表]' } }) addBlock('user', newBlockUser); addBlock('content', keywords); this.toast('同步完成', 'info'); }) .catch(err => { this.toast(err.message, 'error'); }) }, // 重置屏蔽列表 reset() { blockUser = []; blockContent = []; this.blockUser = blockUser; this.blockContent = blockContent; updateBlockList(); this.toast('记录已清空', 'info'); }, // 获取用户信息 fetchUserInfo(uid) { return new Promise((resolve, reject) => { fetch('https://api.bilibili.com/x/space/acc/info?mid=' + uid, { method: "GET", mode: "cors", credentials: "include" }) .then(res => res.json()) .then(resp => { // console.log(resp) if (resp.code === 0) { const { mid, name } = resp.data; resolve({ mid, name }) } else { reject(new Error(resp.message)) } }) .catch(err => { reject(err) }) }) }, // 获取网站屏蔽列表 fetchSiteShield(roomid) { return new Promise((resolve, reject) => { fetch('https://api.live.bilibili.com/xlive/web-room/v1/index/getInfoByUser?room_id=' + roomid, { method: "GET", mode: "cors", credentials: "include" }) .then(res => res.json()) .then(resp => { if (resp.code === 0) { const { shield_user_list: users, keyword_list: keywords } = resp.data.shield_info; resolve({ users, keywords }) } else { reject(new Error(resp.message)) } }) .catch(err => { reject(err) }) }) }, // 添加至网站屏蔽 addSiteShield(type = 'uid', data) { let api = '', body = '' if (type === 'uid') { api = 'https://api.live.bilibili.com/liveact/shield_user'; body = new URLSearchParams({ roomid: this.roomid, uid: data, type: 1, csrf_token: this.token, csrf: this.token, visit_id: '' }) } else if (type === 'content') { api = 'https://api.live.bilibili.com/xlive/web-ucenter/v1/banned/AddShieldKeyword'; const formData = new FormData(); formData.append('keyword', data); formData.append('csrf', this.token); formData.append('csrf_token', this.token); body = formData; } fetch(api, { body, method: "POST", mode: "cors", credentials: "include" }) .then(res => res.json()) .then(resp => { console.log(resp) }) .catch(err => { console.error(err) }) }, // 处理广播过滤 handleBroadcast() { // 弹幕 bliveproxy.addCommandHandler('DANMU_MSG', command => { // 设置里不处理弹幕 if (!this.setting.danmaku) return; let info = command.info const msg = info[1] const [uid, name] = info[2] if (check({ type: '弹幕', uid, name, msg })) { command.cmd = "NULL" } }) // SC bliveproxy.addCommandHandler('SUPER_CHAT_MESSAGE', command => { const { roomid, data } = command const { uid, message: msg, user_info } = data const name = user_info.uname if (check({ type: 'SC', uid, name, msg })) { command.cmd = "NULL" } }) }, ...Utils }, filters: { dateFmt(ts, format) { const dateData = new Date(ts * 1000); const date = { "M+": dateData.getMonth() + 1, "d+": dateData.getDate(), "h+": dateData.getHours(), "m+": dateData.getMinutes(), "s+": dateData.getSeconds(), "q+": Math.floor((dateData.getMonth() + 3) / 3), "S+": dateData.getMilliseconds() }; if (/(y+)/i.test(format)) { format = format.replace(RegExp.$1, String(dateData.getFullYear()).substr(4 - RegExp.$1.length)); } for (let k in date) { if (new RegExp('(' + k + ')').test(format)) { format = format.replace(RegExp.$1, RegExp.$1.length == 1 ? date[k] : ("00" + date[k]).substr(String(date[k]).length)); } } return format; } } } Utils.addStyle(cssText); const $settingPanel = new Vue(appConf) const $btn = $('<span class="btn-sc-block live-skin-main-text">SC屏蔽助手</span>') $btn.css({ fontSize: '12px', margin: '0 5px', cursor: 'pointer', lineHeight: '24px', userSelect: 'none' }) $btn.on('click', function () { $('#sc-block-setting-vm').toggle(200) }) new MutationObserver((mutations, observer) => { if (Utils.get('.icon-right-part')) { observer.disconnect(); $('#control-panel-ctnr-box .icon-right-part').prepend($btn) $('#control-panel-ctnr-box .control-panel-icon-row').append(template) $settingPanel.$mount('#sc-block-setting-vm') } }) .observe(Utils.get('#control-panel-ctnr-box') || document.body, { childList: true, subtree: true }); } // 检查是否在屏蔽名单内 function check({ type = '弹幕', uid, name, msg }) { const content = `[${type}]${name}: ${msg}` // 检查uid if (uidList.includes(uid)) { console.warn('UID blocked', uid, content) return true } // 检查名字和内容 if (pattern) { const match = content.match(pattern) if (match) { console.warn('Content blocked', uid, content, `======> ${match[0]}`) return true } else { return false } } } // 隐藏指定uid发送的sc function hideSC(uids) { if (Utils.typeOf(uids) !== 'array') uids = [uids] $('.card-list .card-item-box').each((_, item) => { const uid = item.__vue__.itemData.uid if (uids.includes(uid)) { item.__vue__.itemData.show = false } }) // 关闭已打开的sc详情 // 详情窗口打开时会监听window对象的click事件,并调用closeMask方法来关闭详情窗口 // 这里直接给body派发一个click事件触发它 $('body').trigger('click') } // 添加屏蔽 function addBlock(type, data) { if (!Array.isArray(data)) { data = [data] } data.forEach(item => { const ts = Date.now() / 1000 | 0 switch (type) { case 'user': const { uid, uname, message, roomid } = item if (!uidList.includes(parseInt(uid))) { blockUser.unshift({ uid: parseInt(uid), // uid uname, // 名字 roomid, // 直播间号 ts, // 时间戳 ref: message // sc内容 }) } else { if (data.length === 1) { Utils.toast('已在屏蔽名单中', 'warn') } } break; case 'content': if (!blockContent.includes(item)) { blockContent.unshift(item) } else { if (data.length === 1) { Utils.toast('已在屏蔽名单中', 'warn') } } break; default: break; } }) if (data.length === 1) Utils.toast('添加屏蔽成功', 'success'); updateBlockList() } // 移除屏蔽项 function removeBlock(type, index) { switch (type) { case 'user': blockUser.splice(index, 1) break; case 'content': blockContent.splice(index, 1) pattern = Utils.generatePattern(blockContent) break; default: break; } Utils.toast('移除屏蔽成功', 'info', 1000); updateBlockList() } // 更新屏蔽列表 function updateBlockList() { // 屏蔽用户uid列表 uidList = blockUser.map(item => parseInt(item.uid)) // 生成过滤正则 pattern = Utils.generatePattern(blockContent) store.set('blockUser', blockUser) store.set('blockContent', blockContent) } // ====================== 工具 ====================== // 存储池管理 class MyStorage { constructor(key) { if (!key) throw new Error('cache key is required') this.key = key this.data = {} try { const _cached = JSON.parse(localStorage.getItem(this.key)) || {} this.data = _cached || {} } catch (e) { this.data = {} } } get(key, defaultValue) { return this.data[key] || defaultValue } set(key, data) { this.data[key] = data this.save() } remove(key) { delete this.data[key] this.save() } clear() { this.data = {} this.save() } has(key) { return this.data[key] !== undefined } save() { localStorage.setItem(this.key, JSON.stringify(this.data)) } } // 封装一些工具 const Utils = { create(nodeType, config, appendTo) { const element = document.createElement(nodeType); config && this.set(element, config); if (appendTo) appendTo.appendChild(element); return element; }, set(element, config, appendTo) { if (config) { for (const [key, value] of Object.entries(config)) { element[key] = value; } } if (appendTo) appendTo.appendChild(element); return element; }, get(selector) { if (selector instanceof Array) { return selector.map(item => this.get(item)); } return document.body.querySelector(selector); }, toast(msg, type = 'success', duration = 3000) { const classMap = { success: 'success', warn: 'caution', error: 'error', info: 'info' } let toast = this.create('div', { innerHTML: `<div class="link-toast absolute ${classMap[type]}" style="left: 50%;top: 60%;"><span class="toast-text">${msg}</span></div>` }); document.querySelector('#aside-area-vm').appendChild(toast); toast.firstChild.style.marginLeft = -toast.firstChild.offsetWidth / 2 + 'px'; setTimeout(() => document.querySelector('#aside-area-vm').removeChild(toast), duration); }, // 检测是否处于iframe内嵌环境 inIframe() { try { return window.self !== window.top; } catch (e) { return true; } }, typeOf(val) { const typed = Object.prototype.toString.call(val) switch (typed) { case '[object Object]': return 'object' case '[object Array]': return 'array' case '[object String]': return 'string' case '[object Number]': return 'number' case '[object Boolean]': return 'boolean' case '[object Function]': return 'function' case '[object RegExp]': return 'regex' case '[object Null]': return 'null' case '[object Undefined]': return 'undefined' default: if (val instanceof Element) { return 'element' } return 'unknown' } }, // 根据传入关键词列表生成正则表达式 generatePattern(list) { if (!list || !list.length) return null const keys = list.map(item => { if (this.isVaildRegex(item)) { // 如果字符串为有效的正则表达式则将其内容作为关键词 return this.getRegex(item).source } else { // 作为普通字符串,为避免生成最终正则时产生歧义,先将其转义 return this.escapeRegex(item) } }) let pattern = null try { // 生成正则表达式 pattern = new RegExp(keys.join('|'), 'i'); console.log('pattern', pattern); } catch (e) { console.error(e) } return pattern }, // 检测字符串是否为有效的正则表达式 eg: /^\d+$/ isVaildRegex(str) { // 非字符串直接返回false if (this.typeOf(str) !== 'string') return false // 字符串长度小于3不可能是正则 if (str.length < 3) return false // 如果字符串以/开头,且以/结尾,则可能是正则 if (/^\/.+\/[gimuy]*$/.test(str)) { try { return new RegExp(str) } catch (e) { return false } } return false }, // 根据传入字符串获取正则表达式对象 getRegex(regex) { try { regex = regex.trim(); let parts = regex.split('/'); if (regex[0] !== '/' || parts.length < 3) { regex = regex.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); //escap common string return new RegExp(regex); } const option = parts[parts.length - 1]; const lastIndex = regex.lastIndexOf('/'); regex = regex.substring(1, lastIndex); return new RegExp(regex, option); } catch (e) { return null } }, // 转义正则表达式中的特殊字符 escapeRegex(string) { return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); }, // 加载js或css,返回函数包裹的promise实例,用于顺序加载队列 loadSource(src) { return () => { return new Promise(function (resolve, reject) { const TYPE = src.split('.').pop() let s = null; let r = false; if (TYPE === 'js') { s = document.createElement('script'); s.type = 'text/javascript'; s.src = src; s.async = true; } else if (TYPE === 'css') { s = document.createElement('link'); s.rel = 'stylesheet'; s.type = 'text/css'; s.href = src; } s.onerror = function (err) { reject(err, s); }; s.onload = s.onreadystatechange = function () { // console.log(this.readyState); // uncomment this line to see which ready states are called. if (!r && (!this.readyState || this.readyState == 'complete')) { r = true; console.log(src) resolve(); } }; const t = document.getElementsByTagName('script')[0]; t.parentElement.insertBefore(s, t); }); } }, // 添加css addStyle(css) { if (typeof GM_addStyle != "undefined") { GM_addStyle(css); } else if (typeof PRO_addStyle != "undefined") { PRO_addStyle(css); } else { const node = document.createElement("style"); node.type = "text/css"; node.appendChild(document.createTextNode(css)); const heads = document.getElementsByTagName("head"); if (heads.length > 0) { heads[0].appendChild(node); } else { // no head yet, stick it whereever document.documentElement.appendChild(node); } } } } // ====================== 初始化 ====================== initApp() // 初始化插件 initSettingPanel() // 初始化设置面板 })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址