Greasy Fork

Greasy Fork is available in English.

哔哩发评反诈-改

评论发送后自动检测状态,避免被发送成功的谎言所欺骗!

当前为 2025-05-27 提交的版本,查看 最新版本

// ==UserScript==
// @name         哔哩发评反诈-改
// @namespace    http://tampermonkey.net/
// @version      3.5
// @description  评论发送后自动检测状态,避免被发送成功的谎言所欺骗!
// @author       freedom-introvert & ChatGPT & DeepSeek
// @match        https://*.bilibili.com/*
// @run-at       document-idle
// @grant        GM.xmlHttpRequest
// @license      GPL
// ==/UserScript==

const waitTime = 5000;//评论发送后的等待时间,单位毫秒,可修改此项,不建议低于5秒

const sortByTime = 0;
const SORT_MODE_TIME = 2;

const originalFetch = unsafeWindow.fetch;//注意是unsafeWindow,不是window,使用 GM.xmlHttpRequest 换掉window里的fecth将不起作用

// Replace the fetch function with a custom one
unsafeWindow.fetch = async function (...args) {
    // Call the original fetch function and wait for the response
    var response = await originalFetch.apply(this, args);

    // Clone the response to read its content without altering the original response
    var clonedResponse = response.clone();

    // Read the response content as text
    clonedResponse.text().then(content => {
        // Log the URL of the fetch request to the console
        var url = args[0];
        //console.log('Fetch request URL:', url);
        // Log the response content to the console
        //console.log('Fetch response content:', content);
        if (url.startsWith("//api.bilibili.com/x/v2/reply/add")) {
            handleAddCommentResponse(url, JSON.parse(content));
        }
    });

    // Return the original response so that the fetch call continues to work as normal
    return response;
};

//动态shadowBan检测
window.onload = function () {
    const currentURL = window.location.href;
    const hostname = window.location.hostname;
    let id = null;

    if (hostname === 't.bilibili.com') {
        // 提取 t.bilibili.com URL 中的数字部分
        const urlPath = window.location.pathname;
        id = urlPath.split('/')[1];
    } else if (hostname === 'www.bilibili.com') {
        // 提取 www.bilibili.com/opus URL 中的数字部分
        const urlPath = window.location.pathname;
        const pathParts = urlPath.split('/');
        if (pathParts[1] === 'opus') {
            id = pathParts[2];
        }
    }

    if (id) {
        console.log('Dynamic ID:', id);
        handleCheckDynamic(id);
    }

}

console.log(window.fetch)
console.log("反诈脚本已加载")

//
var dialogHTML = `
        <style>
        #notification-container {
            position: fixed;
            bottom: 20px;
            right: 20px;
            z-index: 1000;
            max-width: 350px;
        }

        .notification {
            display: none;
            position: relative;
            padding: 15px;
            margin-bottom: 10px;
            background-color: #fff;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
            border-radius: 4px;
            animation: slideIn 0.3s ease-out;
            border-left: 4px solid #FB7299;
        }

        @keyframes slideIn {
            from {
                transform: translateX(100%);
                opacity: 0;
            }
            to {
                transform: translateX(0);
                opacity: 1;
            }
        }

        .notification-title {
            font-size: 16px;
            font-weight: bold;
            margin-bottom: 8px;
            color: #212121;
        }

        .notification-message {
            font-size: 14px;
            color: #616161;
            margin-bottom: 10px;
            white-space: pre-line;
        }

        .notification-progress {
            width: 100%;
            height: 4px;
            background-color: #e0e0e0;
            border-radius: 2px;
            margin: 10px 0;
            overflow: hidden;
        }

        .notification-progress-bar {
            height: 100%;
            background-color: #FB7299;
            width: 0;
            transition: width 0.1s linear;
        }

        .notification-progress-indeterminate {
            position: relative;
            overflow: hidden;
            background-color: #FB7299;
        }

        .notification-progress-indeterminate::before {
            content: '';
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background-color: inherit;
            animation: indeterminate 2s linear infinite;
        }

        @keyframes indeterminate {
            0% {
                transform: translateX(-100%);
            }
            100% {
                transform: translateX(100%);
            }
        }

        .notification-close {
            position: absolute;
            top: 10px;
            right: 10px;
            background: none;
            border: none;
            font-size: 16px;
            color: #9e9e9e;
            cursor: pointer;
            padding: 0;
        }

        .notification-close:hover {
            color: #616161;
        }

        .shadowban-scanner-message {
            --message-background-color: rgb(255, 0, 0, 0.2);
            color: var(--md-sys-color-on-primary);
            padding: 1em;
            border-radius: 0.5em;
            background: var(--message-background-color);
            margin: 1em 0px 0px;
        }

        </style>
        <div id="notification-container"></div>
        `
