// ==UserScript==
// @name 霹雳评论区成分指示器
// @namespace me.valstrax.markusslugia
// @version 0.7.0
// @description B站评论区用户自动/手动查成分
// @author markusslugia
// @match https://www.bilibili.com/video/*
// @match https://t.bilibili.com/*
// @match https://space.bilibili.com/*
// @match https://space.bilibili.com/*
// @match https://www.bilibili.com/read/*
// @icon https://static.hdslb.com/images/favicon.ico
// @connect bilibili.com
// @grant GM_xmlhttpRequest
// @license MIT
// @run-at document-end
// ==/UserScript==
(function () {
'use strict';
const filters = [
// 在这里定义标签规则
{
tag: "抽奖人",
keywords: [["抽", "转"], ["送", "中"], "送我"]
// 最基础的规则由tag和keywords组成
// tag为标记显示的文字,而keywords为识别用的关键词
},
{
tag: "农批",
keywords: ["王者荣耀", "王者", "农药", ["星耀", "上分"], "阿轲"],
// keywords Array中可以有多个OR逻辑的关键词,其中再使用[数组]包裹的部分为AND逻辑
// 比如说,以上代码将匹配“来玩王者荣耀”、“我玩阿轲”,但不会匹配“今天我要上分”
color: "#E03050"
// 可以单独指定标签颜色,别忘了在前面加“#”嗼!
},
{
tag: "粥粥壬",
regex: /(?=.*明日方舟).*|(?=.*罗德岛).*|(?=.*原石)(?=.*理智).*|(?=.*作战)(?=.*理智).*|(?=.*刀客塔).*|(?=.*海猫)(?!.*海猫鸣泣).*$/,
// 如果有需求,也可以直接指定编写好的正则
// regex和keywords同时存在的情况下,regex优先级更高嗼!
color: "#3060F0"
},
{
tag: "是彗宝粉丝捏",
filter: (input) => input.match(/(?=.*天彗).*|(?=.*银翼凶星).*|(?=.*天慧).*$/),
// 还有更加离谱的需求?没问题,也可以通过filter直接传入匹配函数!
// 函数传入的参数为检测的动态文本
// 函数返回true或其他会被判为真的值代表匹配成功,否则代表不匹配
// filter的优先级高于regex和keyword
color: "linear-gradient(to top left, #606080, #E00230 75%, #E00042)"
// 也可以传入其他css background支持的格式。
},
{
// 越靠后的标签会越先被匹配到嗼!
tag: "我焯,原",
keywords: ["原神", "可莉", "满命", ["旅行者", "蒙德"], "璃月", "稻妻", "须弥", ["七七", "椰奶"], "钟离"],
color: "#EBA434"
},
// 不论添加顺序如何,merge规则总是在filter后起效
// 因此写在后面也不用担心
// 不过merge规则列表本身也会有先后顺序,要注意嗼
{
tag: "双P合璧",
merge: ["我焯,原", "农批"],
// 使用merge来合并标签,两个标签同时出现时即合并为此merge标签
color: "linear-gradient(to top left, #E03050, #EBA434 75%, #EBA434)"
},
{
tag: "二刺螈奔赴",
merge: ["我焯,原", "粥粥壬"],
color: "linear-gradient(to top left, #3060F0, #34DB9A 75%, #84DB6A)"
},
{
tag: "不喜欢原",
merge: ["粥粥壬", "农批"],
color: "linear-gradient(to top left, #3060F0, #E03050 75%, #E03050)"
},
{
tag: "三角力量",
merge: ["我焯,原", "粥粥壬", "农批"],
// 越靠后的merge标签会越先匹配到,匹配到后会立即清除被merge的标签
// 比如说,假如“不喜欢原”先于“三角力量”被匹配,则“三角力量”永远无法被匹配到
// 因为匹配所需的其中两个标签必定会被“不喜欢原”抢先清除掉
// 因此,包含更多标签的merge规则应该处于更靠后的位置
color: "linear-gradient(to top left, #3060F0, #EA4A5F 45%, #EB5854 50%, #EBA434 90%)"
},
];
const options = {
// 可以指定选项(当然也可以不指定)
interval: 3000,
// interval指定脚本检查评论区的间隔,以毫秒为单位。
// 1000毫秒等于1秒嗼!
multiTag: true,
// multiTag指定是否允许给同一个人打多个标签。
// 设为false时,一个用户名后面只会显示优先级最高的一个标签。
lazy: false,
// 启用lazy模式后,将不会自动查询评论区用户主页内容。
// 用户名后会显示一个查成分按钮,点击即可查该用户成分。
// 如果网络环境差,则可以考虑手动开启lazy模式。
pageLoad: 1,
// pageLoad指定加载动态的页数。
// 由于B站API的限制,一次请求只能加载12条动态,默认是最新的12条。
// 要加载更老的动态,需要再次发起请求。这样会导致查成分的时间成倍增长。
// 因此,除非对自己的网络很有信心,建议仅加载1-2页。
// 或者也可以开启lazy模式,然后设置为加载更多页。
defaultColor: "#202020",
// defaultColor指定标签规则内未指定颜色时的默认颜色。
emptyText: "纯洁的路人",
// emptyText指定未匹配到的默认文本,默认为空。
// 为空时,未匹配到的用户将不会显示标签。
emptyColor: "#FF8AA0"
// emptyColor指定默认文本的颜色,指定默认文本后才有效果。
};
// ==================================================
// 不建议改动下面的内容,除非你很清楚你在做什么
// ==================================================
class UserFilter {
constructor(allTags, options = {}) {
const { interval, multiTag, lazy, pageLoad, defaultColor, emptyText, emptyColor } = options;
this.interval = interval ? interval : 3000;
this.multiTag = multiTag ? true : false;
this.lazy = lazy ? true : false;
this.pageLoad = pageLoad ? pageLoad : 1;
this.defaultColor = defaultColor ? defaultColor : "#1060FF";
this.emptyText = emptyText ? emptyText : "";
this.emptyColor = emptyColor ? emptyColor : defaultColor;
this.elFiltStateKey = "silver-jet-engine-dragon-" + Math.random().toString().replace(".", "v");
this.pidList = new Set();
let styleNode = document.createElement("style");
styleNode.innerHTML =
`.${this.elFiltStateKey}{`
+ "display:inline;"
+ "color:white;"
+ "margin-left:2px;"
+ "padding:2px 4px;"
+ "height:100%;"
+ "}"
+ `.${this.elFiltStateKey}:first-of-type{`
+ "border-top-left-radius:20px;"
+ "border-bottom-left-radius:20px;"
+ "margin-left:8px;"
+ "padding-left:6px;"
+ "}"
+ `.${this.elFiltStateKey}:last-of-type{`
+ "border-top-right-radius:20px;"
+ "border-bottom-right-radius:20px;"
+ "margin-right:2px;"
+ "padding-right:6px;"
+ "}";
document.head.appendChild(styleNode);
this.emptyEl = document.createElement("div");
if (this.emptyText != "") {
this.emptyEl.className = this.elFiltStateKey;
this.emptyEl.innerHTML = emptyText;
}
else {
this.emptyEl.setAttribute(this.elFiltStateKey, this.elFiltStateKey);
}
this.emptyEl.style.fontWeight = "bold";
this.emptyEl.style.backgroundColor = this.emptyColor;
if (this.lazy) {
this.elFiltPromptKey = "green-insect-electric-dragon-" + Math.random().toString().replace(".", "v");
this.promptEl = document.createElement("div");
this.promptEl.className = this.elFiltPromptKey;
this.promptEl.innerHTML = "查成分";
let styleNodePrompt = document.createElement("style");
styleNodePrompt.innerHTML =
`.${this.elFiltPromptKey}{`
+ "display:inline;"
+ "color:#2060FF;"
+ "background-color:rgba(16,96,255,0.1);"
+ "border-radius:20px;"
+ "border:1px solid #2060FF;"
+ "margin-left:6px;"
+ "padding:1px 5px;"
+ "cursor:pointer;"
+ "}";
document.head.appendChild(styleNodePrompt);
}
if (Array.isArray(allTags)) {
for (const tagOptions of allTags) {
this.addFilter(tagOptions);
}
}
};
filters = [];
merges = [];
pidDict = {};
blog = 'https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/space?&host_mid=';
addFilter(options) { //{tag,keywords/regex/filter,color}
if (typeof options.tag != "string") {
console.error("某个传入标签的tag缺失或有误,因此未添加这个标签。请排查。");
return;
}
let element = this.emptyEl.cloneNode(false);
element.className = this.elFiltStateKey;
element.style.background = options.color ? options.color : this.defaultColor;
element.innerHTML = options.tag;
if (Array.isArray(options.merge)) {
let mergeFunction = (tagNodes) => {
let check = true;
let removes = [];
tagLoop: for (const tag of options.merge) {
let removeIndex = -1;
nodeLoop: for (let index = 0; index < tagNodes.length; index++) {
if (tagNodes[index].innerHTML == tag) {
removeIndex = index;
break nodeLoop;
}
}
if (removeIndex != -1) {
removes.push(removeIndex);
}
else {
check = false;
break tagLoop;
}
}
if (check) {
removes.sort((a, b) => b - a);
for (const removeIndex of removes) {
tagNodes.splice(removeIndex, 1);
}
return true;
}
else return false;
};
this.merges.unshift({
tag: element,
merge: mergeFunction
});
}
else if (typeof options.filter == "function") {
this.filters.unshift({
tag: element,
filter: options.filter
});
}
else if (options.regex instanceof RegExp) {
this.filters.unshift({
tag: element,
filter: input => input.match(options.regex)
});
}
else if (Array.isArray(options.keywords)) {
let regex = UserFilter.regexBuilder(options.keywords);
this.filters.unshift({
tag: element,
filter: input => input.match(regex)
});
}
else if (typeof options.keywords == "string") {
let regex = UserFilter.regexBuilder([options.keywords]);
this.filters.unshift({
tag: element,
filter: input => input.match(regex)
});
}
else console.error("传入标签 \"" + options.tag + "\"的过滤条件缺失或有误,因此未添加这个标签。");
}
static regexBuilder(keywords) {
let regexText = "^";
for (const keyword of keywords) {
let rule = "";
if (regexText != "^") rule += "|";
if (Array.isArray(keyword)) {
for (const andKeyword of keyword) {
rule += `(?=.*${andKeyword})`;
}
} else {
rule += `(?=.*${keyword})`;
}
rule += ".*";
regexText += rule;
}
regexText += "$";
return new RegExp(regexText);
}
requestPosts(pid, offset) {
return new Promise((resolve, reject) => {
if (this.pidDict[pid] != undefined) {
resolve(this.pidDict[pid]);
return;
}
let offsetQuery = offset ? "&offset=" + offset : "";
let blogurl = this.blog + pid + offsetQuery;
GM_xmlhttpRequest({
method: "get",
url: blogurl,
headers: {
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36'
},
onload: function (res) {
if (res.status === 200) {
resolve(JSON.parse(res.response).data);
} else {
reject();
}
},
});
});
}
async filterPid(pid, pages = 1) {
let offset = "";
let postTexts = [];
let retry = 0;
for (let i = 0; i < pages; i++) {
try {
let data = await this.requestPosts(pid, offset);
if (data.has_more) {
offset = parseInt(data.offset) + 1;
} else {
i = pages;
}
for (const item of data.items) {
let concatString = "";
if (item.modules.module_dynamic.desc) {
concatString += JSON.stringify(item.modules.module_dynamic.desc.text);
}
if (item.orig && item.orig.modules.module_dynamic.desc) {
concatString += JSON.stringify(item.orig.modules.module_dynamic.desc.text);
}
postTexts.push(concatString);
}
} catch {
if (retry < 3) {
retry += 1;
console.error("PID" + pid + "的第" + (i + 1) + "页动态请求失败,第" + retry + "次,最多重试3次");
i -= 1;
}
else return;
}
}
this.pidList.delete(pid);
let filtedNodes = [];
filterLoop: for (const filter of this.filters) {
postLoop: for (const post of postTexts) {
if (filter.filter(post)) {
filtedNodes.push(filter.tag);
break postLoop;
}
}
}
let mergedNodes = [];
mergeLoop: for (const merge of this.merges) {
if (merge.merge(filtedNodes)) {
mergedNodes.push(merge.tag);
};
}
let tagNodes = mergedNodes.concat(filtedNodes);
if (tagNodes.length) {
if (this.multiTag) this.pidDict[pid] = tagNodes;
else this.pidDict[pid] = tagNodes.slice(-1);
}
else {
this.pidDict[pid] = [this.emptyEl];
}
}
static getPid(item) {
let pid = item.getAttribute("data-user-id");
if (!pid) pid = item.getAttribute("data-usercard-mid");
if (!pid) pid = 114514;
return pid;
}
static getCommentList() {
return document.querySelectorAll(".user-name,.sub-user-name,.user>.name");
};
addPids(commentList) {
for (const item of commentList) {
let pid = UserFilter.getPid(item);
if (this.pidDict[pid] == undefined) {
this.pidList.add(UserFilter.getPid(item));
}
}
}
updateDom(commentList) {
commentList.forEach(item => {
if (item.innerHTML.indexOf(this.elFiltStateKey) == -1) {
let pid = UserFilter.getPid(item);
if (this.pidDict[pid] != undefined) {
if (this.lazy) {
try {
item.removeChild(item.querySelector("." + this.elFiltPromptKey));
}
catch { void (0); }
}
if (this.pidDict[pid]) {
for (const element of this.pidDict[pid]) {
item.appendChild(element.cloneNode(true));
}
}
}
else if (this.lazy && item.innerHTML.indexOf(this.elFiltPromptKey) == -1) {
let newEl = this.promptEl.cloneNode(true);
newEl.onclick = (event) => {
event.cancelBubble = true;
event.preventDefault();
if (event.stopPropagation) event.stopPropagation();
this.lazyCheck(event);
};
item.appendChild(newEl);
}
}
});
};
lazyCheck(event) {
let pid = UserFilter.getPid(event.target.parentNode);
let commentList = UserFilter.getCommentList();
let checklist = [];
for (const item of commentList) {
if (item.innerHTML.indexOf(this.elFiltStateKey) == -1) {
checklist.push(item);
}
}
this.filterPid(pid, this.pageLoad)
.then(() => this.updateDom(checklist))
.catch(err => console.error(err));
}
main() {
let commentList = UserFilter.getCommentList();
let checklist = [];
if (this.lazy) {
for (const item of commentList) {
if (
item.innerHTML.indexOf(this.elFiltStateKey) == -1
&& item.innerHTML.indexOf(this.elFiltPromptKey) == -1
) {
checklist.push(item);
}
}
this.updateDom(checklist);
} else {
for (const item of commentList) {
if (item.innerHTML.indexOf(this.elFiltStateKey) == -1) {
checklist.push(item);
}
}
this.addPids(checklist);
for (const pid of this.pidList) {
this.filterPid(pid, this.pageLoad)
.then(() => this.updateDom(checklist))
.catch(err => console.error(err));
}
}
}
start() {
this.stop();
this.thread = setInterval(() => this.main(), this.interval);
}
stop() {
if (this.thread != undefined) clearInterval(this.thread);
}
};
console.log("哔哩哔哩评论Tag脚本开始执行");
//初始化时即可传入标签Array,也可以后续使用addFilter()添加
const app = new UserFilter(filters, options);
app.start();
})();