Greasy Fork

来自缓存

Greasy Fork is available in English.

贴吧贴子屏蔽检测

贴吧都快凉了,过去的痕迹都没了,你为什么还在刷贴吧呢?你们建个群不好吗?

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name        贴吧贴子屏蔽检测
// @version     1.1.2
// @description 贴吧都快凉了,过去的痕迹都没了,你为什么还在刷贴吧呢?你们建个群不好吗?
// @match       *://tieba.baidu.com/*
// @include     *://tieba.baidu.com/*
// @grant       none
// @author      864907600cc
// @icon        https://secure.gravatar.com/avatar/147834caf9ccb0a66b2505c753747867
// @namespace   http://ext.ccloli.com
// @license     GPL-3.0
// ==/UserScript==


'use strict';

let threadCache = {};
let replyCache = {};

/**
 * 精简封装 fetch 请求,自带请求 + 通用配置 + 自动 .text()
 *
 * @param {string} url - 请求 URL
 * @param {object} [options={}] - fetch Request 配置
 * @returns {Promise<string>} fetch 请求
 */
const request = (url, options = {}) => fetch(url, Object.assign({
	credentials: 'omit',
	// 部分贴吧(如 firefox 吧)会强制跳转回 http
	redirect: 'follow',
	// 阻止浏览器发出 CORS 检测的 HEAD 请求头
	mode: 'same-origin',
	headers: {
		'X-Requested-With': 'XMLHttpRequest'
	}
}, options)).then(res => res.text());

/**
 * 延迟执行
 *
 * @param {number} time - 延迟毫秒数
 */
const sleep = time => new Promise(resolve => setTimeout(resolve, time));

/**
 * 获取当前用户是否登录
 * 
 * @returns {number|boolean} 是否登录,若已登录,贴吧页为 1,贴子页为 true
 */
const getIsLogin = () => window.PageData.user.is_login;

/**
 * 获取当前用户的用户名
 *
 * @returns {string} 用户名
 */
const getUsername = () => window.PageData.user.name || window.PageData.user.user_name;

/**
 * 获取当前用户的 portrait(适用于无用户名)
 *
 * @returns {string} portrait
 */
const getPortrait = () => window.PageData.user.portrait.split('?').shift();

/**
 * 获取 \u 形式的 unicode 字符串
 *
 * @param {string} str - 需要转码的字符串
 * @returns {string} 转码后的字符串
 */
const getEscapeString = str => escape(str).replace(/%/g, '\\').toLowerCase();

/**
 * 获取主题贴的移动端地址
 *
 * @param {number} tid - 贴子 id
 * @returns {string} URL
 */
const getThreadMoUrl = tid => `//tieba.baidu.com/mo/q-----1-1-0----/m?kz=${tid}`;

/**
 * 获取回复贴的移动端地址
 *
 * @param {number} tid - 贴子 id
 * @param {number} pid - 回复 id
 * @param {number} [pn=0] - 页码
 * @returns {string} URL
 */
const getReplyMoUrl = (tid, pid, pn = 0) => `//tieba.baidu.com/mo/q-----1-1-0----/flr?pid=${pid}&kz=${tid}&pn=${pn}`;

/**
 * 获取回复贴的 ajax 地址
 *
 * @param {number} tid - 贴子 id
 * @param {number} pid - 主回复 id
 * @param {number} spid - 楼中楼回复 id
 * @param {number} [pn=0] - 页码
 * @returns {string} URL
 */
const getReplyUrl = (tid, pid, pn = 0) => `//tieba.baidu.com/p/comment?tid=${tid}&pid=${pid}&pn=${pn}&t=${Date.now()}`;

/**
 * 从页面内容判断贴子是否直接消失
 *
 * @param {string} res - 页面内容
 * @returns {boolean} 是否被屏蔽
 */
const threadIsNotExist = res => res.indexOf('您要浏览的贴子不存在') >= 0 || res.indexOf('(共0贴)') >= 0;

/**
 * 获取主题贴是否被屏蔽
 *
 * @param {number} tid - 贴子 id
 * @returns {Promise<boolean>} 是否被屏蔽
 */
