// ==UserScript==
// @name 哔哩发评反诈
// @namespace http://tampermonkey.net/
// @version 3.0
// @description 评论发送后自动检测状态,避免被发送成功的谎言所欺骗!
// @author freedom-introvert & ChatGPT
// @match https://*.bilibili.com/*
// @run-at document-idle
// @grant none
// @license GPL
// ==/UserScript==
const waitTime = 5000;//评论发送后的等待时间,单位毫秒,可修改此项,不建议低于5秒
const sortByTime = 0;
const sortModeByTime = 2;
const originalFetch = window.fetch;
// Replace the fetch function with a custom one
window.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>
#progress-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 999;
}
#progress-title{
display: block;
font-size: 19px;
margin-block-start: 0.5em;
margin-block-end: 1em;
margin-inline-start: 0px;
margin-inline-end: 0px;
font-weight: bold;
unicode-bidi: isolate;
}
#progress-message{
display: block;
font-size: 16px;
margin-block-start: 1em;
margin-block-end: 1em;
margin-inline-start: 0px;
margin-inline-end: 0px;
unicode-bidi: isolate;
}
#progress-dialog {
display: none;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 60%;
padding: 20px;
background-color: #fff;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
border-radius: 3px;
z-index: 1000;
}
#progress-bar-container {
width: 100%;
height: 5px;
margin-top: 30px;
background-color: #ddd;
overflow: hidden;
position: relative;
margin-bottom: 20px;
}
.progress-bar {
width: 0;
height: 20px;
background-color: #FB7299;
text-align: center;
color: white;
line-height: 20px;
}
/* 不确定进度的线性进度条 摘抄自mdui*/
.progress-bar-indeterminate {
background-color: #FB7299;
&::before {
position: absolute;
top: 0;
bottom: 0;
left: 0;
background-color: inherit;
animation: mdui-progress-indeterminate 2s linear infinite;
content: ' ';
will-change: left, width;
}
&::after {
position: absolute;
top: 0;
bottom: 0;
left: 0;
background-color: inherit;
animation: mdui-progress-indeterminate-short 2s linear infinite;
content: ' ';
will-change: left, width;
}
}
@keyframes mdui-progress-indeterminate {
0% {
left: 0;
width: 0;
}
50% {
left: 30%;
width: 70%;
}
75% {
left: 100%;
width: 0;
}
}
@keyframes mdui-progress-indeterminate-short {
0% {
left: 0;
width: 0;
}
50% {
left: 0;
width: 0;
}
75% {
left: 0;
width: 25%;
}
100% {
left: 100%;
width: 0;
}
}
#close-button {
display: inline-block;
margin-top: 20px;
padding: 10px 15px;
background-color: #fff;
color: #FB7299;
border: none;
border-radius: 4px;
cursor: pointer;
text-align: center;
font-size: 14px;
font-weight: bold;
float: right;
transition: all 0.2s;
}
#close-button:hover {
background-color: #F0F0F0;
}
.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="progress-overlay"></div>
<div id="progress-dialog">
<h3 id="progress-title">Progress</h3>
<p id="progress-message"></p>
<div id="progress-bar-container">
<div id="progressBar" class="progress-bar"></div>
</div>
<button id="close-button">关闭</button>
</div>
`
document.body.insertAdjacentHTML('beforeend', dialogHTML);
const ProgressDialog = {
show: function () {
document.getElementById('progress-overlay').style.display = 'block';
document.getElementById('progress-dialog').style.display = 'block';
},
hide: function () {
document.getElementById('progress-overlay').style.display = 'none';
document.getElementById('progress-dialog').style.display = 'none';
},
setTitle: function (title) {
document.getElementById('progress-title').textContent = title;
},
setMessage: function (message) {
document.getElementById('progress-message').innerText = message;
},
setProgress: function (progress) {
const progressBar = document.getElementById('progressBar');
progressBar.style.width = progress + '%';
},
setIndeterminate: function (indeterminate) {
var progressBar = document.getElementById('progressBar');
if (indeterminate) {
progressBar.className = "progress-bar-indeterminate";
progressBar.style.width = "30%"
} else {
progressBar.className = "progress-bar";
progressBar.style.width = "0"
}
}
};
document.getElementById('close-button').addEventListener('click', function () {
ProgressDialog.hide();
});
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},准备检查评论`);
ProgressDialog.show();
await sleepAndShowInDialog(waitTime);
ProgressDialog.setIndeterminate(true);
ProgressDialog.setTitle("检查中……");
//如果root==0,这是在评论区的根评论,否则是一个对某评论的回复评论
if (root == 0) {
ProgressDialog.setMessage("查找无账号评论区时间排序第一页");
var resp = await fetchBilibiliComments(oid, type, 1, sortByTime, false);
console.log(resp);
var replies = resp.data.replies;
var found = findReplies(replies, rpid);
if (found) {
showOkResult(reply);
} else {
//有账号获取评论回复页
ProgressDialog.setMessage("有账号获取此评论的回复列表");
resp = await fetchBilibiliCommentReplies(oid, type, rpid, 0, sortByTime, true);
//“已经被删除了”状态码
if (resp.code == 12022) {
//自己都显示被删除了那就真删除了(ps,按照流程图还要多个cookie检查,但是浏览器环境没这问题)
showQuickDeleteResult(reply)
} else if (resp.code == 0) {
//继续无账号获取来检查,看看是否是可疑的?
ProgressDialog.setMessage("无账号获取此评论的回复列表");
resp = await fetchBilibiliCommentReplies(oid, type, rpid, 0, sortByTime, false);
if (resp.code == 12022) {
showShadowBanResult(reply);
} else if (resp.code == 0) {
showSusResult(reply);
} else {
console.log(resp);
showErrorResult("获取评论回复列表时发生错误,响应数据:" + resp);
}
} else {
console.log(resp);
showErrorResult("有账号获取评论回复列表时发生错误,响应数据:" + resp);
}
}
} else {
ProgressDialog.setMessage("无账号查找评论回复页……");
for(i = 0;true;i++){
ProgressDialog.setMessage(`无账号查找评论回复第${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);
return;
}
}
for(i = 0;true;i++){
ProgressDialog.setMessage(`有账号查找评论回复第${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);
return;
}
}
//若两个条件都没找到评论则是秒删
showQuickDeleteResult(reply);
}
}
}
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 sleepAndShowInDialog(sleepTime) {
ProgressDialog.setTitle("等待检查中");
var sleepCount = sleepTime / 10;
for (var i = 0; i <= sleepCount; i++) {
await sleep(10);
ProgressDialog.setMessage(`等待 ${i * 10}/${sleepTime}ms 后检查评论`)
ProgressDialog.setProgress(100 / sleepCount * i);
}
ProgressDialog.setProgress(100);
}
/**
* 获取评论区的评论
* @param {*} oid
* @param {*} type
* @param {*} pn
* @param {*} sort
* @param {*} hasCookie
* @returns
*/
async function fetchBilibiliComments(oid, type, pn, sort, hasCookie) {
const url = new URL('https://api.bilibili.com/x/v2/reply');
const params = { oid, type, 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
}
}
/**
* 获取某评论的回复列表
* @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
}
}
/**
* 由于需要残次cookie,浏览器js无法自定义cookie,此方法废弃,需要翻全页
*
* 使用Main api 结合 seek_rpid 参数定位评论
* 如果seek_rpid 的评论id是一个回复别人的评论,
* 那么它会出现在某个根评论的预览评论列表里
* @param {*} oid
* @param {*} type
* @param {*} seek_rpid 要查看的rpid
* @param {*} next 页码(从零开始)
* @param {*} mode 排序模式
* @param {*} hasCookie
* @returns
*/
async function fetchBilibiliCommentsByMainApiUseSeekRpid(oid, type, seek_rpid, next, mode, hasCookie) {
const url = new URL('https://api.bilibili.com/x/v2/reply/main');
const params = { oid, type, seek_rpid, next, mode };
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) {
showResult("恭喜,无账号状态下找到了你的评论,你的评论正常!\n\n你的评论:" + reply.content.message);
}
function showShadowBanResult(reply) {
showResult("你被骗了,此评论被shadow ban(仅自己可见)!\n\n你的评论:" + reply.content.message);
}
function showQuickDeleteResult(reply) {
showResult("你评论没了,此评论已被系统秒删!刷新评论区也许就不见了,复制留个档吧。\n\n你的评论:" + reply.content.message);
}
function showSusResult(reply) {
showResult(`
你评论状态有点可疑,虽然无账号翻找评论区获取不到你的评论,但是无账号可通过
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}
`);
}
function showResult(message) {
ProgressDialog.setIndeterminate(false);
ProgressDialog.setProgress(100);
ProgressDialog.setTitle("检查完毕");
ProgressDialog.setMessage(message);
}
function showErrorResult(message) {
ProgressDialog.setIndeterminate(false);
ProgressDialog.setProgress(0);
ProgressDialog.setTitle("发生错误");
ProgressDialog.setMessage(message);
}
//样式抄自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);
}
}