Greasy Fork

Greasy Fork is available in English.

仙家军成分查询Helper

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

当前为 2023-12-02 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         仙家军成分查询Helper
// @namespace    www.bilibili.com
// @version      1.5.8.3
// @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 = '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,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 ? '*已隐藏*' : 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(/&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();
            let oldTag = await findRecord(uid, name);

            if (oldTag != null) {
                htmlEntity.innerHTML += oldTag;
            } else {
                GM_xmlhttpRequest({
                    method: "get",
                    url: blog + 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,'可能存在误判,请注意辨别');
                            }
                            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();