document.body.insertAdjacentHTML('beforeend', dialogHTML);

const Notification = {
    show: function (title, message, options = {}) {
        const container = document.getElementById('notification-container');
        const notification = document.createElement('div');
        notification.className = 'notification';

        const closeButton = document.createElement('button');
        closeButton.className = 'notification-close';
        closeButton.innerHTML = '&times;';
        closeButton.addEventListener('click', () => {
            notification.style.animation = 'slideIn 0.3s ease-out reverse';
            setTimeout(() => {
                notification.remove();
            }, 300);
        });

        const titleElement = document.createElement('div');
        titleElement.className = 'notification-title';
        titleElement.textContent = title;

        const messageElement = document.createElement('div');
        messageElement.className = 'notification-message';
        messageElement.textContent = message;

        const progressContainer = document.createElement('div');
        progressContainer.className = 'notification-progress';
        const progressBar = document.createElement('div');
        progressBar.className = 'notification-progress-bar';
        progressContainer.appendChild(progressBar);

        notification.appendChild(closeButton);
        notification.appendChild(titleElement);
        notification.appendChild(messageElement);

        if (options.progress !== undefined || options.indeterminate) {
            notification.appendChild(progressContainer);
        }

        if (options.indeterminate) {
            progressBar.className = 'notification-progress-indeterminate';
        } else if (options.progress !== undefined) {
            progressBar.style.width = `${options.progress}%`;
        }

        container.appendChild(notification);
        setTimeout(() => {
            notification.style.display = 'block';
        }, 10);

        return {
            update: function(newTitle, newMessage, newOptions) {
                if (newTitle) titleElement.textContent = newTitle;
                if (newMessage) messageElement.textContent = newMessage;

                if (newOptions) {
                    if (newOptions.progress !== undefined) {
                        progressBar.style.width = `${newOptions.progress}%`;
                        progressBar.className = 'notification-progress-bar';
                    }
                    if (newOptions.indeterminate) {
                        progressBar.className = 'notification-progress-indeterminate';
                    }
                }
            },
            close: function() {
                closeButton.click();
            }
        };
    }
};

function sleep(time) {
    return new Promise((resolve) => setTimeout(resolve, time));
}