const getThreadBlocked = tid => request(getThreadMoUrl(tid))
	.then(threadIsNotExist);

/**
 * 获取回复贴是否被屏蔽
 *
 * @param {number} tid - 贴子 id
 * @param {number} pid - 回复 id
 * @returns {Promise<boolean>} 是否被屏蔽
 */
const getReplyBlocked = (tid, pid) => request(getReplyMoUrl(tid, pid))
	.then(res => threadIsNotExist(res) || res.indexOf('刷新</a><div>楼.&#160;<br/>') >= 0);

/**
 * 获取楼中楼是否被屏蔽
 *
 * @param {number} tid - 贴子 id
 * @param {number} pid - 主回复 id
 * @param {number} spid - 楼中楼回复 id
 * @returns {Promise<boolean>} 是否被屏蔽
 */
const getLzlBlocked = (tid, pid, spid) => request(getReplyUrl(tid, pid))
	// 楼中楼 ajax 翻页后被屏蔽的楼中楼不会展示,所以不需要考虑 pn,同理不需要考虑不在第一页的楼中楼
	.then(res => threadIsNotExist(res) || res.indexOf(`<a rel="noopener" name="${spid}">`) < 0);

/**
 * 获取触发 CSS 样式
 *
 * @param {string} username - 用户名
 * @returns {string} 样式表
 */
const getTriggerStyle = ({ username, portrait }) => {
	const escapedUsername = getEscapeString(username).replace(/\\/g, '\\\\');

	return `
		/* 使用 animation 监测 DOM 变化 */
		@-webkit-keyframes __tieba_blocked_detect__ {}
		@-moz-keyframes __tieba_blocked_detect__ {}
		@keyframes __tieba_blocked_detect__ {}

		/* 主题贴 */
		#thread_list .j_thread_list[data-field*='"author_name":"${escapedUsername}"'],
		#thread_list .j_thread_list[data-field*='"author_portrait":"${portrait}"'],
		/* 回复贴 */
		#j_p_postlist .l_post[data-field*='"user_name":"${escapedUsername}"'],
		#j_p_postlist .l_post[data-field*='"portrait":"${portrait}"'],
		/* 楼中楼 */
		.j_lzl_m_w .lzl_single_post[data-field*="'user_name':'${username}'"],
		.j_lzl_m_w .lzl_single_post[data-field*="'portrait':'${portrait}'"] {
			-webkit-animation: __tieba_blocked_detect__;
			-moz-animation: __tieba_blocked_detect__;
			animation: __tieba_blocked_detect__;
		}

		/* 被屏蔽样式 */
		.__tieba_blocked__,
		.__tieba_blocked__ .d_post_content_main {
			background: rgba(255, 0, 0, 0.05);
			position: relative;
		}
		.__tieba_blocked__.core_title {
			background: #fae2e3;
		}
		.__tieba_blocked__::before {
			background: #f22737;
			position: absolute;
			padding: 5px 10px;
			color: #ffffff;
			font-size: 14px;
			line-height: 1.5em;
			z-index: 399;
		}
		.__tieba_blocked__.lzl_single_post {
			margin-left: -15px;
			margin-right: -15px;
			margin-bottom: -6px;
			padding-left: 15px;
			padding-right: 15px;
			padding-bottom: 6px;
		}

		.__tieba_blocked__.j_thread_list::before,
		.__tieba_blocked__.core_title::before {
			content: '该贴已被屏蔽';
			right: 0;
			top: 0;
		}
		.__tieba_blocked__.l_post::before {
			content: '该楼层已被屏蔽';
			right: 0;
			top: 0;
		}
		.__tieba_blocked__.lzl_single_post::before {
			content: '该楼中楼已被屏蔽';
			left: 0;
			bottom: 0;
		}
	`;
};

/**
 * 检测贴子/回复屏蔽回调函数
 *
 * @param {AnimationEvent} event - 触发的事件对象
 */
