Greasy Fork is available in English.
贴吧都快凉了,过去的痕迹都没了,你为什么还在刷贴吧呢?你们建个群不好吗?
// ==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>楼. <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(r###lt => { threadCache[tid] = r###lt; // saveCache('thread'); return r###lt; }); } } 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(r###lt => { replyCache[pid] = r###lt; // saveCache('reply'); try { if (r###lt && JSON.parse(target.dataset.field).content.post_no === 1) { document.querySelector('.core_title').classList.add('__tieba_blocked__'); } } catch (err) { // pass through } return r###lt; })); } } 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(r###lt => { replyCache[spid] = r###lt; // saveCache('reply'); return r###lt; }); } } if (checker) { Promise.resolve(checker).then(r###lt => { if (r###lt) { 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();