async function handleAddCommentResponse(url, responseJson) {
    console.log(url);
    console.log(responseJson);
    console.log(responseJson.code);
    if (responseJson.code == 0) {
        var data = responseJson.data;
        var reply = data.reply;

        var oid = reply.oid;
        var type = reply.type;
        var rpid = reply.rpid;
        var root = reply.root;

        console.log(`${data.success_toast},准备检查评论`);
        let notification = Notification.show("检查评论状态", "等待检查中...", {progress: 0});
        await sleepAndShowInNotification(waitTime, notification);
        notification.update("检查中...", "正在检查评论状态", {indeterminate: true});

        //如果root==0,这是在评论区的根评论,否则是一个对某评论的回复评论
        if (root == 0) {
            notification.update("检查中...", "查找无账号评论区时间排序第一页");
            var resp = await getMainCommentList(oid, type,0,SORT_MODE_TIME,false);
            if(resp.code != 0){
                showErrorResult("获取评论主列表时发生错误,响应数据:" + resp, notification);
                return;
            }
            console.log(resp);
            var replies = resp.data.replies;
            var found = findReplies(replies, rpid);
            if (found) {
                showOkResult(reply, notification);
            } else {
                //有账号获取评论回复页
                notification.update("检查中...", "有账号获取此评论的回复列表");
                resp = await fetchBilibiliCommentReplies(oid, type, rpid, 0, sortByTime, true);
                //"已经被删除了"状态码
                if (resp.code == 12022) {
                    //自己都显示被删除了那就真删除了(ps,按照流程图还要多个cookie检查,但是浏览器环境没这问题)
                    showQuickDeleteResult(reply, notification)
                } else if (resp.code == 0) {
                    //继续无账号获取来检查,看看是否是可疑的?
                    notification.update("检查中...", "无账号获取此评论的回复列表");
                    resp = await fetchBilibiliCommentReplies(oid, type, rpid, 0, sortByTime, false);
                    if (resp.code == 12022) {
                        showShadowBanResult(reply, notification);
                    } else if (resp.code == 0) {
                        showSusResult(reply, notification);
                    } else {
                        console.log(resp);
                        showErrorResult("获取评论回复列表时发生错误,响应数据:" + resp, notification);
                    }
                } else {
                    console.log(resp);
                    showErrorResult("有账号获取评论回复列表时发生错误,响应数据:" + resp, notification);
                }
            }
        } else {
            notification.update("检查中...", "无账号查找评论回复页……");

            for (i = 0; true; i++) {
                notification.update("检查中...", `无账号查找评论回复第${i}页……`);
                var resp = await fetchBilibiliCommentReplies(oid, type, root, i, sortByTime, false);
                var replies = resp.data.replies;
                console.log(resp);
                if (replies === null || replies.length === 0) {
                    console.log("已翻遍无账号下的评论回复页");
                    break;
                }

                if (findReplies(replies, rpid)) {
                    showOkResult(reply, notification);
                    return;
                }
            }

            for (i = 0; true; i++) {
                notification.update("检查中...", `有账号查找评论回复第${i}页……`);
                var resp = await fetchBilibiliCommentReplies(oid, type, root, i, sortByTime, true);
                var replies = resp.data.replies;
                console.log(resp);
                if (replies === null || replies.length === 0) {
                    console.log("已翻遍有账号下的评论回复页");
                    break;
                }

                if (findReplies(replies, rpid)) {
                    showShadowBanResult(reply, notification);
                    return;
                }
            }

            //若两个条件都没找到评论则是秒删
            showQuickDeleteResult(reply, notification);
        }
    }
}

async function handleCheckDynamic(id) {
    var resp = await fetchDynamic(id, false);
    console.log(resp);
    if (resp.code == -352) {
        addDynamicShadowBannedHint("检测到此动态被shadowBan,仅自己可见!(也可能是误判了,你可以在无痕模式去验证一下)");
    } else if (resp.code == 4101131) {
        console.log("检测到动态被shadowBan!");
        addDynamicShadowBannedHint("检测到此动态被shadowBan,仅自己可见!(可能你转发到动态的评论被ShadowBan)");
    } else if (resp.code == 500) {
        console.log("检测到动态被shadowBan!");
        addDynamicShadowBannedHint("检测到此动态被shadowBan,仅自己可见!(可能你转发到动态的评论疑似审核中)");
    } else if (resp.code == 0) {
        console.log("检查到此动态正常,没被shadowBan");
    } else {
        console.log("动态检查出错:未知的响应码", resp);
    }
}

function findReplies(replies, rpid) {
    for (var i in replies) {
        var reply = replies[i];
        console.log(reply);
        if (reply.rpid == rpid) {
            return reply;
        }
    }
    return null;
}

function findReplyInReplies(replies, rpid) {
    for (var i in replies) {
        var reply = replies[i];
        console.log(reply);
        var subReplies = reply.replies;
        console.log(subReplies)
        for (var j in subReplies) {
            var subReply = subReplies[j];
            console.log(subReply);
            if (subReply.rpid == rpid) {
                return subReply;
            }
        }
    }
    return null;
}

async function sleepAndShowInNotification(sleepTime, notification) {
    let sleepCount = sleepTime / 100;
    for (var i = 0; i <= sleepCount; i++) {
        await sleep(100);
        notification.update("等待检查中", `等待 ${i * 100}/${sleepTime}ms 后检查评论`, {progress: 100 / sleepCount * i});
    }
    notification.update("等待检查中", `等待 ${sleepTime}/${sleepTime}ms 后检查评论`, {progress: 100});
}

/**
 *
 * @param {*} oid
 * @param {*} type
 * @param {*} next
 * @param {*} mode 评论排序模式 2为按时间
 * @param {*} isLogin 是否携带cookie
 * @param {*} seek_rpid 定位rpid
 * @returns
 */

