仙家军成分查询Helper

用于标记仙家军和动态转发仙以及使用仙话术的b站用户。可能存在误伤,请注意辨别。脚本改自【糊狸-B站成分查询Helper】

目前为 2023-12-03 提交的版本。查看 最新版本

// ==UserScript==
// @name         仙家军成分查询Helper
// @namespace    www.bilibili.com
// @version      1.5.8.5
// @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
// ==/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 = [];

// 转发者常见仙的,包含且不限于一些up主/被仙缠上的人等等
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_URL = 'https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/space?&host_mid=';

const PRIVATE_TIPS = '*已隐藏,注意可能是无关话题被匹配*';

const XIAN_MATCH_TIPS = '*可能存在误判,请注意辨别*';

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,text) {
    return `<a class="xian" style='color: ${data[1]} !important' title='${text}' target='_blank'>&lt;${data[0]}&gt;</a>`;
}

const spawnApiHtml = function (data) {
    return `<a class="xian-fail" style='color: ${data[1]} !important' href='https://space.bilibili.com/208259/dynamic' target='_blank'>&lt;${data[0]}&gt;</a>`;
}

const spawnRefreshHtml = function (data) {
    return `<a class="xian-fail" style='color: ${data[1]} !important' target='_blank' onclick='refreshTags()'>&lt;${data[0]}&gt;</a>`;
}

const spawnHtmlWithRef = function (data, word, link, text) {
    return `<a class="xian" style='color: ${data[1]} !important' href='https://t.bilibili.com/${link}' title='${text}' target='_blank'>&lt;${data[0]}${word}&gt;</a>`;
}

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 ? PRIVATE_TIPS : previewText(ownFullText, ownIndex, config.previewLength / 2);
            returnPart = ownTextPart;
            if (!isBlank(origName)) {
                origTextPart = origIndex < 0 ? PRIVATE_TIPS : 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(/&gt;&lt;/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(`<span class="xian`) === -1) {
            let xianSpan = document.createElement('span');
            xianSpan.className = 'xian';
            htmlEntity.appendChild(xianSpan);
            const uid = String(getUid(htmlEntity, checkType));
            if (isBlank(uid)) {
                return;
            }
            const name = getName(htmlEntity, checkType).trim();
            if (ignoreList.indexOf(name) >= 0) {
                return;
            }
            let oldTag = await findRecord(uid, name);

            if (oldTag != null) {
                htmlEntity.innerHTML += oldTag;
            } else {
                GM_xmlhttpRequest({
                    method: "get",
                    url: BLOG_URL + uid,
                    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) {
                            let newTag = '';
                            if (xianList.indexOf(uid) > -1) {
                                log('>>Find Target:' + name + '@UID-' + uid + '>>' + checkType);
                                newTag += spawnHtml(xianTag, XIAN_MATCH_TIPS);
                            }
                            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 += spawnHtmlWithRef(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 += spawnHtmlWithRef(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 += spawnHtmlWithRef(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();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址