Last active
February 26, 2024 07:29
-
-
Save journey-ad/664ec90813bc1ff99d4538fdbe76f8ec to your computer and use it in GitHub Desktop.
bilibili直播SC过滤
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Bilibili直播SC过滤 | |
// @namespace https://github.com/journey-ad | |
// @version 0.3.2 | |
// @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://greasyfork.org/scripts/417560-bliveproxy/code/bliveproxy.js?version=984333 | |
// @grant none | |
// @license MIT | |
// @run-at document-end | |
// ==/UserScript== | |
(function () { | |
'use strict'; | |
const __SCRIPT_VERSION = '0.3.2'; | |
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); | |
}, | |
onError: (error, handler) => { | |
// 触发b站自己的xhr请求错误处理逻辑,避免一些未预期的行为 如无法显示大航海列表 | |
handler?.xhrProxy?.onerror?.() | |
handler.next(error) | |
} | |
}) | |
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.setting-btn svg { | |
fill: #222; | |
} | |
.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; | |
width: 250px; | |
white-space: nowrap; | |
text-overflow: ellipsis; | |
} | |
.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 span { | |
width: 210px; | |
} | |
.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 { | |
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 { | |
display: flex; | |
justify-content: flex-start; | |
max-width: 220px; | |
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 .uid { | |
flex: none; | |
} | |
.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"/><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 :title="item">{{ 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" :title="item.uname">{{ 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() // 初始化设置面板 | |
})(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.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; | |
* { | |
box-sizing: border-box; | |
} | |
::-webkit-scrollbar { | |
width : 4px !important; | |
height: 4px !important; | |
} | |
::-webkit-scrollbar-button { | |
width : 0; | |
height: 0; | |
} | |
::-webkit-scrollbar-thumb { | |
background : #e1e1e1 !important; | |
border-radius: 4px; | |
} | |
fieldset { | |
margin : 0; | |
padding: 0 4px; | |
border : 1px solid #efefef; | |
legend { | |
padding : 0 4px; | |
margin-bottom: 2px; | |
} | |
} | |
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; | |
&::-webkit-input-placeholder { | |
color: #ccc; | |
} | |
&:focus { | |
border-color: #4caf50; | |
} | |
} | |
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 .12s ease-in-out; | |
&:hover { | |
color : #fff; | |
background: #23ade5; | |
} | |
} | |
.header-bar { | |
display : flex; | |
justify-content: space-between; | |
align-items : center; | |
font-size : 14px; | |
padding : 0 0 6px; | |
border-bottom : 1px solid #efefef; | |
h4 { | |
font-size: 14px; | |
margin : 0; | |
} | |
.btn { | |
cursor : pointer; | |
user-select : none; | |
font-size : 0; | |
margin-right: 2px; | |
&.setting-btn { | |
svg { | |
fill: #222; | |
} | |
} | |
svg { | |
fill: #444; | |
} | |
} | |
.setting-btn { | |
margin-left : auto; | |
margin-right: 8px; | |
} | |
} | |
.setting-content { | |
display : flex; | |
flex-wrap : wrap; | |
justify-content: flex-start; | |
overflow : hidden; | |
.setting-item, | |
.func-item { | |
float : left; | |
display : flex; | |
align-items: center; | |
height : 24px; | |
margin-top : 4px; | |
} | |
.setting-item { | |
margin: 0 5px; | |
input[type="checkbox"] { | |
cursor: pointer; | |
} | |
label { | |
margin-left: 2px; | |
cursor : pointer; | |
user-select: none; | |
} | |
} | |
} | |
.section { | |
margin-top: 10px; | |
.empty { | |
display : flex; | |
justify-content: center; | |
align-items : center; | |
height : 40px; | |
color : #5e5e5e; | |
} | |
} | |
.keyword-wrap { | |
display : flex; | |
justify-content: space-between; | |
align-items : center; | |
.sc-block-keyword {} | |
.add-keyword, | |
.add-uid { | |
flex: none; | |
} | |
} | |
.block-list { | |
min-height: 50px; | |
max-height: 122px; | |
margin-top: 8px; | |
overflow-y: auto; | |
&.list-content { | |
.block-item { | |
display : flex; | |
justify-content: space-between; | |
align-items : center; | |
width : 250px; | |
white-space : nowrap; | |
text-overflow : ellipsis; | |
&.is-regex { | |
color : #ff9800; | |
background: #fff9c5; | |
&::after { | |
content : "[正则]"; | |
position : absolute; | |
top : 50%; | |
right : 28px; | |
transform: translateY(-50%); | |
color : #ccc; | |
} | |
span { | |
width: 190px; | |
} | |
} | |
span { | |
width: 210px; | |
} | |
button { | |
margin-right: 6px; | |
} | |
} | |
} | |
&.list-user { | |
.block-item { | |
button { | |
position: absolute; | |
right : 6px; | |
top : 12px; | |
} | |
} | |
} | |
.block-item { | |
position : relative; | |
padding : 2px 6px; | |
padding-right: 0; | |
&:nth-of-type(odd) { | |
background-color: #f9f9f9; | |
border-top : 1px solid #FAFAFA; | |
} | |
&:hover { | |
button { | |
opacity: 1; | |
} | |
} | |
a { | |
color : #23ade5; | |
cursor: pointer; | |
} | |
span { | |
margin-right : 4px; | |
white-space : nowrap; | |
overflow : hidden; | |
text-overflow: ellipsis; | |
} | |
button { | |
flex : none; | |
width : 16px; | |
height : 16px; | |
border-radius: 50%; | |
font-size : 10px; | |
margin-left : 4px; | |
opacity : 0; | |
transition : .2s opacity ease-in-out; | |
} | |
.user-info, | |
.meta-info { | |
display : flex; | |
justify-content: flex-start; | |
max-width : 220px; | |
line-height : 16px; | |
} | |
.meta-info { | |
font-size: 11px; | |
color : #9f9f9f; | |
a { | |
color: #9f9f9f; | |
} | |
} | |
.uid { | |
flex: none; | |
} | |
.message { | |
height : 24px; | |
line-height : 24px; | |
width : 235px; | |
white-space : nowrap; | |
overflow : hidden; | |
text-overflow: ellipsis; | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment