// ==UserScript== // @name 仙家军成分查询Helper // @namespace www.bilibili.com // @version 1.5.8.1 // @description 用于标记仙家军和动态转发仙以及使用仙话术的b站用户。可能存在误伤,请注意辨别。脚本改自【糊狸-B站成分查询Helper】 // @author Darknights // @match https://*.bilibili.com/* // @match https://*.biligame.com/detail/?id=* // @exclude https://message.bilibili.com/* // @exclude https://manga.bilibili.com/* // @exclude https://www.bilibili.com/correspond/* // @exclude https://www.bilibili.com/page-proxy/* // @exclude https://live.bilibili.com/* // @exclude https://search.bilibili.com/* // @icon https://static.hdslb.com/images/favicon.ico // @connect bilibili.com // @connect biligame.com // @connect fastly.jsdelivr.net // @connect raw.githubusercontent.com // @grant GM_xmlhttpRequest // @grant GM_addStyle // @license MIT // @run-at document-end // @downloadURL none // ==/UserScript== 'use strict'; /* 配置区 */ const config = { urlSource: 1, // 0:githubusercontent, 1:jsdelivr times: 2500, // 标签处理间隔时间 单位:ms testLog: 0, // 是否开启调试日志。0:不开启,1:开启 previewLength: 60, usingCheckProfile: 1, // 是否监测个人主页UID。0:不开启,1:开启 usingCheckComments: 1, // 是否监测评论区。0:不开启,1:开启 usingCheckRepos: 1, // 是否监测转发区。0:不开启,1:开启 usingCheckReferences: 1, // 是否监测动态被转发者。0:不开启,1:开启 usingCheckAts: 1, // 是否监测@他人。0:不开启,1:开启 usingCheckFollows: 1, // 是否监测关注/粉丝列表。0:不开启,1:开启 usingCheckGameComments: 1 // 是否监测游戏评价区。0:不开启,1:开启 } // 显示标签配置在👇面 let xianList; let xianFavList; let xianWordList; let xianLeakList; let ignoreList; //以下为本地名单,可以自行添加 // 大部分为仙,少部分为其他成分但也跑到别游评论区贩过剑,极少数可能有误判 const localXianList = []; // 转发者常见仙的,包含且不限于米吹/被仙死缠烂打的人等等 const localXianFavList = []; // 无视官号和无关号的动态,防止匹配到关键词浪费标签 const localIgnoreList = []; // 仙可能会用的词汇 const localXianWordList = []; // 被开盒者隐私信息,需要特殊处理故与关键词列表区分 const localXianLeakList = []; // 辅助,因为有些正则匹配返回值为空 const aidList = ['响指', '瘴']; const xianTag = ["目标:仙", "#11DD77"]; const xianRepostTag = ["转发仙:", "#1E971E"]; const favRepostTag = ["转发:", "#2C9EFF"]; const xianWordTag = ["仙语:", "#04AEAB"]; const apiTag = ["出错,点此验证", "#FF3434"]; const refreshTag = ["然后点此🔄", "#FF7B00"]; const blog = 'https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/space?&host_mid='; const CheckType = { Profile: 0, Comment: 1, At: 2, Reference: 3, Repo: 4, Follow: 5, GameComment: 6 } const recordMap = new Map(); const uidSet = new Set(); let updateTime; const log = function (message) { return config.testLog ? console.log(message) : null; }; const spawnHtml = function (data) { return `<${data[0]}>`; } const spawnApiHtml = function (data) { return `<${data[0]}>`; } const spawnRefreshHtml = function (data) { return `<${data[0]}>`; } const spawnHtmlWithStrRef = function (data, word, link, text) { return `<${data[0]}${word}>`; } const getxianListUrl = function () { if (config.urlSource === 0) { return "https://raw.githubusercontent.com/Darknights1750/XianLists/main/xianLists.json"; } return "https://fastly.jsdelivr.net/gh/Darknights1750/XianLists@main/xianLists.json"; }; // 检测是不是新版 const isNew = function () { if (location.host === 'space.bilibili.com') { return true; } if (document.getElementsByClassName('item goback').length > 0) { return true; } if (document.getElementsByClassName('app-v1').length > 0) { return true; } if (document.getElementsByClassName('opus-detail').length > 0) { return true; } if (document.getElementsByClassName('bgc').length > 0) { return true; } return false; }; // 检测是不是游戏中心 const checkURL = function () { if (location.host === 'www.biligame.com') { config.usingCheckProfile = 0; config.usingCheckComments = 0; config.usingCheckRepos = 0; config.usingCheckReferences = 0; config.usingCheckAts = 0; config.usingCheckFollows = 0; } else { config.usingCheckGameComments = 0; if(location.host !== 'space.bilibili.com'){ config.usingCheckProfile = 0; config.usingCheckFollows = 0; } } }; const sleep = (delay) => new Promise((resolve) => setTimeout(resolve, delay)); const getXianListOnline = function () { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: getxianListUrl(), data: '', 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: res => { if (res.status === 200) { resolve(JSON.parse(res.responseText)); } else { resolve(JSON.parse('{"xianList":[],"xianFavList":[],"xianWordList":[]}')); } } }); }); } const createRefreshFun = function () { let e = document.createElement('script'); e.innerText = `const refreshTags=function(){Array.prototype.slice.call(document.getElementsByClassName('xian-fail')).forEach((item)=>{item.remove()})}`; document.head.appendChild(e); } const fillLists = async function () { let json = await getXianListOnline(); xianList = [...localXianList, ...json.xianList]; xianFavList = [...localXianFavList, ...json.xianFavList]; ignoreList = [...localIgnoreList, ...json.ignoreList]; let xianWordStrList = [...localXianWordList, ...json.xianWordList]; xianWordList = xianWordStrList.map((item) => new RegExp(item)); let xianLeakStrList = [...localXianLeakList, ...json.xianLeakList]; xianLeakList = xianLeakStrList.map((item) => new RegExp(item)); updateTime = json.updateTime; } const runHelper = function () { /* Functions */ const isBlank = function (str) { if (!str || /^\s*$/.test(str)) { return true; } return false; } const getUid = function (htmlEntity, checkType) { if (checkType === CheckType.Profile) { return window.location.href.match(/(?<=space\.bilibili\.com\/)\d+/)[0]; } if (checkType === CheckType.Comment) { return isNew() ? htmlEntity.dataset.userId : htmlEntity.children[0].href.replace(/[^\d]/g, ""); } if (checkType === CheckType.Repo) { return htmlEntity._profile.uid; } if (checkType === CheckType.At) { return htmlEntity.dataset.oid ? htmlEntity.dataset.oid : htmlEntity.dataset.userId; } if (checkType === CheckType.Reference) { return htmlEntity._profile.uid; } if (checkType === CheckType.Follow) { return htmlEntity.parentElement.href.replace(/[^\d]/g, ""); } if (checkType === CheckType.GameComment) { return htmlEntity.href.replace(/[^\d]/g, ""); } } const getName = function (htmlEntity, checkType) { if (checkType === CheckType.Profile) { return htmlEntity.innerText; } if (checkType === CheckType.Comment) { return isNew() ? htmlEntity.innerText : htmlEntity.children[0].innerText; } if (checkType === CheckType.Repo) { return htmlEntity.innerText; } if (checkType === CheckType.At) { return htmlEntity.innerText.replace(/@/g, ""); } if (checkType === CheckType.Reference) { return htmlEntity.innerText; } if (checkType === CheckType.Follow) { return htmlEntity.innerText; } if (checkType === CheckType.GameComment) { return htmlEntity.innerText; } } const getCommentList = function () { if (isNew()) { const lst = new Set(); for (let c of document.getElementsByClassName('user-name')) { lst.add(c); } for (let c of document.getElementsByClassName('sub-user-name')) { lst.add(c); } return Array.from(lst); } else { return document.getElementsByClassName('user'); } } const getRepoList = function () { return document.getElementsByClassName('bili-dyn-forward-item__uname'); } const getReferenceList = function () { return document.getElementsByClassName('dyn-orig-author__name'); } const getFollowList = function () { return Array.from(document.getElementsByClassName('fans-name')); } const getGameCommentList = function () { return Array.from(document.querySelectorAll("a.user-name")); } const getAtList = function () { const lst = new Set(); for (let c of document.getElementsByClassName('jump-link user')) { lst.add(c); } for (let c of document.getElementsByClassName('bili-rich-text-module at')) { lst.add(c); } return Array.from(lst); } const spliceText = function (moduleDynamic) { let fullTextArr = []; if (moduleDynamic.topic != null && !isBlank(moduleDynamic.topic.name)) { fullTextArr.push(moduleDynamic.topic.name); } if (moduleDynamic.desc != null && !isBlank(moduleDynamic.desc.text)) { fullTextArr.push(moduleDynamic.desc.text); } if (moduleDynamic.major != null) { if (moduleDynamic.major.archive != null) { if (!isBlank(moduleDynamic.major.archive.title)) { fullTextArr.push(moduleDynamic.major.archive.title); } if (!isBlank(moduleDynamic.major.archive.desc)) { fullTextArr.push(moduleDynamic.major.archive.desc); } } if (moduleDynamic.major.article != null) { if (!isBlank(moduleDynamic.major.article.title)) { fullTextArr.push(moduleDynamic.major.article.title); } if (!isBlank(moduleDynamic.major.article.desc)) { fullTextArr.push(moduleDynamic.major.article.desc); } } if (moduleDynamic.major.live != null && !isBlank(moduleDynamic.major.live.title)) { fullTextArr.push(moduleDynamic.major.live.title); } } if (moduleDynamic.additional != null && moduleDynamic.additional.ugc != null && !isBlank(moduleDynamic.additional.ugc.title)) { fullTextArr.push(moduleDynamic.additional.ugc.title); } return fullTextArr.join('//'); } const previewText = function (text, index, len) { const left = Math.max(0, index - len); const right = Math.min(text.length, index + len); let textPart = ''; if (left > 0) { textPart += '...'; } textPart += text.substring(left, right).replace(/\n|\r/g, '').trim(); if (right < text.length) { textPart += '...'; } return textPart; } const findRepost = function (items, ownId, isFav) { const usingList = isFav ? xianFavList : xianList; for (let i = 0; i < items.length; i++) { const item = items[i]; for (const key in item) { if (key === 'orig') { const origId = String(item.orig.modules.module_author.mid); if (origId === ownId) { return null; } if (usingList.indexOf(origId) > -1) { const origName = String(item.orig.modules.module_author.name); const ownFullText = spliceText(item.modules.module_dynamic); const origFullText = spliceText(item.orig.modules.module_dynamic); const ownTextPart = previewText(ownFullText, 0, config.previewLength); const origTextPart = previewText(origFullText, 0, config.previewLength); const bothTextPart = `${ownTextPart}//@${origName}:${origTextPart}`; const link = String(item.id_str); return [origId, origName, link, bothTextPart]; } } } } return null; } const hear = function (text, name) { if (isBlank(text) || ignoreList.indexOf(name) >= 0) { return null; } for (const word of xianWordList) { const matchRes = text.match(word); if (matchRes != null) { let matchStr = matchRes[0]; let matchIndex = matchRes.index; if (matchStr === '') { for (const aidWord of aidList) { const matchAid = text.match(aidWord); if (matchAid != null) { matchStr = matchAid[0]; matchIndex = matchAid.index; break; } } } matchStr = matchStr.replace(/\n|\r/g, '').trim(); return [matchStr, matchIndex + matchStr.length / 2]; } } for (const word of xianLeakList) { const matchRes = text.match(word); if (matchRes != null) { return ['被开盒者隐私', -1]; } } return null; } /** * 查找关键词 * @param {} items 动态列表 * @returns [关键词,动态id,动态片段] */ const findWord = function (items, ownName) { for (let i = 0; i < items.length; i++) { const item = items[i]; let origName; let ownFullText = spliceText(item.modules.module_dynamic); let origFullText; let ownTextPart; let origTextPart; let returnWord; // 关键词 let returnPart; // 预览文本 for (const key in item) { if (key === 'orig') { origName = String(item.orig.modules.module_author.name); origFullText = spliceText(item.orig.modules.module_dynamic); break; } } const ownMatch = hear(ownFullText, ownName); const origMatch = hear(origFullText, origName); if (ownMatch == null && origMatch == null) { continue; } if (ownMatch == null) { returnWord = '🔁' + origMatch[0]; } else { returnWord = ownMatch[0]; } const ownIndex = ownMatch ? ownMatch[1] : 0; const origIndex = origMatch ? origMatch[1] : 0; ownTextPart = ownIndex < 0 ? '*已隐藏*' : previewText(ownFullText, ownIndex, config.previewLength / 2); returnPart = ownTextPart; if (!isBlank(origName)) { origTextPart = origIndex < 0 ? '*已隐藏*' : previewText(origFullText, origIndex, config.previewLength / 2); returnPart = ownTextPart + `//@${origName}:${origTextPart}`; } return [returnWord, String(item.id_str), returnPart]; } return null; } //检查记录 const findRecord = async function (uid, name) { let oldTag; if (recordMap.has(uid)) { oldTag = recordMap.get(uid); uidSet.delete(uid); if (oldTag) { log('>>Record:' + name + '@UID-' + uid + '>>find>>' + oldTag.replaceAll(/<\/?a.*?>/g, "").replaceAll(/></g, "、").replaceAll(/&.t;/g, "")); } } else if (uidSet.has(uid)) { await sleep(500); oldTag = findRecord(uid, name); } else { uidSet.add(uid); } return oldTag; } const checkEntity = async function (htmlEntity, checkType) { if (htmlEntity.innerHTML.indexOf(` { if (res.status === 200) { let newTag = ''; if (xianList.indexOf(uid) > -1) { log('>>Find Target:' + name + '@UID-' + uid + '>>' + checkType); newTag += spawnHtml(xianTag); } const dynamicJson = JSON.parse(res.response).data; if (dynamicJson) { if (dynamicJson.items) { const repostMatch = findRepost(dynamicJson.items, uid, false); if (repostMatch != null) { log('>>Find Repost:' + name + '@UID-' + uid + '>>repost>>' + repostMatch[1] + '@UID-' + repostMatch[0] + '>>' + checkType); newTag += spawnHtmlWithStrRef(xianRepostTag, repostMatch[1], repostMatch[2], repostMatch[3]); } const wordMatch = findWord(dynamicJson.items, name); if (wordMatch != null) { log('>>Find Word:' + name + '@UID-' + uid + '>>say>>' + wordMatch[0] + '>>' + checkType); let fixedText = wordMatch[0]; if (fixedText.length > 15) { fixedText = fixedText.slice(0, 12) + '...'; } newTag += spawnHtmlWithStrRef(xianWordTag, fixedText, wordMatch[1], wordMatch[2]); } const favRepostMatch = findRepost(dynamicJson.items, uid, true); if (favRepostMatch != null) { log('>>Find Fav:' + name + '@UID-' + uid + '>>repost>>' + favRepostMatch[1] + '@UID-' + favRepostMatch[0] + '>>' + checkType); newTag += spawnHtmlWithStrRef(favRepostTag, favRepostMatch[1], favRepostMatch[2], favRepostMatch[3]); } } htmlEntity.innerHTML += newTag; recordMap.set(uid, newTag); } else { xianSpan.className = 'xian-fail'; htmlEntity.innerHTML += spawnApiHtml(apiTag); htmlEntity.innerHTML += spawnRefreshHtml(refreshTag); uidSet.delete(uid); log('仙家军成分查询Helper get dynamic fail...'); log(htmlEntity); log(res); } } else { xianSpan.className = 'xian-fail'; log('仙家军成分查询Helper request fail...'); log(htmlEntity); log(res); } }, }); } } } const checkComments = function () { const commentlist = getCommentList(); if (commentlist != null && commentlist.length > 0) { commentlist.forEach(htmlEntity => { checkEntity(htmlEntity, CheckType.Comment); }); } } const checkRepos = function () { const repolist = getRepoList(); if (repolist != null && repolist.length > 0) { repolist.forEach(htmlEntity => { checkEntity(htmlEntity, CheckType.Repo); }); } } const checkAts = function () { const atList = getAtList(); if (atList != null && atList.length > 0) { atList.forEach(htmlEntity => { checkEntity(htmlEntity, CheckType.At); }); } } const checkReferences = function () { const referenceList = getReferenceList(); if (referenceList != null && referenceList.length > 0) { referenceList.forEach(htmlEntity => { checkEntity(htmlEntity, CheckType.Reference); }); } } const checkFollows = function () { const followList = getFollowList(); if (followList != null && followList.length > 0) { followList.forEach(htmlEntity => { checkEntity(htmlEntity, CheckType.Follow); }); } } const checkProfile = async function () { let htmlEntity = document.getElementById('h-name'); if (htmlEntity != null) { checkEntity(htmlEntity, CheckType.Profile); } } const checkGameComments = async function () { const gameCommentList = getGameCommentList(); if (gameCommentList != null && gameCommentList.length > 0) { gameCommentList.forEach(htmlEntity => { checkEntity(htmlEntity, CheckType.GameComment); }); } } log(`仙家军成分查询Helper,启动! >>isNew: ${isNew()} >>Loading: ${window.location.href} >>List update time: ${updateTime} `) setInterval(() => { if (config.usingCheckProfile) { checkProfile(); } if (config.usingCheckComments) { checkComments(); } if (config.usingCheckRepos) { checkRepos(); } if (config.usingCheckReferences) { checkReferences(); } if (config.usingCheckAts) { checkAts(); } if (config.usingCheckFollows) { checkFollows(); } if (config.usingCheckGameComments) { checkGameComments(); } }, config.times); } const start = async function () { checkURL(); createRefreshFun(); await fillLists(); runHelper(); } start();