async function getMainCommentList(oid, type, next, mode,isLogin,seek_rpid) {
    let url = `https://api.bilibili.com/x/v2/reply/main?oid=${oid}&type=${type}&next=${next}&mode=${mode}` + (seek_rpid ? `&seek_rpid=${seek_rpid}` : "")
    const req = {
        url,
        anonymous: !isLogin
    }

    req.headers={ "cookie": getBuvid3Cookie() };
    let response = (await GM.xmlHttpRequest(req).catch(e => console.error(e))).response;
    let resp = JSON.parse(response);
    console.log("获取主评论列表,携带cookie:"+isLogin,url,resp)
    return resp;
}

/**
 * 获取某评论的回复列表
 * @param {*} oid
 * @param {*} type
 * @param {*} root
 * @param {*} pn
 * @param {*} sort
 * @param {*} hasCookie
 * @returns
 */
async function fetchBilibiliCommentReplies(oid, type, root, pn, sort, hasCookie) {
    const url = new URL('https://api.bilibili.com/x/v2/reply/reply');
    const params = { oid, type, root, pn, sort };
    url.search = new URLSearchParams(params).toString();

    try {
        const response = await originalFetch(url, hasCookie ? { credentials: 'include' } : { credentials: 'omit' });
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        return await response.json(); // Return JSON object
    } catch (error) {
        throw error; // Rethrow the error
    }
}

async function fetchDynamic(id, hasCookie) {
    const url = new URL('https://api.bilibili.com/x/polymer/web-dynamic/v1/detail');
    const params = { id };
    url.search = new URLSearchParams(params).toString();

    try {
        const response = await originalFetch(url, hasCookie ? { credentials: 'include' } : { credentials: 'omit' });
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        return await response.json(); // Return JSON object
    } catch (error) {
        throw error; // Rethrow the error
    }
}


function showOkResult(reply, notification) {
    notification.update("检查完成", "恭喜,无账号状态下找到了你的评论,你的评论正常!\n\n你的评论:" + reply.content.message, {progress: 100});
    setTimeout(() => notification.close(), 5000);
}

function showShadowBanResult(reply, notification) {
    notification.update("检查完成", "你被骗了,此评论被shadow ban(仅自己可见)!\n\n你的评论:" + reply.content.message, {progress: 100});
    setTimeout(() => notification.close(), 5000);
}

function showQuickDeleteResult(reply, notification) {
    notification.update("检查完成", "你评论没了,此评论已被系统秒删!刷新评论区也许就不见了,复制留个档吧。\n\n你的评论:" + reply.content.message, {progress: 100});
    setTimeout(() => notification.close(), 5000);
}

function showSusResult(reply, notification) {
    notification.update("检查完成", `
                你评论状态有点可疑,虽然无账号翻找评论区获取不到你的评论,但是无账号可通过
                https://api.bilibili.com/x/v2/reply/reply?oid=${reply.oid}&pn=1&ps=20&root=${reply.rpid}&type=${reply.type}&sort=0
                获取你的评论,疑似评论区被戒严或者这是你的视频。

                你的评论:${reply.content.message}
            `, {progress: 100});
    setTimeout(() => notification.close(), 5000);
}

function showErrorResult(message, notification) {
    notification.update("发生错误", message, {progress: 0});
    setTimeout(() => notification.close(), 5000);
}

//样式抄自X(Twitter)的shadowBan检查器,插件可在Chrome商店搜索
function addDynamicShadowBannedHint(message) {
    const biliDynContent = document.querySelector('.bili-dyn-content');

    if (biliDynContent) {
        const shadowbanMessage = document.createElement('div');
        shadowbanMessage.className = 'shadowban-scanner-message';
        shadowbanMessage.style.setProperty('--md-sys-color-on-primary', 'rgb(15, 20, 25)');

        const messageSpan = document.createElement('span');
        messageSpan.textContent = message;

        shadowbanMessage.appendChild(messageSpan);
        biliDynContent.appendChild(shadowbanMessage);
    }
}

function getBuvid3Cookie() {
    var cookies = document.cookie.split(';');
    for (var i = 0; i < cookies.length; i++) {
        var cookie = cookies[i].trim();
        if (cookie.startsWith('buvid3=')) {
            return cookie;
        }
    }
    return null;
}