Skip to content

Instantly share code, notes, and snippets.

@journey-ad
Last active February 26, 2024 07:29
Show Gist options
  • Save journey-ad/664ec90813bc1ff99d4538fdbe76f8ec to your computer and use it in GitHub Desktop.
Save journey-ad/664ec90813bc1ff99d4538fdbe76f8ec to your computer and use it in GitHub Desktop.
bilibili直播SC过滤
// ==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/jquery@3.2.1/dist/jquery.min.js
// @require https://cdn.jsdelivr.net/npm/ajax-hook@2.0.3/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() // 初始化设置面板
})();
.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