const detectBlocked = (event) => {
	if (event.animationName !== '__tieba_blocked_detect__') {
		return;
	}

	const { target } = event;
	const { classList } = target;
	let checker;

	if (classList.contains('j_thread_list')) {
		const tid = target.dataset.tid;
		if (threadCache[tid] !== undefined) {
			checker = threadCache[tid];
		}
		else {
			checker = getThreadBlocked(tid).then(result => {
				threadCache[tid] = result;
				// saveCache('thread');

				return result;
			});
		}
	}
	else if (classList.contains('l_post')) {
		const tid = window.PageData.thread.thread_id;
		const pid = target.dataset.pid || '';
		if (!pid) {
			// 新回复可能没有 pid
			return;
		}

		if (replyCache[pid] !== undefined) {
			checker = replyCache[pid];
		}
		else {
			// 回复时直接取值结果不准确,延迟 5 秒后请求
			checker = sleep(5000).then(() => getReplyBlocked(tid, pid).then(result => {
				replyCache[pid] = result;
				// saveCache('reply');
				try {
					if (result && JSON.parse(target.dataset.field).content.post_no === 1) {
						document.querySelector('.core_title').classList.add('__tieba_blocked__');
					}
				}
				catch (err) {
					// pass through
				}

				return result;
			}));
		}
	}
	else if (classList.contains('lzl_single_post')) {
		const field = target.dataset.field || '';
		const parent = target.parentElement;
		const pageNumber = parent.querySelector('.tP');
		if (pageNumber && pageNumber.textContent.trim() !== '1') {
			// 翻页后的楼中楼不会显示屏蔽的楼中楼,所以命中的楼中楼一定是不会屏蔽的,不需要处理
			return;
		}

		const tid = window.PageData.thread.thread_id;
		const pid = (field.match(/'pid':'?(\d+)'?/) || [])[1];
		const spid = (field.match(/'spid':'?(\d+)'?/) || [])[1];
		if (!spid) {
			// 新回复没有 spid
			return;
		}

		if (replyCache[spid] !== undefined) {
			checker = replyCache[spid];
		}
		else {
			checker = getLzlBlocked(tid, pid, spid).then(result => {
				replyCache[spid] = result;
				// saveCache('reply');

				return result;
			});
		}
	}

	if (checker) {
		Promise.resolve(checker).then(result => {
			if (result) {
				classList.add('__tieba_blocked__');
			}
		});
	}
};

/**
 * 初始化样式
 *
 * @param {object} param - 用户参数
 */
const initStyle = (param) => {
	const style = document.createElement('style');
	style.textContent = getTriggerStyle(param);
	document.head.appendChild(style);
};

/**
 * 初始化事件监听
 *
 */
const initListener = () => {
	document.addEventListener('webkitAnimationStart', detectBlocked, false);
	document.addEventListener('MSAnimationStart', detectBlocked, false);
	document.addEventListener('animationstart', detectBlocked, false);
};

/**
 * 加载并没有什么卵用的缓存
 *
 */
const loadCache = () => {
	const thread = sessionStorage.getItem('tieba-blocked-cache-thread');
	const reply = sessionStorage.getItem('tieba-blocked-cache-reply');
	if (thread) {
		try {
			threadCache = JSON.parse(thread);
		}
		catch (error) {
			// pass through
		}
	}
	if (reply) {
		try {
			replyCache = JSON.parse(reply);
		}
		catch (error) {
			// pass through
		}
	}
};

/**
 * 保存并没有什么卵用的缓存
 *
 * @param {string} key - 缓存 key
 */
const saveCache = (key) => {
	if (key === 'thread') {
		sessionStorage.setItem('tieba-blocked-cache-thread', JSON.stringify(threadCache));
	}
	else if (key === 'reply') {
		sessionStorage.setItem('tieba-blocked-cache-reply', JSON.stringify(replyCache));
	}
};

/**
 * 初始化执行
 *
 */
const init = () => {
	if (getIsLogin()) {
		const username = getUsername();
		const portrait = getPortrait();
		// loadCache();
		initListener();
		initStyle({ username, portrait });
	}
};

init();