评论隐藏信息

给视频或者根评论点踩即可获取并复制隐藏信息,发送<|内容|过期天数||>或者<|内容|过期天数|密码|>到已有评论下可以创建隐藏信息

// ==UserScript==
// @name         评论隐藏信息
// @version      2025-08-17-ddd
// @description  给视频或者根评论点踩即可获取并复制隐藏信息,发送<|内容|过期天数||>或者<|内容|过期天数|密码|>到已有评论下可以创建隐藏信息
// @author       RK
// @match        *://*.bilibili.com/*
// @connect      txttool.cn
// @icon         https://static.hdslb.com/images/favicon.ico
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/sweetalert2.all.min.js#md5=D4Le54swPmFIKP1ejrXgqg==
// @require      https://cdn.bootcdn.net/ajax/libs/crypto-js/4.1.1/crypto-js.min.js#md5=LKA62HiFq5g1QQkrh62ymQ==
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_setClipboard
// @grant        GM_xmlhttpRequest
// @grant        unsafeWindow
// @grant        GM_getResourceText
// @resource     DATA https://api.bilibili.com/x/emote/package?ids=1&business=reply
// @license      WTFPL
// @namespace    https://greasyfork.org/users/1503113
// ==/UserScript==

(function () {
    'use strict';
    const oFetch = fetch;
    var originalSend = XMLHttpRequest.prototype.send;

    const hateApi = '//api.bilibili.com/x/v2/reply/hate';
    const getNoteApi = 'https://api.txttool.cn/netcut/note/info';
    const addApi = '//api.bilibili.com/x/v2/reply/add';
    const saveNoteApi = 'https://api.txttool.cn/netcut/note/save';
    const replyApi = '//api.bilibili.com/x/v2/reply/reply';
    const likeApi = '//api.bilibili.com/x/web-interface/archive/like';


    const expireTime = new Map();
    expireTime.set('1H', 3600);
    expireTime.set('6H', 21600);
    expireTime.set('1D', 86400);
    expireTime.set('3D', 259200);
    expireTime.set('1W', 604800);
    expireTime.set('1M', 2592000);
    expireTime.set('3M', 7776000);
    expireTime.set('6M', 15552000);
    expireTime.set('1Y', 31536000);
    expireTime.set('2Y', 63072000);
    expireTime.set('3Y', 94608000);

    function getMessage(oid, rpid) {
        const params = new URLSearchParams();
        params.append('note_name', `_${rpid}`);
        params.append('note_pwd', oid);

        GM_xmlhttpRequest({
            method: 'GET',
            url: `${getNoteApi}?${params}`,
            onload: async (res) => {
                const jObject = JSON.parse(res.responseText);
                if (jObject.status == 1) {
                    const note_content = CryptoJS.AES.decrypt(jObject.data.note_content, oid);
                    const escaped = note_content.toString(CryptoJS.enc.Utf8).replaceAll(/\\n/g, '\n');
                    GM_setClipboard(escaped, 'text');
                    Swal.fire({
                        title: '内容已复制',
                        text: escaped,
                        icon: 'success'
                    });
                }
                else if (jObject.status == 4) {
                    const { value: password } = await Swal.fire({
                        title: '输入密码',
                        input: 'text',
                        inputAttributes: {
                            maxlength: '32',
                            autocapitalize: 'off',
                            autocorrect: 'off',
                            autocomplete: 'off'
                        }
                    });
                    if (password) {
                        params.set('note_pwd', password);
                        GM_xmlhttpRequest({
                            method: 'GET',
                            url: `${getNoteApi}?${params}`,
                            onload: (res) => {
                                const jObject = JSON.parse(res.responseText);
                                if (jObject.status == 1) {
                                    const note_content = CryptoJS.AES.decrypt(jObject.data.note_content, password);
                                    const escaped = note_content.toString(CryptoJS.enc.Utf8).replaceAll(/\\n/g, '\n');
                                    GM_setClipboard(escaped, 'text');
                                    Swal.fire({
                                        title: '内容已复制',
                                        text: escaped,
                                        icon: 'success'
                                    });
                                }
                                else {
                                    Swal.fire({
                                        title: '密码错误',
                                        icon: 'error'
                                    });
                                }
                            }
                        });
                    }
                }
            }
        });
    }


    async function handleHate(urlParams) {
        const resObject = { code: 111 };
        const oid = urlParams.get('oid');
        const rpid = urlParams.get('rpid');
        getMessage(oid, rpid);
        return new Response(JSON.stringify(resObject));
    }

    async function handleAdd(urlParams, url, options) {
        const resObject = { code: 111 };
        const oid = urlParams.get('oid');
        const message = urlParams.get('message');
        const matches = message.match(/<\|([^\|]+)\|([^\|]+)\|([^\|]+)?\|>/);

        if (matches == null) {
            return await oFetch(url, options);
        }

        let root = urlParams.get('root');
        if (root == null || root == '0') {
            root = oid;
        }

        const content = matches[1];
        const expire = matches[2].toUpperCase();
        const password = matches[3] ?? oid;

        const params = new URLSearchParams();
        params.append('note_name', `_${root}`);
        params.append('note_content', CryptoJS.AES.encrypt(content, password).toString());
        if (expireTime.has(expire)) {
            params.append('expire_time', expireTime.get(expire));
        }
        else {
            Swal.fire({
                title: '不正确的有效时间',
                text: `应在[${expireTime.keys().toArray().toString()}]中选择`,
                icon: 'error'
            });
            resObject.message = '无法发送评论';
            return new Response(JSON.stringify(resObject));
        }
        params.append('note_pwd', password);

        GM_xmlhttpRequest({
            method: 'POST',
            url: saveNoteApi,
            data: params.toString(),
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded'
            },
            onload: (res) => {
                const jObject = JSON.parse(res.responseText);
                if (jObject.status == 1) {
                    Swal.fire({
                        title: '成功发送消息',
                        icon: 'success'
                    });
                }
                else {
                    Swal.fire({
                        title: '消息发送失败',
                        icon: 'error'
                    });
                }
            }
        });

        resObject.message = '已尝试发出消息';
        return new Response(JSON.stringify(resObject));
    }

    async function handleReply(urlParams, url, options) {
        const replyWhiteList = GM_getValue('replyWhiteList', []);
        const useReply = GM_getValue('useReply', 0);
        if (useReply == 0 && replyWhiteList.length == 0) {
            return await oFetch(url, options);
        }

        const oid = urlParams.get('oid');
        const root = urlParams.get('root');
        const params = new URLSearchParams();
        params.append('note_name', `_${root}`);
        params.append('note_pwd', oid);
        return new Promise((resolve, reject) => {
            oFetch(url, options)
                .then(response => response.json())
                .then(data => {
                    if (useReply == 0 && !replyWhiteList.includes(data.data.root.mid)) {
                        resolve(new Response(JSON.stringify(data)));
                        return;
                    }
                    GM_xmlhttpRequest({
                        method: 'GET',
                        url: `${getNoteApi}?${params}`,
                        onload: async (res) => {
                            const jObject = JSON.parse(res.responseText);
                            const moke = {
                                mid: 0,
                                count: 0,
                                rcount: 0,
                                state: 0,
                                ctime: 0,
                                like: 0,
                                replies: null,
                                content: {
                                    message: '<无信息>',
                                    jump_url: {},
                                    max_line: 32,
                                    members: []
                                },
                                member: {
                                    mid: 0,
                                    uname: '匿名',
                                    sex: '保密',
                                    avatar: 'https://i1.hdslb.com/bfs/face/member/noface.jpg',
                                    sign: '',
                                    level_info: {
                                        current_level: 0,
                                        current_min: 0,
                                        current_exp: 0,
                                        next_exp: 0
                                    }
                                }
                            };

                            if (jObject.status == 1) {
                                moke.ctime = Math.floor(Date.parse(jObject.data.updated_time) / 1000);
                                const note_content = CryptoJS.AES.decrypt(jObject.data.note_content, oid);
                                const escaped = note_content.toString(CryptoJS.enc.Utf8).replaceAll(/\\n/g, '\n');
                                moke.content.message = escaped;
                                moke.content.emote = {};
                                for (let eitem of (escaped.match(/\[[^\[\]]+\]/g) ?? [])) {
                                    const emoteInfo = DATA.data.packages[0].emote.find(item => item.text == eitem);
                                    if (emoteInfo) {
                                        moke.content.emote[emoteInfo.text] = emoteInfo;
                                    }
                                }
                                data.data.replies.unshift(moke);
                            }
                            resolve(new Response(JSON.stringify(data)));
                        }
                    });
                });
        });
    }

    async function handleLike(urlParams) {
        const like = urlParams.get('like');
        if (like != 2) {
            return false;
        }
        const aid = urlParams.get('aid');
        getMessage(aid, aid);
        return true;
    }

    async function mFetch(url, options) {
        const params = url.substring(url.indexOf('?') + 1);
        if (url.startsWith(hateApi) || url.startsWith(`https:${hateApi}`)) {
            return await handleHate(new URLSearchParams(options.body ?? params));
        }
        else if (url.startsWith(addApi) || url.startsWith(`https:${addApi}`)) {
            return await handleAdd(new URLSearchParams(options.body ?? params), url, options);
        }
        else if (url.startsWith(replyApi) || url.startsWith(`https:${replyApi}`)) {
            return await handleReply(new URLSearchParams(options.body ?? params), url, options);
        }
        /*
        else if (url.startsWith(likeApi) || url.startsWith(`https:${likeApi}`)) {
            return await handleLike(new URLSearchParams(options.body ?? params), url, options);
        }
        */
        return await oFetch(url, options);
    }

    window.unsafeWindow.fetch = mFetch;

    XMLHttpRequest.prototype.send = function (args) {
        const url = this._url;
        const params = new URLSearchParams(args);
        if (url) {
            if (url.startsWith(likeApi) || url.startsWith(`https:${likeApi}`)) {
                if (handleLike(params)) {
                    arguments[0] = arguments[0]?.replace('like=2', 'like=1');
                }
            }
        }
        else if (params.has('aid') && params.has('like')) {
            if (handleLike(params)) {
                arguments[0] = arguments[0]?.replace('like=2', 'like=1');
            }
        }
        originalSend.apply(this, arguments);
    };

    const DATA = JSON.parse(GM_getResourceText('DATA'));

    GM_registerMenuCommand('设置查看回复获取', async function () {
        const useReply = GM_getValue('useReply', 0);
        const { value: accept } = await Swal.fire({
            title: '设置查看回复获取',
            input: 'checkbox',
            inputValue: useReply,
            inputPlaceholder: '每次查看回复都获取信息',
            confirmButtonText: '确认',
        });
        GM_setValue('useReply', accept ?? 0);
    });

    GM_registerMenuCommand('设置查看回复白名单', async function () {
        const replyWhiteList = GM_getValue('replyWhiteList', []);
        const { value: text } = await Swal.fire({
            input: 'textarea',
            inputLabel: '白名单列表',
            inputPlaceholder: '输入包含UP主UID的列表,此列表内的评论始终会尝试获取',
            inputValue: JSON.stringify(replyWhiteList),
            showCancelButton: true
        });
        if (text) {
            GM_setValue('replyWhiteList', JSON.parse(text));
        }
    });

})();